Read write LBA (a wrapper for INT 13H)

In this post, I write about the development of a wrapper around the INT 13H that I wanted to use instead of the naked interrupt.
Why a wrapper for INT 13H? Well, I mentioned already that when the code runs in real mode on the PC I am "alone in the dark" and I wanted to take some torchlight with me. I mean that despite intensive testing with DEBUG.EXE before deploying any "SOFTWARE.BIG", something can still go wrong in real address mode. When this happens, it is very hard trying to figure it out where the problem is especially if the problem cannot be reproduced in DEBUG.EXE. To make myself some help in case of problem, I wanted to create a wrapper around the INT 13H Read and Write that brings me some feedbacks on screen in case things went wrong. Looking on Table #00234 at the "Ralf Brown Interrupt List", one can find the error codes returned in AH, with the corresponding description of the error. I thought that it was a good idea to print on-screen the error code when INT 13H returned with any other additional helpful information. In the beginning, I thought that I could have coded some extra instructions soon after INT 13H returned everywhere I called it. After a short while, I realized that it was better to build a new service procedure that did it automatically every time I needed to use INT 13H. In the end, this service procedure is a wrapper around the call to INT 13H and its return value.
I would like to inform you that the wrapper RW_LBA is much bigger than I originally expected. This was because I wanted to have as much and as rich error messages as possible, so find yourself a comfortable place for reading: this is going to be a long post.

Fig. A - calling hierarchy of RW_LBA
Fig. A - calling hierarchy of RW_LBA

Let me present you the (partially incomplete) calling hierarchy of RW_LBA in Fig. A (I omitted to draw the further calls to INT 10H, INT 16H and INT 19H because the resulting picture was less clear with all calls rather than not). I continue presenting you the code in all sections meanwhile you can find the complete file RW_LBA.npp1 in the DOWNLOAD AREA.

Program opening of MAIN

[...] a 7c00 ;234567890123456789012345678901234567890123456789012345678901234567890123456789 ;-------10--------20--------30--------40--------50--------60--------70-------79 ;------------------------------------------------------------------------------ ; RW_LBA: Read / Write disk with LBA technique. ; ; Copyright (C) 2020 - Michele Musci ; Distributed under the GNU Affero General Public License version 3. ; See https://www.gnu.org/licenses/agpl-3.0.txt ; ; This procedure reads from or writes to disk with LBA technique. ; ; INPUT: AX = 4200h -> LBA Read ; AX = 4302h -> LBA Write with verify ; (on version >= 2.1). ; DS:[SI] -> 16 bytes (10h) LBA address packet. ; OUTPUT: CarryFlag set (1) -> TRUE (ERROR during execution) ; CarryFlag cleared (0) -> FALSE (execution OK) ; ; REG. USAGE: all ; THIS CALLS: SHOW_STR.BIN, WRTASCII.BIN, INT 13H, INT 16H ; ; Build it with command: ; debug < RW_LBA.npp > RW_LBA_dbg.npp ;------------------------------------------------------------------------------ ; ;------------------------------------------------------------------------------ ; ; Load service procedures into memory ; (load here service procedures with command "n" and "l" of debug.exe) ; ;------------------------------------------------------------------------------ n SHOW_STR.BIN l 7c10 n WRTASCII.BIN l 7c30 a 7c60 ;------------------------------------------------------------------------------ ; ; CODE (entry point of service procedure) ; ;------------------------------------------------------------------------------ ; [BP + 16] -> Return address of the caller push ax ; [BP + 14] -> AX push bx ; [BP + 12] -> BX push cx ; [BP + 10] -> CX push dx ; [BP + 0e] -> DX push ds ; [BP + 0c] -> DS push si ; [BP + 0a] -> SI <-- point here LDS SI, [...] push es ; [BP + 08] -> ES push di ; [BP + 06] -> DI <-- point here LES DI, [...] push bp ; [BP + 04] -> BP of previous stack frame push sp ; [BP + 02] -> SP call 7c6d ; push IP with call myself: ---+ ; | ; myself: <-------------------------------------------------+ ; mov bp, sp ; [BP + 00] -> IP_real sub word ptr [bp], 7c6d ; [BP + 00] -> Corr_Factor = (IP_real - IP_debug) ; jmp 7ec8 ; Jump Start: ---> ; ; ;------------------------------------------------------------------------------ ; END of Procedure. ; ; The reason for this strange way of preparing the code is that backwards ; jumps keep in 90% of cases the address meanwhile the code is growing ; so that I can keep extending this procedure without the need of fixing ; everytimes the jumps. ;------------------------------------------------------------------------------ ; ; exit_ok: <--- ; ; clc ; Clear Carry Flag (0) -> FALSE: ; execution OK. jmp 7c7f ; Jump reset_stack: ---> ; ; exit_error: <--- ; ;------------------------------------------------- ; wait for key press and terminate ;------------------------------------------------- xor ax, ax ; int 16/ah = 00 : keyboard get keystroke int 16 ; returns: ah = BIOS scan code ; al = ASCII character ; stc ; Set Carry Flag (1) -> TRUE: ; ERROR during execution. ; ; reset_stack: <--- ; mov sp, [bp + 02] ; Regardless what went on in the procedure ; we reset SP to its initial condition. pop bp ; We reset all registers to the initial pop di ; conditions. pop es pop si pop ds pop dx pop cx pop bx pop ax ret [...]

