When a computer is switched on or reset, it runs through a series of diagnostics called POST - Power-On Self-Test. This sequence culminates in locating a bootable device, such as a Floppy disk, CD-ROM or a Hard disk in the order that the firmware is configured to.
Master Boot Record
The (legacy) BIOS checks bootable devices for a boot signature, a so called magic number. The boot signature is in a boot sector (sector number 0) and it contains the byte sequence 0x55, 0xAA at byte offsets 510 and 511 respectively. When the BIOS finds such a boot sector, it is loaded into memory at 0x0000:0x7c00 (segment 0, address 0x7c00). (However, some BIOS' load to 0x7c0:0x0000 (segment 0x07c0, offset 0), which resolves to the same physical address, but can be surprising. A good practice is to enforce CS:IP at the very start of your boot sector.)
Execution is then transferred to the freshly loaded boot record. On a floppy disk, all 512 bytes of the boot record may contain executable code. On a hard drive, the Master Boot Record (MBR) holds executable code at offset 0x0000 - 0x01bd, followed by table entries for the four primary partitions, using sixteen bytes per entry (0x01be - 0x01fd), and the two-byte signature (0x01fe - 0x01ff).
This early execution environment is highly implementation defined, meaning the implementation of your particular BIOS. Never make any assumptions on the contents of registers: They might be initialized to 0, but they might contain a spurious value just as well.
The CPU is currently in Real Mode. (Unless you are running on one of those rare BIOS' which believe they're doing you a favor by activating Protected Mode for you. Which means you not only have to write code for activating protected mode on any other hardware, but should also add a test condition if it's already activated.)
Finally, the bootloader loads the kernel into memory and passes control to it.
Now we know what we have to load, let's see how we get it loaded.
If booting from hard drive, you have only 446 bytes available for your boot record. Looking at the list of things to do before your kernel image can run, you will agree that this is not much:
- determine which partition to boot from (either by looking for the active partition, or by presenting the user with a selection of installed operating systems to chose from);
- determine where your kernel image is located on the boot partition (either by interpreting the file system, or by loading the image from a fixed position);
- load the kernel image into memory (requires basic disk I/O);
- enable protected mode;
- preparing the runtime environment for the kernel (e.g. setting up stack space);
You don't have to do things in this order, but all of this has to be done before you can call kmain().
To make things worse, GCC generates protected mode executables only, so the code for this early environment is one of the Things You Cannot Do With C.
There are several approaches to this problem:
- Geek loading: Squeeze everything from the above list into the boot record. This is next to impossible, and does not leave room for any special-case handling or useful error messages.
- One-stage loading: Write a stub program for making the switch, and link that in front of your kernel image. Boot record loads kernel image (below the 1mb memory mark, because in real mode that's the upper memory limit!), jumps into the stub, stub makes the switch to Protected Mode and runtime preparations, jumps into kernel proper.
- Two-stage loading: Write a separate stub program which is loaded below the 1mb memory mark, and does everything from the above list.
The Traditional Way
Traditionally, the MBR relocates itself to 0x0000:0x0600, determines the active partition from the partition table, loads the first sector of that partition (the "partition boot record") to 0x0000:0x7c00 (hence the previous relocation), and jumps to that address. This is called "chain loading". If you want your self-written boot record to be capable of dual-booting e.g. Windows, it should mimic this behaviour.
Easy Way Out
The most prominent one is GRUB, a two-stage bootloader that not only provides a boot menu with chainloading capability, but initializes the early environment to a well-defined state (including Protected Mode and reading various interesting information from the BIOS), can load generic executables as kernel images (instead of requiring flat binaries like most other bootloaders), supports optional kernel modules, various file systems, and if ./configure'd correctly, Diskless Booting.