All Articles

Roll Your Own: Bootloader Edition

There’s something innately cool about low-level OS development. In order to learn more about this I have built an extremely simple bootloader using assembly language.

What Is a Bootloader?

When you turn on your computer, the BIOS (Basic Input Output System) runs a POST (Power On Self Test) test to ensure your machine has ample power, memory, and devices installed. If this test passes, the BIOS then loads a bootloader which is responsible for loading the kernel.

When the bootloader program is ran it is executed under 16-bit Real Mode. Real Mode means that you have unlimited direct access to all addressable memory. Also, virtual memory and memory protection do not exist at this point.

In this example we will not be loading a kernel, but instead display a simple “Hello, World!” on an x86 emulator. However, the concepts learned here are the basis for building a real bootloader.

The Nitty-Gritty

Here is the full program. I will walk you through it step-by-step below.

;-------------
; tinyboot.asm
;-------------

        bits    16                      ; Declare we are in 16 bit real mode

        org     0x7c00                  ; BIOS loads bootloader at this address

start:
        jmp         main


        
msg     db          "Hello, World!", 0


print:
        lodsb                            ; Transfers byte @ DS:SI into AL && SI++
        or          al, al
        jz          return
        mov         ah, 0x0e             ; Load "Teletype Output"
        int         0x10                 ; Print character to screen
        jmp         print
        
return:
        ret
        
main:
        cli                              ; Disable hardware interrupts
        mov         si, msg
        call        print
        hlt

times 510 - ($-$$) db 0                  ; Fill remaining bytes to 0
dw 0xAA55                                ; Boot Signature

Thankfully there’s not too much code here. Let’s break it down.

bits    16
org     0x7c00 

The first line declares we are using 16 bit mode. All x86 compatible computers boot into 16 bit mode. The second line tells the compiler to ensure all address are relative to 0x7c00. The BIOS automatically loads us into 0x7c00 so we need to account for that.

start:
        jmp         main

Assembly programs are read line-by-line. We make the first executable command to jump over the rest of our helper operations and into the main part of our program.

main:
        cli                              ; Disable hardware interrupts
        mov         si, msg
        call        print
        hlt

In main, we first disable all hardware interrupts. This is necessary for bootloaders that run on x86 machines. Next, we move the address of the first character in msg into the si register. This sets us up to be able to run our print function to print msg. After the address has been loaded into si we call the print function. Let’s see what print looks like.

print:
        lodsb                            ; Transfers byte @ DS:SI into AL && SI++
        or          al, al
        jz          return
        mov         ah, 0x0e             ; Load "Teletype Output"
        int         0x10                 ; Print character to screen
        jmp         print

lodsb loads what is pointed to by si into the al register and increments si by one. We then check if we’ve reached the end of the null terminated string with or al, al. If we have we return out of the function, if not we keep going. Now we set up to print the first character to the screen. We are going to use the 0x10 interrupt to print but first we must load 0x0e into ah. 0x0e is the number of the Teletype Output operation in the 0x10 interrupt vector. Finally, we call the interrupt and then loop to the top of print again and repeat until we’ve reached the end of the string.

main:
        cli                              ; Disable hardware interrupts
        mov         si, msg
        call        print
-->     hlt

After returning out the print operation we halt the program because we are finished.

times 510 - ($-$$) db 0
dw 0xAA55  

In order for a bootloader to be valid it needs to be exactly 512 bytes. The first line ensures that we fill whatever space we didn’t use up to 512 with 0’s. dw 0xAA55 is the Boot Signature. The BIOS looks for this value when determining for a bootable disk. This declares the disk the bootloader is stored on as bootable.

Running the Bootloader

We will be using an x86 emulator called QEMU to test our bootloader.

First, we must compile the assembly program into a binary. We will be using NASM for this.

nasm -f bin tinyboot.asm -o tinyboot.bin

Once we have the binary we will be using dd to convert the binary into a floppy image we can boot from.

dd conv=notrunc if=tinyboot.bin of=floppy.flp

Almost there! We just need to launch QEMU with our new floppy image to see the magic happen.

qemu-system-i386 -fda floppy.flp

Bootloader
Sample bootloader running on QEMU

So there you have it…our beautiful bootloader in all its glory!

Final Thoughts

This was a fun exercise but writing a custom bootloader is not very practical. If you were to develop an OS it is best to use a pre-existing bootloader like GRUB. Also, if you would like to learn more about bootloaders and OS development in general BrokenThorn and OS Dev are excellent resources.

Source code available on GitHub.

Published January 10 2017