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 |
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
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:
- preserving all registers regardless of their effective usage in the procedure is a good thing especially if the code is going to be complex;
- 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
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.
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
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 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
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
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
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 |
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 |
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 |
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
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 |
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.
Comments
Post a Comment