read_write_LBA is a service procedure, so I put no instruction at 0x7C00 but I started the program at 0x7C60 soon after the loading of the two other service procedures: show_string_II and write_ASCII. You may have noticed that I decided to push everything on the stack. The reason was that during the development, I was continuously taking notes of the used registers and then I kept going up and down from the stack set-up (in this session) to the code I was developing down in the file. After a while, I felt it as an annoying load and I decided to push everything on the stack. In the end, it was more safe to push all registers on the stack and it freed me from going up and down fixing the code, and most painfully updating the addresses. In fact, every time I added a PUSH and a POP instruction, new bytes of opcodes came in and those shifted all the addresses down the lines so I had to fix everything every time. Based on this experience I would say that:

  1. preserving all registers regardless of their effective usage in the procedure is a good thing especially if the code is going to be complex;
  2. put some spare NOP (mnemonic) or 0x90 (opcode) between the end of every session and the beginning of the next one is a good idea to realign the addresses in DEBUG.EXE.

Once I pushed all registers on the stack, I pushed the instruction pointer register too and I calculated the necessary correction factor directly on the stack2 to make the program fully relocatable.

You can also observe that I put the start and the end of the procedure immediately one after the other and I performed a jump from procedure opening over procedure closing to the real beginning of the code. Once the code-core had finished its job, it had to jump back to the exit procedure which I located here at the beginning of the file. This is an unusual way of writing the program. Everybody would expect to find the end of the program at the end of the file and not at the beginning. The point is that DEBUG.EXE converts the mnemonics in opcode immediately as I was writing them, which means that all addresses of code and data, which were already written (so in the past, before the actual point of writing), were also known, meanwhile all addresses of the still not written program (so in the future, after the actual point of writing) were just unknown. As a consequence, I had to (or I preferred to) call and jump towards known addresses in DEBUG.EXE so I organized the code in such a way that the main procedure was just at the end of everything. In this way, I knew where to jump, where to call and where to point strings that were all defined before the main procedure. I think that, after this explanation, you can understand what I mean when I say that "I used to structure my code in DEBUG.EXE in such a way that the backward jumps occurred more often than the forward ones".

DATA

[...] 137B:7C8C ;------------------------------------------------------------------------------ 137B:7C8C ; 137B:7C8C ; DATA 137B:7C8C ; 137B:7C8C ;------------------------------------------------------------------------------ 137B:7C8C DB 'RW_LBA:', 0d, 0a, 00 137B:7C96 DB 'AX = 0x' 137B:7C9D DB '???? is invalid!', 0d, 0a 137B:7CAF DB 'AX can be either 0x4200 (read) or 0x4302 (write and verify).', 0d, 0a, 00 137B:7CEE DB 'SI points to an implausble LBA Packet!', 0d, 0a, 00 137B:7D17 DB 'Read error!', 0d, 0a, 00 137B:7D25 DB 'Write error!', 0d, 0a, 00 137B:7D34 DB 'Error code = 0x' 137B:7D43 DB '??.', 0d, 0a 137B:7D48 DB 'Number of blocks successfully transferred = 0x' 137B:7D76 DB '????. ', 0d, 0a 137B:7D7E DB 'SI = 0x' 137B:7D85 DB '????.', 0d, 0a 137B:7D8C DB 'LBA Packet: 0x' 137B:7D9A DB '???? 0x' 137B:7DA1 DB '???? 0x' 137B:7DA8 DB '????_' 137B:7DAD DB '???? 0x' 137B:7DB4 DB '????_' 137B:7DB9 DB '????_' 137B:7DBE DB '????_' 137B:7DC3 DB '????.', 0d, 0a, 00 137B:7DCB DB '=======================================', 137B:7DF2 DB '=======================================', 0d, 0a, 00 137B:7E1C DB 'COPY PLACE LBA..' 137B:7E2C ; [...]

