Operating System Development: First and second stage bootloaders

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 […]

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.

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 the series on writing your own toy operating system.

How it all works

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:

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

mFileFile

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

Series index

Save

Save

Save

Trackbacks

  1. Writing your own toy operating system: Jumping to protected mode | Independent Software

Comments

5 5 Responses to “Writing your own toy operating system: first and second-stage bootloaders together”
  1. Alberto says:

    I have an error when I try to excute the makefile.
    The error is: i386:x86-64 the input file architecture “kernel.o” is not compatible with the outpt i386.
    What I should do?
    I wish you will answer me.
    Best regards, Alberto

  2. BillKoul says:

    I’ve succesfully run it until it goes to void start_kernel() but after that no matter what loop i try to write it keeps rebooting, is that an error?

    • alex says:

      Are you stepping through the code with bochs? Verify that the code looks like you think it does. If you’re not in i386 mode, your kernel code may simply reboot. The CPU may fault and cause a reboot as well. At this level, the only way to get things right is to step through the code from beginning to end and see what it does, also looking closely at bochs console output. Since you’ve gotten this far, your BIOS sector reading code is probably fine, but you may have errors in the part where you move to protected mode.

  3. Dylan Turner says:

    Can you please continue the tutorial and show how to write the kernel?
    I have the kernel set up, but it keeps rebooting after it says hello. It won’t loop in the main function, in the video_init function, or in assembly. I have tried it with my version of the bootloader and the original, and they both end up producing the same effect.
    I want to see how the kernel was made so I can fix this issue and add on to the kernel.

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">