Operating System Development: Using makefiles and the second-stage bootloader

In our endeavor to write a boot loader for a toy operating system, we’ve come far already. Now it’s time to look at the second-stage boot loader. While we’re at it, we’ll introduce makefiles to make smooth out our compilation cycle. Our boot loader does the following so far: Setup data segments Reset the drive […]

This article was posted by Independent Software, a website and database application development company based in Maputo, Mozambique. Our website offers regular write-ups on technical and design issues, ranging from details at code level to 3D Studio Max rendering. Read more about Independent Software's philosophy, or get in touch with Independent Software.

In our endeavor to write a boot loader for a toy operating system, we’ve come far already. Now it’s time to look at the second-stage boot loader. While we’re at it, we’ll introduce makefiles to make smooth out our compilation cycle. 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”.

This article is part of the series on toy operating system development.

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.

Makefiles

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:

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:

Note, for instance, the lines:

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.

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:

Bochs's Start menu

Bochs’s Start menu

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:

Starting the Bochs debugger

Starting the Bochs 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:

The Bochs debugger

The Bochs debugger

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.

Magic Breakpoints

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:

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.

Summary

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”.

Series index Next tutorial: Memory and how the CPU accesses it

Save

Save

Save

Did this article help you out? Please help us find more time to write useful guides & articles like this by donating a buck or two. It'll keep us coffee-fueled. Thanks!

Trackbacks

  1. Writing your own toy operating system: Memory and how the CPU accesses it | Independent Software

Comments

8 8 Responses to “Writing your own toy operating system: Using makefiles and the second-stage bootloader”
  1. Danny says:

    Thanks for these tutorials. 🙂 They are very well written and quite easy to read. Although my lack of knowledge when it comes to ASM has me looking through reference manuals at every other line of source code, hahaha!

    • alex says:

      Not to worry. Only the boot loader and the second-stage boot loader need to be written in assembly code, in order to minimize the code’s footprint. The kernel and everything else you can happily write in C or C++.

      • Danny says:

        Yes, I know. 🙂 Were you planning on doing the kernel development part any time soon? I’ve been reading many other tutorials around the web ( namely the James Molloy’s and BKERNELDEV ones ) but none have the same.. readability and simple, clear explanations of some of the important things.

        • alex says:

          Thanks, I appreciate that. I will show how to put together a 2nd-stage boot loader next. That’ll put the processor in protected mode, then load and launch the kernel. Once all that’s out of the way, we’ll set about writing a very simple kernel.

  2. dromo says:

    THANKS a lot.
    VERY well written and easy understanding tutorials !!!
    I’ve never seen a such good articles befor.
    Continue

  3. Alexander says:

    I’ve recently found your tutorial. Thanks a lot for your great work! I followed every step and it was very exciting experience. Can’t wait for other parts!

Leave a Reply

Your email address will not be published. Required fields are marked *