Have you ever wondered what happens when you press the Power button on your PC? What happens right from when your motherboard gets electrical supply? How does your device become bootable?
I had. And I will share this knowledge with you alongside the code of boot loader you may run via a QEMU emulator.
Boot loader && bare-metal
Before we dive in, a few words about boot loader and bare-metal, what exactly we will implement here.
Boot loader is a program that loads an operating system (usually, although boot loader can be used for other purposes). It is loaded into operating memory from persistent memory, such as a hard drive or whatever else.
Bare-metal stands for bare-metal programming. We will not use any layers of abstraction such as GRUB loader or C language or operating system (we don’t have it at this step). We will use Assembly language (nasm compiler) and that’s it. We will interact with a system at the hardware level.
However, we apply simplifications here and implement a simple “Hello, World!” printing. This will be enough for understanding the principles.
All begins here — BIOS (Basic Input/Output System). Let me copy paste explanation from Wikipedia:
For IBM PC-compatible computers, BIOS is non-volatile firmware used to perform hardware initialization during the booting process (power-on startup), and to provide runtime services for operating systems and programs. The BIOS firmware comes pre-installed on a personal computer’s system board, and it is the first software run when powered on.
What does it mean for us as “boot loader” developers?
It means that we already have some software on our PC which runs in the first place and we need to integrate with it. So, let’s start with getting to know what happens in there by pressing the Power button (short story).
You pressed the Power button…
LED on your computer blinks…
BIOS prepares to call POST procedure…
POST stands for Power-On-Self-Test and the purposes of this procedure are simple — check if everything works correctly. I bet you all saw it at least once in your life:
The interesting part here is where this sequence leads us to. This sequence of POST procedures culminates in locating a bootable device, such as a floppy disk, cd-rom, hard disk or usb stick, whatever.
How does BIOS recognize a device as bootable?
Turns out, by magic numbers. These numbers are
0xAA, or 85 and 170 in decimal appropriately. Also, these magic numbers must be located exactly in bytes 511 and 512 in our bootable device.
You already got it; these magic numbers are just markers for BIOS that help to identify bootable devices from other devices.
When the BIOS finds such a boot sector, it loads it into memory at a specific address —
That’s the picture, that’s the deal. We know where we need to store the program so that the BIOS can load it into operating memory.
Let’s write some code!
Preparing the environment
I don’t want to burden you, but before writing some code you surely must have an environment for this. I use MacOS, so the instructions below are for MacOS.
brew install nasm qemu
That’s all what we need. For sure, let’s write some code!
Create a file
boot.asm in your testing folder, where you will play with it. The simplest boot loader with its signature, so BIOS can locate it, will look like this:
Remember the two “must have” rules for showing your device as bootable:
- Magic numbers are
- Store them in 511 and 512 bytes in our boot sector;
dw 0xAA55 writes our magic numbers and
times 510 — ($ — $$) db 0 makes sure they will be written exactly at 511 and 512 bytes. How?
dw stands for “data write” so it’s just a stupid writing of 2 bytes, more interesting is with
We know that the boot sector must be:
- 512 bytes in size;
- 511 and 512 bytes must be
Based on that, we can make a mathematical formula, calculate how many zeros we need to write after our code, so magic numbers will be at the correct place:
510 — CURRENT_ADDRESS — START_ADDRESS.
Just for an example, assume that we have 100 bytes of our code, 2 bytes of magic numbers. Based on the formula above, we need to write 410 bytes of zeros after our code, so magic numbers will be written at 511 and 512 bytes. That’s how the command
times 510 — ($ — $$) db 0 is working.
Let’s run it:
- Compile our assembly file
boot.asmvia nasm, running the command:
nasm boot.asm -f bin -o boot.bin;
- Run the compiled binary file via QEMU:
qemu-system-i386 -fda boot.bin;
Since we have only one command for now:
jmp $, we do nothing here, just an infinite loop. That’s why it stops on
Booting from Floppy... step. Let’s add some action here — let’s print the “Hello, World” message.
Since we have
boot.asm file with magic numbers, let’s change it to print “Hello, World”. I’ll prepend each command with comments, so you’ll be able to understand what exactly the command does:
Compile this code
nasm boot.asm -f bin -o boot.bin and run it
qemu-system-i386 -fda boot.bin.
As you can see, we have “Hello, World!” message in our boot sector.
In case you’re the laziest person in the world, I made a script you can use for running it on your Mac with one command:
curl https://gist.githubusercontent.com/ghaiklor/552d7f9c6c11e0c756ad305e55a0fff0/raw/cacfc2b3a84b84cc07d56e24e197ec51dc5d5133/hello-world-bootloader.sh | bash
Just copy the command above and run it in your terminal. I’m lazy, so I performed none checks, just a linear execution. So, you need to have installed
curl commands on your Mac.
Leave your thoughts in the comments would you like to read more about it, or maybe you noticed some errors I didn’t. I’ll be glad to discuss anything with you all.
In case, you are interested in sources of my simple operating system, you can find them here — github.com/ghaiklor/ghaiklor-os-gcc.
Eugene Obrezkov, Senior Software Engineer at elastic.io, Kyiv, Ukraine.