In the previous post, we’ve made our first baby step on a bare metal PC. With the whole world open to us, where shall we go now? 512 bytes is really tight, real mode is really ugly, Forth is really intriguing. Should we build a bootloader, go in protected mode, hack a Forth right away?
32-bit can wait. The transition from real mode to protected mode1 is one of the more confusing aspects of the i386 architecture. I might even try to find a way to gloss over those gory details which are quite frankly boring.
Forth is the most exciting choice, but 512 bytes is too tight to get a Forth going. There is sectorforth and miniforth which manage to pull it off and boot from a single 512 byte sector, but they’re too weird of an implementation to be good learning material. We’ll need a bootloader.
A bootloader is a piece of code whose job is to read code from some kind of mass storage and get it into memory. Once that’s done, the bootloader jumps to that address in memory to yield control of the system. Some bigger operating systems even have multiple bootloading stages organized like russian dolls, but our OS is going to be small, stay small, we won’t need those fancy dolls.
“It’s not the destination, but the journey that’s important”, said no OS developer, ever. Loading is one thing, but you’ve got to know where to unload. Because we’re in 16-bit mode and because we don’t want to be playing with those fancy segment registers too much, we’re limited to the 64K address space.
That space isn’t all up for grabs. As you can see in the x86 memory map, the first 0x500 bytes of that space are taken. The rest is free all the way to 640K2, with the notable exception of your bootloader at 0x7c00.
Therefore, a good destination for your payload is address 0x500 as long as your payload isn’t bigger than 0x7700 bytes3. Forths are notoriously compact, this is more than enough for us.
One thing the bootloader can’t choose is its source media. We don’t know
beforehand whether we’re booting from a hard drive, a floppy, a USB key, etc.
Are we going to have to develop a complicated routine to detect the boot
source? Fortunately no, this information is fed to us by the BIOS through the
What we need to choose, however, are the sectors we’re about to load. We’ll generally want to put our payload in the sector directly following our boot sector. So, we read starting from the second sector. Easy right?
Well, yes and no. While we generally think of our sectors linearly4, the BIOS addresses sectors in CHS mode. However, because our payload is small and that QEMU’s boot drive has 0x3f sectors (31KB) per track, we don’t have to worry about CHS addressing for now as we will always read from cylinder 0 and head 0.
We’re about to start talking about code. Up until now, all code was inlined in the articles, but it’s becoming too voluminous for this. The code associated with this article is available in a tarball which you should use in order to follow along.
We have a source? We have a destination? Then we know what to do, let’s write
it. Oh wait, I wrote it for you already. If you run this (with
run), you’ll have a result similar to the previous “Hello World!”, but this
time, the code that’s executed comes from the second sector of the disk.
The code is separated in two parts, the
bootloader.asm part, which is the
first sector, has the 0xaa555 signature. The
payload.asm part will be in
the second sector and loaded at address 0x500.
The payload code is almost exactly the same as the previous one, so I won’t cover it.
The bootloader is, again, a single BIOS function call. This time, instead of
int13h, the family of BIOS functions related to mass storage.
If you look at INT13h documentation, you'll see that
DL is always
the “drive number” parameter. The BIOS assigns a number to each drive it
supports, this number being divided in two categories: floppies and hard
drives. IDs starting from 0x00 are floppies, from 0x80, hard drives.
Then, if you look at the bootloader, you’ll see that references to
eerily absent. That’s because before it jumps the 0x7c00, the BIOS ensures that
DL is set to the ID corresponding to the drive it booted from. Convenient
isn’t it? All we need to do is to preserve its value.
The rest of the parameters are self-explanatory and
ES is set to zero in the
same fashion and for the same reasons as last time. One oddity in the
parameters is that we specify sector 2. Wouldn’t the sector ID for the second
sector be 1? In a perfectly consistent world, yes, but it turns out that in CHS
addressing, sector numbers start at 1. Sector 0 is invalid. But that’s only for
sectors. Cylinders and heads start at 0.
Finally, we can jump to our payload and yield control
of the system to it. Why is there a
0: prefix in front of the
0x500 address? To indicate that this is a “far” jump, that is, a jump that also
CS register, another of those pesky segment registers,
this one for executable code. It’s not guaranteed to be zero at boot, so we
ensure that here. Jumping is the only way to set this register, so we can’t use
mov like with the others6.
So… that’s it? That’s a bootloader? yes! But wait just a second before you bust in the streets shouting that Grub is nothing but a fancy splash screen! That’s a fragile bootloader that’s only going to work for small payloads, on drives with a lot of sectors per track, on machines that don’t write to the BIOS parameter block (which is the case of QEMU). But for learning purposes, yes we can afford to stay simple.
Tip of the hat to you for having kept pace so far. Wag of the finger for not having exercised your new knowledge! Here are some exercises for you:
All of these will be implemented in the next article’s code, but you’ll feel much better if you’ve done them first.
By the way, if you’re having difficulties keeping up the pace, let me know by sending me an email. It's very possible that I missed important details and I'd be delighted to improve my article's grokkability.
At last, we will begin implementing a Forth! So far, we’ve been mostly hacking around x86 and PC specifications. It’s far from boring, but it’s not mind blowing. Forth is. Prepare yourself.
Next: Words in the shell
That’s what we call the mode where we’re in 32-bit. ↩
Yes, that 640K that ought to be enough for anybody, as Bill Gates famously never said. But you can quote me on that. ↩
Overwriting your bootloader’s memory while it’s running triggers fireworks. ↩
In other words, in “LBA” mode, for Linear Block Address. ↩
0xaa55? Didn’t I tell you earlier that the signature is 0x55 followed by 0xaa? Yes it is, but the i386 CPU is little endian, so if you were to write this signature in a single 16-bit register and write it at offset 510, that number would need to be 0xaa55 to have the intended effect. ↩
You think that’s complicated? Just wait until you try to get in protected mode! ↩