It’s finally time to put together the first and second stage bootloaders we’ve seen so far and make them work – although if you’ve been programming along with the past few installments of this series, you may already have done so. In the last few sections, we looked at the Intel 80386 processor’s protected mode and what’s required to get to it.
In this part, we’ll take a short break and put everything we’ve done so far together to see it run. Might as well have a little payoff for all the work, right?
This article is going to repeat all the code presented in the previous articles to see how it all goes together. You can read through it, or you can grab all the source code here.
This article is part of a series on toy operating system development.
Before we get to code, let’s have a summary of everything that happens during the boot process. Loading the boot sector
When it’s switched on, the computer performs a Power-On Self Test (POST). It then detects whether a bootable disk is inserted in the disk drive, and loads its boot sector (the first 512 bytes on the floppy disk). A boot sector is executable if its last two bytes are the magic value 0xaa55.
The entire boot sector is loaded at address 0000:7c00, and the CPU starts executing the very first instruction found at that address.
Only the first 3 bytes of the boot sector can be code. The next 59 are boot data bytes that describe the floppy disk. The executable code continues after that. In that code, we have to do everything necessary to load the second-stage bootloader.
First-stage bootloader
In the first-stage bootloader, we must do the following:
Setup the memory segments and stack used by the bootloader code
Reset the disk system
Display a string saying “Loading OS…”
Find the second-stage boot loader in the FAT directory
Read the second-stage boot loader image into memory at 1000:0000
Transfer control to the second-stage bootloader
We must leave the rest of the steps to get to protected mode and load the kernel to the second-stage bootloader, as there simply isn’t enough space available in the first-stage bootloader to do all this.
Second-stage bootloader
In the second-stage bootloader, we must do the following:
Copy the boot sector data bytes to a local memory area, as they will be overwritten
Find the kernel image in the FAT directory
Read the kernel image into memory at 2000:0000
Reset the disk system
Enable the A20 line
Setup the interrupt descriptor table at 0000:0000
Setup the global descriptor table at 0000:0800
Load the descriptor tables into the CPU
Switch to protected mode
Clear the prefetch queue
Setup protected mode memory segments and stack for use by the kernel code
Transfer control to the kernel code using a long jump
Boot sector structure
Both of our bootloaders will need to know the structure of the boot sector data bytes: bootsector:
Required macros
Some of the assembler code that we write for the bootloaders takes the form of functions that we will call multiple times. Other code is called only once and implemented in the form of macros, which serve merely to give structure to the code (we could not use macros, but then our code would become a very long and unreadable list of assembly instructions). For reference, I will present the macros used here.
mInitSegments
The first-stage bootloader must setup real-mode memory segments to work with, as well as a stack. The boot sector is loaded by the BIOS at 0000:7c00. We will define a stack at 0000:7c00, which will grown downward from there (as stacks always do). We must disable interrupts while we define our memory segments.
mResetDiskSystem
Our first-stage bootloader will need to read the FAT table from disk and find the second-stage bootloader image in it. To do so, it must first reset the disk system through the floppy disk controller. If this should fail, we cannot continue and must reboot.
mFindFile
This macro loads the root directory of the floppy disk’s FAT into memory at the provided segment loadsegment. It then looks for filename and finds the disk sector where this file begins as well as the number of sectors it occupies. These values are stored for later use. If the filename is not found in the FAT, then the system shows an error message and reboots.
This macro makes use of the ReadSector function which we will show below.
mReadFAT
The mReadFAT macro loads the floppy disk’s FAT into memory at a specified segment. This macro also makes use of the ReadSector function. Once the FAT is loaded, it will be used by the mReadFile macro to actually read a file.
mReadFile
This macro reads a file image into memory at the specified memory segment. It uses the FAT table in memory read earlier by the mReadFAT macro. It also makes use of the start sector and number of sectors of the file to be read, which were earlier determined by the mFindFile macro.
mStartSecondStage
This macro sets up data segments for the second stage bootloader code which was loaded at 1000:0000. It then performs a long jump to the second-stage code, thus causing the code segment to point to the code’s location.
mCopyBootSector
The second-stage bootloader requires the data in the boot sector as well. When the second stage runs, the first thing it does is copy the boot sector data to a local memory area, before it gets overwritten.
mGoProtected
This macro switches the CPU to protected mode by setting the least significant bit in the CR0 register.
mClearPrefetchQueue
This macro clears the CPU’s prefetch queue. This must be done just after switching to protected mode, since any instructions that the CPU has prefetched will have been decoded as 16 bit code. Since we are now in 32-bit mode, we must discard these decoded instructions and have the CPU fetch the instructions again. This is done by performing an absolute jump.
mSetup386Segments
Before we jump to the kernel code, we must setup the data segments and stack that the kernel will use. Since we are in protected mode at this point, we no longer specify segmented addresses but must use a selector instead. Also, we define a stack at 0x30000 (linear address) that will grow downwards.
mJumpToKernel
This final macro transfers control to the kernel code. It does this by executing a long jump, which will set the code segment cs to point to the kernel code. This has to be a 32-bit long jump, since we are not in protected mode. However, all of this code gets compiled as 16-bit code, so we need to encode the 32-bit instruction ourselves, just like a 32-bit assembler would:
Required functions
Apart from the above macros, we must write a number of functions that our bootloaders will use.
WriteString
The WriteString function writes a string to the terminal. It does this using BIOS interrupt 0x10, function 0xe. This can only be done while we’re still in real mode. After the switch to protected mode, we’ll need to write our own functions to write to the screen, since we can’t use the BIOS anymore.
This function expects a NULL-terminated string at ds:si.
Reboot
This function displays a message “Press any key…”, waits for a keypress, then reboots. The keypress is read using BIOS interrupt 0x16, function 0x0. Again, this can only be done while we’re still in real mode. After the switch to protected mode, we’ll need to write our own functions to write to the screen, since we can’t use the BIOS anymore.
ReadSector
This last function reads a sector from the floppy disk and stores it in es:bx. The sector logical address is provided in register ax. This function uses BIOS interrupt 0x13, function 0x2 to do the reading. Note that floppy disks are not very reliable, so the code attempts to read the sector data four times before giving up. If it gives up, the boot process fails and the system reboots.
Enabling the A20 line
The second-stage bootloader will need to enable the processors 21st address line (see this article for an in-depth explanation). The following functions and macros do just this.
CheckA20
This function checks whether the A20 line is currently enable. It does this by testing whether the memory “wraps” at the one megabyte mark.
mSetA20BIOS
This macro uses BIOS interrupt 0x15, function 0x2401 to attempt to enable the A20 line.
Wait_8042_command
This function has the CPU wait until the 8042 keyboard controller is ready to accept a command byte.
Wait_8042_data
This function has the CPU wait until the 8042 keyboard controller is ready to accept a data byte.
mSetA20Keyboard
This macro uses the 8042 keyboard controller chip to attempt to enable the A20 line.
mSetA20FastGate
This macro attempts to enable the A20 line by using the “Fast A20” method, which is a special port that some chipsets have.
mEnableA20
This final macro combines all previous A20 macros to enable the A20 line. If none of the methods work, the macro gives up and reboots the system.
Macros for descriptor tables
The second-stage bootloader will need to setup a global descriptor table (GDT) and an empty interrupt descriptor table (IDT). The following macros take care of that:
mSetupGDT
This macro creates a GDT with three entries:
A NULL-descriptor (required)
A code segment of 4GB, starting at 0x0
A data segment of 4GB, starting at 0x0
mSetupIDT
This macro creates an interrupt descriptor table at 0000:0000. It has 256 entries of 8 bytes each, all filled with zeroes. The presence of an IDT is required to switch to protected mode, but we don’t need to define any interrupt handlers yet.
mLoadDescriptorTables
This final macro tells the CPUs where the GDT and the IDT are:
First-stage bootloader
Now that we’ve got all the required macros and functions out of the way, cobbling together the first-stage bootloader becomes simple. We basically turn the recipe presented at the start of this article into code, calling macros as necessary.
Second-stage bootloader
Now we can finally write the complete second-stage bootloader.
Summary
In this section of the “writing your own toy operating system” series, we’ve put together all the code we have written so far. The result is a first and second-stage bootloader that work together to load a kernel image, switch to protected mode, and run the kernel.
We haven’t actually gotten around yet to writing any kernel code, but in the source code that goes with this article there actually is a small kernel program that displays “Hello world”.
In the next section, we see about writing an actual kernel! Check back soon.