In our endeavor to write a boot loader for a toy operating system, we’ve come far already. Our boot loader does the following so far:
- Setup data segments
- Reset the drive system
- Write a “loading” message
- Find the kernel file on disk, using parameters found in the boot sector
- Read the FAT table into memory
- Read the kernel file into memory, using the FAT table
- Reboot gracefully if the file could not be found or if reading fails
We are now in a position to put all the code fragments together and compile our boot loader. We’ll also add some initial code to our kernel, so that it can say “Hello”.
Putting the code together
The bits of code we’ve written so far actually come in two flavors: code that’s used only once and code that’s called various times. The code that’s called in multiple places should be implemented as functions; the rest will be macros. We use macros (a feature of the GNU assembler) merely to be able to give a name to a body of code so our source does not become unreadable.
The functions and macros will reside in their own files. This is because even with what we’ve done so far, the boot process isn’t quite complete yet. We’ll still have to write code that puts the processor in protected mode, sets up protected mode descriptor tables, and starts the kernel (we’ll get to all that later). Since we only have a few bytes left in our boot sector (as you’ll see after compiling), we’ll have to put this code somewhere else: in a “second stage boot loader”. The upshot of this is that while our boot sector code is limited to only one segment of 512 bytes, our second stage boot loader can occupy many sectors.
The second stage boot loader will again need to read the root directory and the FAT table in order to be able to find and read a file. With our functions and macros placed in separate files, we’ll be able to use all that code again easily.
Therefore, the code will be divided into files like this:
- boot.s – Primary boot loader
- 2ndstage.s – Second stage boot loader
- bootsector.s – Macro for the actual boot sector data
- macros.s – Reusable macros
- functions.s – Reusable functions
You can download the source code so far here.
Since we now have to compile each file separately, we’ll tie the build process together with a makefile.
In order to compile our five files, and turn them into a disk image we can test with Bochs, we need quite a few instructions:
as -o boot.o boot.s ld -o boot.out boot.o -Ttext 0x7c00 objcopy -O binary -j .text boot.out boot.bin as -o 2ndstage.o 2ndstage.s ld -o 2ndstage.out 2ndstage.o -Ttext 0x0 objcopy -O binary -j .text 2ndstage.out 2ndstage.bin imagefs c test.img 720 imagefs b test.img boot.bin imagefs a test.img 2ndstage.bin
Instead of typing all this at the command prompt each time you test, you’d be better off placing the instructions in a batch file. Better yet, the GNU toolchain comes with a solution: makefiles. Imagine your code growing, so that you have many subdirectories with many files that need to be compiled. You’ll write many batchfiles with similar instructions. Also, you have no way of cleaning up intermediary files. And worst of all, you’ll always compile everything, including files that haven’t changed, which might get slow if you have a lot of code.
With makefiles, its gets easier. GNU make determines automatically which files have changed, and compiles only those files.
Here is a makefile for our boot loader code:
AS := as LD := ld all: boot.bin 2ndstage.bin boot.bin: boot.out objcopy -O binary -j .text boot.out boot.bin boot.out: boot.o $(LD) -o boot.out boot.o -Ttext 0x7c00 boot.o: boot.s bootsector.s functions.s macros.s $(AS) -o boot.o boot.s 2ndstage.bin: 2ndstage.out objcopy -O binary -j .text 2ndstage.out 2ndstage.bin 2ndstage.out: 2ndstage.o $(LD) -o 2ndstage.out 2ndstage.o -Ttext 0x0 2ndstage.o: 2ndstage.s bootsector.s functions.s macros.s $(AS) -o 2ndstage.o 2ndstage.s clean: del *.o del *.out del *.bin
Note, for instance, the lines:
boot.o: boot.s bootsector.s functions.s macros.s $(AS) -o boot.o boot.s
This instructions tells make that the boot.o object file depends on the files boot.s, bootsector.s, functions.s and macros.s. If any of these files change, then boot.o must be recompiled. In short, makefiles allow you to describe dependencies.
In order to build your code, you can now do:
And to clean everything up except the source code, do:
The second-stage boot loader
We can now write a first version of our second-stage boot loader, just to prove that our concept works. We’ll write one that simply writes “Hello” to the screen, then hangs.
.code16 .intel_syntax noprefix .text .org 0x0 .include "macros.s" .global main main: mWriteString msg hang: jmp hang # Booting has failed because of a disk error. # Inform the user and reboot. bootFailure: mWriteString diskerror mReboot .include "functions.s" msg: .asciz "Hello!\r\n" rebootmsg: .asciz "Press any key to reboot.\r\n" diskerror: .asciz "Disk error. " .include "bootsector.s" .fill 1024, 1, 1 # Pad 1K with 1-bytes to test file larger than 1 sector
You’ll note several things here. We’ve included the files functions.s and macros.s which were originally written for the primary boot loader. We’ll need them later when we load our kernel file. Also, we need the mWriteString macro in order to write text to the screen. The bootFailure section is also present, along with the messages it prints. We’ll also need this later, but right now we must include it since the code in functions.s and macros.s requires it. Since we’re not actually calling much of the code in macros.s, the resulting binary file for the second stage boot loader will be quite small.
Also, I’ve added 1024 bytes with a value of 1 to the end of the file, just to give it a size of over two sectors. This will allow us to see that our primary boot loader does in fact use the FAT table correctly to load a 3-sector file into memory.
Debugging the code
All right, now that we’ve put it all together (don’t forget to download the final code here) we can compile and run it. If you like, you can use the makefile above to do this, or you can do it manually. Next, create a disk image from it. (You could add the required instructions to the makefile as an exercise.)
The resulting disk image can now be run in Bochs as we’ve seen before. However, can and will go wrong and this is as good a time as any to get acquainted with Bochs’s built-in debugger, which will become your friend soon. Bochs actually comes with two main executables: bochs.exe and bochsdbg.exe, the latter being the debugger. You can start it directly, or you can use configuration files. The latter will be easier for reuse of your configuration.
In order to create a configuration file, load up Bochs (doesn’t matter which executable you start) and use the configuration window to setup your test system (also see part 3 of this guide for a refresher). When you’re all done, do not start the emulator, but save your configuration to file:
This will create a file called bochsrc.bxrc on your disk which contains your configuration settings. Close the configuration window now with the Quit button. In order to start the emulator, you can now right-click on the bochsrc.bxrc file and select Debugger:
Of course, you can also select Run to run in normal, non-debug mode. When debugging, Bochs stops execution of the emulator at the very beginning: inside the POST (Power-On Self Test) code. You can use the debugger to step through this code, if you like, and follow exactly what is does. Eventually your boot sector will be loaded and you can step through that code, as well:
With every step, you can study the value of the processor’s registers and the contents of the physical memory, which will be solid gold when you run into trouble. Of course, it’s pain in the neck so have to step through all the BIOS code every time you run the emulator. What we really need are breakpoints. You can either use the debugger itself to place breakpoints by double-clicking an assembly instruction, but that would require scrolling through lots of code, and some of your code hasn’t even been loaded yet! It’s easier to actually add a breakpoint instruction to your source code.
Bochs supports a concept called magic breakpoints. The Intel IA-32 processor has an instruction that no-one uses since it does nothing (no, it’s not NOP). The instruction is:
xchg bx, bx
This simply exchanges with contents of the bx register with itself. Bochs assumes you’ll never use this instruction in your code, so when the debugger encounters it, it stops execution: a breakpoint. Sprinkle some of these instructions through your code and you can just use the “Continue” option in the Bochs debugger to move from one breakpoint to the next. Finally, you can debug properly!
Note that you may need to enable magic breakpoints in the Bochs configuration.
In this section, we’ve put all our code together and compiled it (here is the source for download). We’ve seen how to use makefiles to make the compilation process easy and repeatable. We’ve also seen how we can use Bochs’s internal debugger to step through our code as it executes, and how to set magic breakpoints. Finally, we’ve added a tentative second-stage boot loader to our toy OS that, for the time being, says “Hello”.
In the next part, we’ll talk about fleshing out our second-stage boot loader with switching the processor to protected mode, and launching our kernel. Stay tuned!