After the opening and closing sequence of the main program, I placed the block of data required for the program, which consisted mainly of strings (but not only).

The way I prepared the strings in memory was such that it made it easier for me to read, on the left side, the addresses I wanted. For instance, you can consider the string: "LBA packet: 0x???? 0x???? 0x????_???? 0x????_????_????_????.". I placed the start of it at address 0x7D8C and I "folded it" (so to say) at each point where I had to convert the place holder "????" in the actual value at runtime. In this way, I could immediately read the addresses of the place holders from the DEBUG.EXE.

According to the "Ralf Brown Interrupt List", the INT 13H, depending on the error occurred, can modify the original LBA package, and it returns the real effective number of blocks transferred, in the same position of the required number of blocks for the transfer. Suppose that we had 5 blocks to transfer at the offset 0x02 in the LBA address packet3 and during a read or write operation, after that the third block was successfully transferred, an error occurred. In that case, INT 13H would have returned with an error code in AH and overwritten the original 5 blocks with 3 (3 blocks were transferred until error). In the past, I used to try reading with INT 13H three times in a row before giving it up, now suppose that such an error, as described here, occurred on the first attempt, then the LBA packet used for the following two attempts were bad. But even worst, you can suppose that the first attempt went wrong and that INT 13H had modified the LBA packet; so imagine that the second attempt (with the modified packet) was successful, then the transfer in RAM would have been different from the desired one, with memory corruption and no error on the screen. Finally, I realized that the INT 13H could modify the LBA packet which is indeed in the memory space of the caller procedure. For this reason, I created a place holder at address 0x7E1C (pre-filled with 0x10 Bytes: "COPY PLACE LBA..") where to copy the original LBA packet before calling INT 13H. In this way, I was always able to distinguish between the original LBA packet and the eventually modified one from INT 13H.

The local procedure BAD_AX

After the strings, I placed the tree local procedures BAD_AX, RETRIVE_LBA_PACKET and BAD_SI before the core of the MAIN procedure. The hierarchy among main procedure, local procedures and service procedures was illustrated already in Fig. A.

[...] ;------------------------------------------------------------------------------ ; ; LOCAL PROCEDURES ; ;------------------------------------------------------------------------------ ;------------------------------------------------------------------------------ ; BAD_AX ;------------------------------------------------------------------------------ ;------------------------------------------ ; AX contains a bad value for the call. ; We store the information in string ; "AX = 0x???? is invalid!" ;------------------------------------------ mov bx, ax ; mov di, 7c9d ; DI points the first "?" of string ; in debug address space. add di, [bp] ; Address conversion from debug to real. mov cx, 04 ; 4 nibbles will be converted. call 7c30 ; call WRTASCII ; ; ;------------------------------------------- ; Show error message on screen ;------------------------------------------- mov si, 7c8c ; "RW_LBA:" in debug address space. add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ; mov si, 7c96 ; "AX = 0x???? is inva..." in debug address space. add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ; mov si, 7dcb ; "===..." in debug address space. add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ; ; ;------------------------------------------------------------------------------ ; END of local procedure BAD_AX ;------------------------------------------------------------------------------ ret ; [...]

I checked, in the core of the MAIN procedure, the value of the register AX and if it was bad, I called BAD_AX to prepare and send the appropriate error message on the screen. If you consider that, the program called BAD_AX only one time from one place, you could tell me that I would better write the instructions for BAD_AX inline rather than have such instruction grouped as a separate sub-procedure. Honestly, I think you are right. If you ask me why I did that, the simple and honest answer is my lack of experience. After all, I am a hobbyist in this field.

The local procedure RETRIEVE_LBA_PACKET

