MBR: Finally Done(-ish); Second-Stage Choices
There were a few things in the last post that I indicated I still wanted to get done before calling the MBR “done” and actually moving on to the VBR.
Those things were:
- Validate that the second stage bootloader has the correct signature (0xAA55) at the end, and bail if it doesn’t;
- Take into account BIOSes that don’t support reading via LBA for fixed disks and booting off of floppies by falling-back to CHS INT 13h calls;
- Reduce the bloat by streamlining boot and error messages, etc.
I finally got around to implementing these three things, so the MBR is done (at least, for now).
(Note: the repository revision that goes with this post is afc34693. I’m going to start including the corresponding revision hash in posts for posterity’s sake, and so that posts are still relevant even after the code evolves.)
Validate the Boot Signature
This one was easy: after reading (what we hope is) the volume boot record into memory (at 0000:7c00, if you remember), we just check the word at 0000:7dfe and see if it equals 0xAA55. Since I have room in the MBR code, before reading in the VBR sector I set the word at 0000:7dfe to 0x0000. The reason for this is because, even though the MBR has been relocated to 0000:0600, the “old” code is still in memory at 0000:7c00. Thus, the word at 0000:7dfe is already set to the magic value 0xAA55 because it was also present in the MBR at the same location; so if there was some sort of problem loading the VBR (a.k.a., “bug”) that wasn’t detected, the MBR will bail instead of jumping back into the MBR again, creating a loop.
LBA-to-CHS Conversion
I have no illusions that writing this bit was the hardest thing I’ll do, but for someone pretty much learning assembly while writing it, writing the LBA-sector-to Cylinder/Head/Sector conversion code was a pain. However, I can now say that, instead of just ripping off a conversion formula from a wiki somewhere, I actually understand what’s going on now. I will not get into how to convert an LBA value to its CHS equivalent here; if you really need to know how to do it, there are various places to find that information, as well as an online conversion utility you can use to check your work.
Reduce the Bloat!
Sadly, most of the code I axed–mainly consisting of some strings used for debugging and a “function” to print a value in hex–was replaced by the CHS conversion code. (I don’t need them anymore, because I have since discovered how to debug a VMware virtual machine using remote gdb…crazy!) At this point, the actual code in the MBR takes up 394 of 436 usable bytes. Still…at least I’ve got another 40 bytes to play with if I need to.
So that means that my master boot record does everything it’s supposed to do right now, and I can begin to focus on the volume boot record. Speaking of…
Choices to be Made for the Second Stage Loader
I’ve got some choices I need to make about what all this “volume boot loader” is going to do versus what the kernel (or a kernel bootstrap binary) is going to do. For instance:
- When do I jump to protected mode?
- If I need anything from the BIOS it has to be done in real mode; for instance, memory maps, video modes, etc.
- Speaking of, what do I need from the BIOS?
- The only way to load a kernel somewhere other than the first 1MB of memory (I believe) is to jump to protected- or unreal mode, load the kernel wherever you want, and then call kmain() (or whatever). However, this could also be done with a kernel bootstrap binary.
- How do I load the kernel?
- OpenBSD patches static values describing where the “/boot” file (their kernel bootstrap) resides into the VBR whenever you run installboot(8)
- Another option is to use a filesystem simple enough to enable writing code to dynamically find and load the kernel. I don’t like this.
Actually, I guess that’s about it, at least until I figure out I need to do other stuff. I kind of thought there was more to that.
Anyway, back to the code…