3 April 2004 Hard-disk reads I took some time off from Karig, then came back to figure out how to read sectors from the hard disk into memory. It took me a while to figure out how to do all this correctly. Code My latest boot sector is inspired by "Reading the harddisk using ports" by "qark." I also used information found in Chapter 11 of Frank van Gilluwe's book The Undocumented PC (2nd ed.). The command to reset the hard-disk controller wasn't in either of these sources, however; I found it in a list of hardware ports taken from Part D of Ralf Brown's massive Interrupt List. The boot sector sets up the stack and segment registers as usual, and then it clears the screen. Then it sets things up for a call to the BIOS to read a sector from the hard disk into memory. Using the BIOS The next four lines of code need a little explanation. mov ax, 0x0201 mov bx, 0x1000 mov cx, 0x13FF ; ch = cylinder, cl = sector mov dx, 0x7F80 ; dh = head First of all, they set things up for a call to the BIOS function INT 13h, AH=02h, which reads one or more 512-byte sectors from the hard disk. I'll rewrite the above lines for clarity: %define cyl 787 %define head 127 %define sect 63 %define sect_cl (sect + ((cyl & 0x300) >> 2)) %define cyl__ch (cyl & 0xFF) mov al, 0x01 ; Number of sectors to read into memory. mov ah, 0x02 ; Function 2: Read sector. mov bx, 0x1000 ; Address of sector buffer. ; (Buffer is in segment zero; ES already points there.) mov cl, sect_cl mov ch, cyl__ch mov dl, 0x80 ; Drive number (0x80 = first hard disk) mov dh, head Here I'm loading the last sector on my hard disk. When I press the Del key at startup and look at the BIOS screen for hard-disk detection, I see that the BIOS sees the hard disk as containing 788 cylinders, 128 heads (tracks) per cylinder, and 63 sectors per track. Cylinders and heads are numbered from zero up, but sectors are numbered from one up — so the last cylinder is 787 and the last head is 127, but the last sector is 63 (not 62). Now note the odd way that the cylinder and sector numbers are specified. The sector number fits into six bits, but the cylinder number requires ten bits. A byte register like CH can hold no more than eight bits, so two of the bits from the cylinder number have to share space within the CL register with the six bits from the sector number. Specifically: cyl = 787 = binary 11 0001 0011 (10 bits) sect = 63 = binary 11 1111 (6 bits) CH = bits 0..7 (bottom eight bits) of cyl = 0001 0011 = 0x13 CL has two parts: - Bits 0..5 (bottom six bits) contain sect = 11 1111 - Bits 6..7 (top two bits) contain top two bits of cyl = 11 - Result = binary 11 111111 = 0xFF After setting up the registers as above, I save the sector-buffer address on the stack and call the BIOS function to read the sector into memory. push bx int 0x13 I then save the error code to address 0000:1F00. mov [0x1F00], ah Finally, I dump the first 128 bytes of the sector buffer, enter a blank line on the screen, and dump the location where I saved the error code. (If the sector was read in correctly, the error code should be zero.) pop bx call dump_16 call dump_16 call dump_16 call dump_16 call dump_16 call dump_16 call dump_16 call dump_16 call next_row ; Dump error byte. mov bx, 0x1F00 call dump_16 Using the I/O ports Using the BIOS is fine if the machine is in real mode, but Karig will work in protected mode, and the BIOS doesn't work in protected mode. If I want to read sectors from the hard disk in protected mode, I have to use the I/O ports — that is, I have to send commands to the hardware myself. Here the plan is to reload the same sector I just loaded using the BIOS, but load it into a different location, and send commands to the hardware to load the sector. First I turn off interrupts. I don't know if this is truly necessary, but it seems prudent to do this. cli I pass to the hard-disk controller the number of sectors to read (via port 0x1F2). mov dx, 0x1F2 mov al, 1 ; # sectors to transfer Next, I pass the sector number to the hard-disk controller via ports 0x1F3 through 0x1F6. inc dx ; mov dx, 0x1F3 mov eax, 0x60F5FF ; LBA out dx, al inc dx ; mov dx, 0x1F4 shr eax, 8 out dx, al inc dx ; mov dx, 0x1F5 shr eax, 8 out dx, al inc dx ; mov dx, 0x1F6 mov al, 0xE0 out dx, al This obviously requires some explanation. First, note that I am specifying an LBA number — an absolute sector number — instead of the cylinder, head, and sector (CHS) values for the sector. To convert CHS into LBA, you calculate LBA as follows: LBA = (cylinder-number x heads-per-cylinder x sectors-per-track) + (head-number x sectors-per-track) + (sector-number - 1) In this case: cylinder-number = 787 (0x313) head-number = 127 (0x7F) sector-number = 63 (0x3F) heads-per-cylinder = 128 (0x80) sectors-per-track = 63 (0x3F) LBA = (787 x 128 x 63) + (127 x 63) + ( 63 - 1) Thus LBA = 6,346,368 + 8,001 + 62 = 6,354,431 (0x60F5FF). Also note that an LBA is supposed to be 28 bits long. Here I specify only the bottom 24 bits and assume that the top four bits will be zero. In this particular case, this is true; the LBA value 0x60F5FF fits in 24 bits. The value I stuff into port 0x1F6 is 0xE0. The bottom four bits are the highest four bits of the 28-bit LBA value. The top four bits are as follows:
Value 0xE0 = binary 1110 0000
-- Bit 7 is 1 -- Must always be 1
-- Bit 6 is 1 -- Set to 1 to specify sector using LBA
Set to 0 to specify sector using CHS
-- Bit 5 is 1 -- Must always be 1
-- Bit 4 is 0 -- Set to 0 to specify hard disk 0 (master)
Set to 1 to specify hard disk 1 (slave)
Now that the sector number has been sent, I send the command to read the sector (and to retry several times automatically if the read doesn't happen correctly): inc dx ; mov dx, 0x1F7 mov al, 0x20 ; read with retry out dx, al Now I wait. The command to read the sector has been sent, but I still have to wait for the hard-disk controller to tell me that the sector data is actually available for me to read. The reason, of course, is that the hard disk platters have to rotate into place, so that the sector I want to read is sitting right under one of the hard disk's read heads. These platters do rotate very quickly in human time — a 7200 rpm hard disk spins its platters 120 times per second, or once every 8.3333 milliseconds. However, the computer can still execute many instructions in the time it takes for a platter to rotate. Therefore I have to keep asking the hardware if the data is ready yet. (Other operating systems set up an interrupt, so that the processor can do other things while the hard disk platters are turning, and the hard-disk controller can interrupt the processor later when the data is ready. However, here I'll keep things simple.) To see if the data is ready, I have to read a byte from port 0x1F7 (the port I just used to send the read-with-retry command) and check bit 4. If the bit is set, then the data is ready to be read; otherwise I continue to wait. ; Wait for data to be ready. .rdy: in al, dx and al, 8 jz .rdy Once the data is ready, I read 256 words (512 bytes) from port 0x1F0 and store them into the second sector buffer (at 0000:2000). (I also save the buffer address in BX so that I can dump the buffer contents a little later.) ; Read data. mov dx, 0x1F0 mov cx, 256 mov di, 0x2000 mov bx, di cld rep insw Now I check for errors. Here I just read a byte each from ports 0x1F7 and 0x1F1 and store them into memory at 0000:1F00. A real hard-disk driver would read the byte from 0x1F7, and if the byte is odd (i.e., bit 0 is set to 1), it would read the error-code byte from 0x1F1. (Bits 0, 6, and 7 of this error-code byte, if set, would indicate a bad or even unusable sector; bit 4, if set, would indicate a bad sector number or some other problem with the read request.) ; Get error bytes. push bx mov dx, 0x1F7 in al, dx mov [0x1F00], al mov dx, 0x1F1 in al, dx mov [0x1F01], al Now I need to do something about that hard-disk light; at this point it's still on, even though the data has been read and the job is finished. So I reset the hard-disk controller. Note that it is necessary to wait again. Simply sending the reset command immediately doesn't work; the light stays on. I have to wait for the controller to tell me that it's not busy. ; Wait for not-busy. mov dx, 0x1F7 .bsy: in al, dx and al, 0x80 jnz .bsy ; Turn off drive motor. mov dx, 0x1F7 mov al, 0x08 ; rest out dx, al Now I can reactivate interrupts, dump the address where the two error bytes were stored, print a blank line, dump the first 128 bytes of the second sector buffer, and hang the computer. sti ; Dump error bytes. mov bx, 0x1F00 call dump_16 call next_row pop bx ; ------ Dump sector. call dump_16 call dump_16 call dump_16 call dump_16 call dump_16 call dump_16 call dump_16 call dump_16 ; ------ Hang computer. jmp $ The two sector dumps should match, byte for byte. I tried this routine not only on the last sector of the hard disk, but also on many other sectors — I simply changed the numbers sent to the BIOS and to the hardware and reassembled and reran the code. So I'm satisfied that the code works. The only sector where this code doesn't work on my Soyo laptop, for some reason, is on sector 0 — the very first sector on the hard disk. I can't read the MBR using the ports with the above code for some reason. I can apparently read any sector on the hard disk except the very first one. (Reading the MBR into memory using the BIOS, however, does work.) However, I tried code very similar to the code above on my computer at work, and I was able to read the MBR into memory using the ports. So I suspect that there is a bug in the hard-disk controller on my Soyo laptop, one that causes it to throw an error if the sector number (LBA or not) is zero. Whatever the reason is, though, I'll just avoid writing code that tries to access the MBR from within Karig itself. Check the index for other entries. |