[...] ;------------------------------------------------------------------------------ ; RETRIEVE_LBA_PACKET ;------------------------------------------------------------------------------ ;------------------------------------------ ; Si points to an implausible LBA packet. ; We store the information in string ; "SI = 0x????." ;------------------------------------------ mov si, [bp + 0a] ; Original SI. This is used few lines ahead again. mov bx, si ; Copy SI. mov di, 7d85 ; First "?" of string in debug address space. add di, [bp] ; Address conversion from debug to real. mov cx, 04 ; 4 nibbles will be converted. call 7c30 ; call WRTASCII ; ; ;------------------------------------------- ; Retrieve the LBA packet ;------------------------------------------- mov ax, 7dc3 ; Push first "?" of each part of the LBA string push ax ; on stack in reverse order. mov ax, 7dbe ; This is required to fill the pipeline of push ax ; addresses to be used later in the loop. mov ax, 7db9 ; push ax ; mov ax, 7db4 ; push ax ; mov ax, 7dad ; push ax ; mov ax, 7da8 ; push ax ; mov ax, 7da1 ; push ax ; mov ax, 7d9a ; push ax ; ; ; ;------------------------------- ; FOR CX = 8, to 0, dec cx ;------------------------------- mov cx, 8 ; ; ; loop_start: <--- ; mov dx, cx ; store the value of CX to resume it before loop. ; WRTASCII needs CX as selector for the nibbles as ; well. ; lodsw ; AX = [DS:SI]. SI = SI +2 mov bx, ax ; pop di ; First "?" of each part of the string ; in debug address space. add di, [bp] ; Address conversion from debug to real. mov cx, 04 ; 4 nibbles will be converted. call 7c30 ; call WRTASCII ; ; ;------------------------------- ; NEXT cx <= 4, to 0, dec cx ;------------------------------- mov cx, dx ; Restore the counter for CX before loop. loop 7e8a ; loop loop_start: ---> ; dec cx ; jnz ; ; ;------------------------------------------------------------------------------ ; END of local RETRIEVE_LBA_PACKET ;------------------------------------------------------------------------------ ret [...]

There were two callers for RETRIEVE_LBA_PACKET: BAD_SI and the MAIN of RW_LBA. RETRIEVE_LBA_PACKET was legitimately a sub-procedure. In the program flow, I checked first of all the inputs. Once AX was confirmed to be good, I moved on and checked SI. Were SI bad, then I called BAD_SI to prepare and send the appropriate error message on the screen. If SI were bad, was it so because it was pointing to an implausible LBA packet, so I wanted to have back on screen the LBA packet too and for this reason, I created RETRIVE_LBA_PACKET.

IMPORTANT!!!
The version of RETRIVE_LBA_PACKET here on the post contains a bug!!!
The one on the file RW_LBA.npp in the DOWNLOAD AREA not. I decided to put the version with the bug here in the post because I wanted to talk with you about that bug and challenge you to spot the bug immediately before I move on and show it to you. I hope you don't cheat and spot the bug just using a text compare between the RETRIVE_LBA_PACKET here on the post and the one that you can find in the DOWNLOAD AREA. Remember that errors are the most valuable learning experience one can do, as a consequence, trying to spot the bug and solve the error using your investigative intelligence is always the best way to go.

Besides the bug, RETRIVE_LBA_PACKET filled the place holders "????" of the LBA packet with the real values at runtime.

The local procedure BAD_SI

[...] ;------------------------------------------------------------------------------ ; BAD_SI ;------------------------------------------------------------------------------ ;------------------------------------------ ; retrieve SI and LBA packet ;------------------------------------------ call 7e56 ; Call RETRIEVE_LBA_PACKET ; It fills all "?" in the strings for SI ; and LBA packet. ; ; ;------------------------------------------- ; Show error message on screen ;------------------------------------------- mov si, 7c8c ; "RW_LBA:" in debug address space. add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ; mov si, 7cee ; "SI points an unplau..." in debug address space. add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ; mov si, 7d7e ; "SI = 0x????." in debug address space. add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ; mov si, 7dcb ; "===..." in debug address space. add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ; ; ;------------------------------------------------------------------------------ ; END of local BAD_SI ;------------------------------------------------------------------------------ ret [...]

BAD_SI was the cousin of BAD_AX (so to say). Same as BAD_AX, the purpose of BAD_SI was to fill the place holders "????" with the actual value at runtime and put the strings on the screen. As BAD_AX, BAD_SI was called just one time by the MAIN procedure so I had better coded it inline rather than as separate local procedure.

