Hello everybody, it’s me, Lovelace! In this post, you will learn how the hard disk works, its real mode interrupts and how we can read sectors from it.
How does the hard disk work?
The first thing that you need to know about disks, is that files do not exist on them, a hard disk doesn't know what a file is, it's the responsibility of the kernel to have a filesystem implementation. A filesystem is a special data structure that explains files, it's up to the kernel to read these data structures correctly, the disk doesn't have the concept of files, it just holds blocks of data which are called sectors.
If you want your kernel to understand files, you have to create a filesystem driver that targets the one that's on the hard disk. Data is typically written in sectors of - usually - 512 bytes each. For example, we would write 512 bytes to sector three and then we would read 512 bytes from sector three. That's how it works. If we were to read from sector three, the disk would return 512 bytes of data, we will then read that data into memory.
The old style of reading disks is called CHS (Cylinder Head Sector) and how it works is, sectors are read and written by specifying a head track and a sector so you actually have to select where in the disk you want to read (as shown in the above image). This is a pretty old way of doing it and it's generally more complicated as well because you need to know things such as how many sectors there are, how many cylinders, how many tracks and how many heads, and it will be more complicated. We will use CHS for demonstration purposes, but then, we will use LBA to use our hard disk.
LBA (Logical Block Address) is the modern way of reading from a hard disk. Rather than specifying "head", "track" and "sector", we just specify a number that starts from zero. For example, if we read
LBA (0), that would return the first sector on the disk. It also allows us to read from the disk as if we are reading blocks from a very large file, for example, let's say we have a 20 GB file you might do something like:
fread (ptr, 512, 6, fptr);
To read 6 blocks of 512 bytes each from
fptr, that's exactly how LBA works.
Now let's talk about something that we will encounter later on when working with the hard disk, how do we know in what sector is a file located at byte 65149 on the disk?
First, we have to divide the position by 512 (sector size) to get our LBA, "65149 / 512 = 127". The result of that division is our LBA but we haven't finished yet, because we also need to get an offset as that division might not be perfect. Knowing the LBA, we can load it into memory, reading 512 bytes and what's left to do is to get that offset to know where our file is in the loaded buffer, to get the offset we need to get the remainder of the number of bytes by 512, in this case, "65149 % 512 = 125", that means, the file we want to read is 125 bytes ahead in our buffer. You can test this math by doing "127 * 512 = 65024" and then adding our offset to that result "65024 + 125 = 65149". Loading data from LBA blocks is not hard at all, right?
While we are in real mode, the BIOS provides the
int 13 for disk-related operations, so we don't have to write our own disk driver (for now) to read and write to it. Let's use it.
Using the hard disk in Real mode
Before we continue writing code for our project, let's write a Makefile to automate the compilation and execution of our kernel. For now, let's keep this Makefile easy:
all: nasm -f bin boot.asm -o boot.bin run: qemu-system-x86_64 -hda boot.bin
Write the contents of that snippet to a file called Makefile.
Once that file is in the root of our project directory, you can just execute
make in a terminal emulator to compile our kernel, and
make run to run it.
As our operating system grows, the Makefile will do as well, if you don't know Make I'd recommend you search on that (it's not necessary to be an expert with it, my Make abilities are so bad yet good enough to manage all my projects).
Also, in our project root directory let's create a new file, which will be the one that we will load and read from the disk, you can call this file whatever you want, for example,
an_amazing_file_to_be_read_from seems to be a good name, we will just store a simple message there, so it won't matter that much, the message that mine will print is "Become Ungovernable!".
Now, we will need to add another rule to our Makefile, so this file is also added to the final file of our operating system, just add the following command to it:
all: nasm -f bin boot.asm -o boot.bin dd if=an_amazing_file_to_be_read_from >> boot.bin
Now, if you execute
make in a terminal emulator and open the
boot.bin file with a hex editor (I will use emacs) you'll see an output like the following:
As you can see, there's our message after our bootloader and the zero-padding, but if you remember my other posts, you might already know that there's an error, we also need to pad our message to be one sector in size, we can do that by adding another command to the
dd if=/dev/zero bs=512 count=1 >> boot.bin
What it will do, is to basically write one sector to our
boot.bin file. If you execute
make again, and open the boot.bin file with a hex editor, you'll see that now our message fits in a zero-padded sector.
Once we are finished preparing all this, we are ready to start writing code.
Note: Ralf Brown's Interrupt List will be a magnificent resource when working with the hard disk, I recommend you to read more on that.
As you can see in the above link, we will be using the Int 13/AH=02h to read sectors from the hard disk. These are the parameters this interrupt expects:
AH = 02h AL = number of sectors to read (must be nonzero) CH = low eight bits of cylinder number CL = sector number 1-63 (bits 0-5) high two bits of cylinder (bits 6-7, hard disk only) DH = head number DL = drive number (bit 7 set for hard disk) ES:BX -> data buffer
Note: When the BIOS loads our bootloader, the DL register (drive number) is already set to the drive number automatically, so we won't need to set this. Also, the data read is saved in the ES segment, and BX is the offset. The Carry flag (CF) is set if there was an error.
Before we start writing code, to keep our bootloader as clean as possible, let's remove the interrupts we made in the past lesson, the
handle_int0 label, the two lines where we modify the IVT, the
message label and the
mov si, message instruction.
Now, let's read from the hard disk!
step2 label, after we enabled the interrupts, we will set the value of
ah to "2", as that's the interrupt number for reading from the hard disk, we will set the value of
al to "1", as we are reading one sector from the disk, the value of
ch (cylinder number) will be set to zero, we will set
cl to "2" as we will read from sector
to sector 2,cl
represents the last sector we want to read, finally, we will set the value ofdh
(head number) to zero and callint 0x13
. We won't need to set the value ofdl` as it's automatically done by the BIOS. This is how that code would look like:
; [...] more code sti ; Enable interrupts mov ah, 2 ; Read sector int number mov al, 1 ; Total of sectors to read mov ch, 0 ; Cylinder number mov cl, 2 ; Sector number int 0x13
Now, we will need to create a label for our buffer, to do that, just add:
After the boot signature (dw 0xAA55).
We will also define an error message, just in case there's an error reading from the hard disk, to do so, just create a new label - now before our boot signature, we defined
buffer after it, so we are sure it won't rewrite any of our code -, for example:
err_msg: db 'Failed to load from hard disk :(', 0
Once we have both our buffer and our error message defined, we will need to set
bx to point to our buffer value, before calling
mov ah, 2 ; Read sector int number mov al, 1 ; Total of sectors to read mov ch, 0 ; Cylinder number mov cl, 2 ; Sector number mov bx, buffer int 0x13
Before we run it, let's create a new label called
err after we call the interrupt 13h, this label will basically be responsible of printing
err_msg to the screen, this label should look like this:
err: mov si, err_msg call print
Now, we need to check if there has been an error or not because it makes no sense to print our error message even when everything has gone well, right? Think... How could we do that? First, add a
jmp $ instruction before the
err label, so there's an infinite jump, and the program just does not keep executing, and add
jc err before the infinite jump, basically... If the carry flag is set, it means that there has been an error, therefore our message should be displayed:
jc err jmp $ err: mov si, err_msg call print
Now, you can make your project and run it, if there's no error message, that means that we managed to read one sector from the hard disk successfully. What's left to do now, is just to print our
buffer label, after the
jc err instruction add the following lines:
mov si, buffer call print
When you assemble your bootloader and run it again, you'd see an output like the following:
This entry's changes: herepab