The MAIN: check inputs

[...] ;------------------------------------------------------------------------------ ; ; START MAIN procedure ; ;------------------------------------------------------------------------------ ; ; start: <--- ; ;----------------------------------------- ; check that AX is legal. ;----------------------------------------- cmp ax, 4200 ; flags = AX - 0x4200 je 7ed8 ; JumpEqual isOK_A: ---> ; cmp ax, 4302 ; flags = AX - 0x4302 je 7ed8 ; JumpEqual isOK_A: ---> ; ; ; error: ; call 7e2c ; Call local function BAD_AX jmp 7c7a ; Jump exit_error: ---> ; ; ; ; isOK_A: <--- ; ;----------------------------------------- ; check that SI points a plausible ; LBA packet ;----------------------------------------- mov ax, [si] ; cmp ax, 0010 ; flags = AX - 0x0010 je 7ee5 ; JumpEqual isOK_B: ---> ; ; error: ; call 7ea0 ; Call local function BAD_SI jmp 7c7a ; Jump exit_error: ---> ; ; ; ; isOK_B: <--- ; [...]

I started the main with the input control and I do believe that the input control is THE MOST important thing ever in every software development. When I write a procedure, I don't make any assumptions about what is outside the procedure. The procedure knows the outer word ONLY thru the inputs and talks to the outer word ONLY thru the outputs. It is my responsibility to specify the requirements for the inputs in the way I need them to be for the job of the procedure and to check that the inputs are according to the specification. I strive myself to have a bullet-proof input control as much as I can and, despite the extra code and stress that it costs, it is always worth to do it.

In this case, AX could have been either 0x4200 (read) or 0x4302 (write with verify). The case of SI was a little bit more complicated because SI was just pointing to the LBA packet so any value of SI was legitimate in principle. What I had to check was the LBA packet pointed by SI, in order to tell if SI was pointing valid data or a random location in RAM. The only thing I could test was the beginning of the LBA packet: it had to be 0x10 bytes long. Besides this simple check, it wasn't possible to check anything more than that based on the information available at the input of the MAIN procedure.

The MAIN: invoke INT 13H

[...] ; ; isOK_B: <--- ; ;----------------------------------- ; Try 3 times a read / write ; WARNING!!! ; INT13 may destroy the original ; LBA packet so we have to rebuild ; it from original at every attempt. ;----------------------------------- cld ; Clear dir. flag -> make auto increment SI, DI. mov di, 7e1c ; DI points the destination of the copy ; in debug address space. add di, [bp] ; Address conversion from debug to real. ; mov cx, 03 ; number of attempts ; ; try_again: <--- ; push di ; Store DI because it will be reused by each ; attempt and we don't want to recalculate it. push cx ; We have to preserve CX of the loop counter ; because we are going to reuse CX for the MOVSW ; operation. push di ; Store DI because it will be used by SI as soon ; as the copy of the LBA package is done. mov cx, 8 ; The LBA packet is 16 bytes long hence 8 words ; are enough for the copy. mov si, [bp + 0a] ; Recover original SI that points to original LBA ; packet. ; repz ; Step 1 -> if CX = 0 terminate string operation. movsw ; Step 2 -> execute pending interrupt (if any). ; Step 3 -> DEC CX. ; step 4 -> ES:[DI] = word at DS:[SI] ; step 5 -> DI = DI + 2; SI = SI + 2 ; mov ax, [bp + 14] ; AX gets destroyed by INT13 on error, so we have ; to restore the original value by each attempt. mov dx, 80 ; DL = 0x80 (first HDD) pop si ; LBA package can get destroyed by INT13 on error, ; so we copy the original by every attempt and ; point SI to the copy. int 13 ; Issue INT13; return CF = 1 on error. ; pop cx ; Reload the loop counter CX and DI HERE BEFORE ; CHECK of carry flag because in either cases ; of the branch we have to restore the stack. pop di ; Reload the DI pointer for next loop. jc 7f0a ; Jump over next instruction because conditional ; jumps are just short +127 / -128 from IP. jmp 7c77 ; Jump exit_ok: ---> ; mov bl, ah ; Error code in AH. It goes in BL for conversion. ; AH contains the error code, but we need to set ; AX to 0 in order to reset INT13. So we have to ; save a copy of it in BL. xor ax, ax ; INT13 / AX = 0000 --> reset HDD int 13 ; loop 7eef ; loop try_again: ---> ; ; ; ; error ; ;------------------------------------------- ; BL contains the error code that was in AH. ; We store the error code in string ; "Error code = 0x??." ;------------------------------------------- [...]

Finally, I can show you the core of the MAIN. I don't think I can add much more than what I wrote already in the comments. Maybe I should tell that this block of code had two possible ways out: either I had a successful INT 13H operation, in which case I jumped out and back to the exit_ok: label, or once all attempts were done, the code followed thru in the error detection branch.

The MAIN: final error handling

[...] ; ; error ; ;------------------------------------------- ; BL contains the error code that was in AH. ; We store the error code in string ; "Error code = 0x??." ;------------------------------------------- mov di, 7d43 ; First "?" of string in debug address space. add di, [bp] ; Address conversion from debug to real. ; BL contains already the error code. mov cx, 02 ; 2 nibbles will be converted. call 7c30 ; call WRTASCII ; ; ;------------------------------------------- ; DS:[SI + 02] contains the number ; of blocks successfully transferred. ; We store this information in string ; "Number of blocks successfully..." ; IMPORTANT!!!! ; SI points the copy of the LBA which is ; the one that gets modified by INT13 on ; error, so it is correct that we find ; the information here! (and not in the ; original LBA package). ;------------------------------------------- mov di, 7d76 ; First "?" of string in debug address space. add di, [bp] ; Address conversion from debug to real. mov bx, [si + 02] ; Number of blocks successfully transferred ; goes in BX for conversion. mov cx, 04 ; 4 nibbles will be converted. call 7c30 ; call WRTASCII ; ; ;------------------------------------------- ; Probably SI points a bad LBA packet. ; retrieve original SI and ; original LBA packet. ;------------------------------------------- call 7e56 ; Call RETRIEVE_LBA_PACKET ; It fills all "?" in the strings for SI ; and LBA packet. ; ; ;------------------------------------------- ; Show error message on screen ;------------------------------------------- mov si, 7c8c ; "RW_LBA:" in debug address space. add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ; ;------------------------------------------- mov si, 7d17 ; "Read error!" in debug address space. mov ax, [bp + 14] ; AX gets destroyed by INT13 on error, ; so we have to restore the original value. cmp ax, 4200 ; Flag = AX - 0x4200 je 7f47 ; JumpEqual is_read: ---> mov si, 7d25 ; "Write error!" in debug address space. ; ; is_read: <--- ; add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ;------------------------------------------- ; mov si, 7d34 ; "Error code = 0x" in debug address space. add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ; mov si, 7dcb ; "===..." in debug address space. add si, [bp] ; Address conversion from debug to real. call 7c10 ; Call SHOW_STR ; ; ;------------------------------------------------------------------------------ ; END of Procedure MAIN. ;------------------------------------------------------------------------------ jmp 7c7a ; Jump exit_error: ---> ; [...]

This last part of the code was exactly what I was doing all of this for: an error happened and I was able to collect a lot of information about the condition causing the error. So I started collecting all this information from the error code returned by INT 13H. Then I collected the total number of block transferred but I knew that this information wasn't always relevant. It depended on the actual error code detected. I decided to print back information on the screen always because I could have checked in the "Ralf Brown Interrupt List" the error code and know whether the information on the screen was relevant or not. At latest, I collected the original LBA packet: the one passed in input and not the one potentially overwritten by INT 13H.

Initial tests

Fig. B - Check value of AX in input
Fig. B - Check value of AX in input

When I meant that the program was finished, I started a final set of tests which, according to my expectation, had just to confirm that everything was running smooth, instead I discovered the bug in RETRIVE_LBA_PACKET (as mentioned before). The address 0x7C00 was free to use (as you can tell by the program opening of MAIN) and I put there the trigger for the test. I assembled a direct call to the entry point of read_write_LBA, then a backward jump to reset the initial condition and finally the bytes 0x10 and 0x00 to be used for the testing of the LBA packet. In Fig. B, you see that the first test was ok. The program detected the illegal value for AX.

Fig. C - Check plausibility of LBA packet in input
Fig. C - Check plausibility of LBA packet in input

I changed the value of AX and I repeated the test (Fig. C). This time the program detected the implausibility of the LBA packet addressed by SI. As I told you, there was a bug in RETRIVE_LBA_PACKET which was affecting the result on the screen here, but it was impossible to detect the bug just by this test.

Fig. D - Detection of write error
Fig. D - Detection of write error

In the next testing step (Fig. D), I changed the value of SI and I made it point to the address 0x7C05 where the test LBA packet was starting with the word 0x0010 (I prepared it, as you saw in Fig. B). In my intention, this test had to go thru the input control and check the core. The test of the core was a success. read_write_LBA reported a write error since DEBUG.EXE doesn't support the INT 13H extension in its simulated environment. Finally, this is the picture that showed for the first time the bug of RETRIVE_LBA_PACKET. Did you find it? Not yet? I was also not able to find it at late evening; I realized the problem only the morning after when I was looking back at the screenshot and preparing my working notes in the diary.

The problem laid in the representation of the LBA packet on screen. The procedure write_ASCII stored in memory the content of a register from left to right in the same way we write and read: in the same way a string is located in memory and printed on the screen. In other terms, in the case of the register SI = 7C05, the representation on the screen was correct (highlighted in blue in Fig. D). On the contrary, once I took words of memory to be converted into strings the bytes got flipped. As you can see in Fig. D, the LBA packet started with 0x0010 which was the value it became once the little-endian 0x1000 was read into the register4. You can see the problem again in the last three words of the LBA packet of Fig. D if you compare them with the memory dump of Fig. B. These last three words had to be 0x3F50_53B4_0EBB to be right. One cannot spot the problem in the rest of the string because 0x3F3F turned back again 0x3F3F after swapping the bytes. So I had to fix this point before continuing.

Final test

[...] ; ; loop_start: <--- ; mov dx, cx ; store the value of CX to resume it before loop. ; WRTASCII needs CX as selector for the nibbles as ; well. ; lodsw ; AX = [DS:SI]. SI = SI +2 mov bx, ax ; ;------------------------------------------------------------------------------ ; IMPORTANT!!! ; xchg bh, bl is a very crucial instruction in order to get back on screen ; the same representation of memory dump. In fact the bytes get swapped when ; read into registers, so you have to swap them back before writing them ; as ASCII in the string. ;------------------------------------------------------------------------------ xchg bh, bl ; IMPORTANT!!! It fixes the little-endian swap. ; pop di ; First "?" of each part of the string ; in debug address space. add di, [bp] ; Address conversion from debug to real. mov cx, 04 ; 4 nibbles will be converted. call 7c30 ; call WRTASCII ; ; [...]

The little-endian bug fix in RETRIEVE_LBA_PACKET was easy and painful at the same time. It was easy simply because I had to add a swap of bytes with the instruction XCHG BH, BL to fix it (I provide you with a short fragment of code where I did the correction). It was painful because the insertion of this instruction scrolled all the program in the address space from the point of insertion onwards. The consequence for me was a long, annoying and painful check and fix of all jumps and calls in the program. That was the most terrific limitation of writing code directly in DEBUG.EXE, but I hadn't any other tool at that time.

Fig. E - Final test
Fig. E - Final test

Once the painful check and fix of all address was over, I restarted the tests. I bring here one screenshot in Fig. E to show you the little-endian fix. Please notice the bytes marked with green colour. When I presented them as a memory dump of the LBA packet they appeared in the little-endian form 0x1122. When I presented them as the total number of block transferred, I considered them as a number so I displayed 0x2211 (the high part first and the low part in the end).

The creation of the procedure read_write_LBA was a lot of work for me, but a very good learning experience at the same time indeed.



  1. I remember you that RW_LBA.npp is a plain text file that you can open with any text editor, but I associated the extension ".npp" with NOTEPAD++ for my convenience. If you don't remember or haven't read the post "Change of gear" yet, you can do it now and find the naming convention adopted for the files. [click back]
  2. I wrote a post about the problems related to the creation of a truly relocatable program and how I solved them. In the post "Relocatable code" I explained in detail this technique which consists of pushing IP on the stack, calculating a correction factor and using it every time one has to address a piece of data. [click back]
  3. You may want to look back again at Fig. A of the post "Read the first block of HDD". [click back]
  4. I have a separate post about "Little Endian" that you can read if you need a refresh on this topic. [click back]

<PREV.  -  ALL  -  NEXT>

Comments