How to implement your own “Hello, World!” boot loader

using Assembly language on bare-metal machine

Eugene Obrezkov
Eugene Obrezkov
5 min readOct 21, 2017

--

Have you ever wondered what happens when you press the Power button on your PC? What happens right from the moment when your motherboard starts getting electrical supply? How your device became bootable?

I had. And I’m going to share this knowledge with you alongside with the code of boot loader that you may run via QEMU emulator.

Boot loader && bare-metal

Before we dive in, a few words about boot loader and bare-metal, what exactly we are going to 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’re not going to 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 are going to interact with a system at the hardware level.

Though, we apply simplifications here and going to implement a simple “Hello, World!” printing. This will be enough for understanding the principles. Press the button!

BIOS

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 somehow integrate with it. So, let’s start with getting to know what happens in there by pressing the Power button (short story, of course).

You pressed the Power button…
LED on your computer starts blinking…
BIOS prepares to call POST procedure…

POST stands for Power-On-Self-Test and the purposes of this procedure are simple — check if everything is working correctly. I bet you all saw it at least once in your life:

Power-On-Self-Test

The really interesting part here is where this sequence leads us to. This sequence of POST procedures culminates into locating a bootable device, such as a floppy disk, cdrom, hard disk or usb stick, whatever.

Bootable device

How do BIOS recognizes a device as bootable?

Turns out, by magic numbers. And these numbers are 0x55 and 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 helps to identify bootable devices from other devices.

When the BIOS finds such a boot sector, it is loaded into memory at specific address — 0x0000:0x7C00.

That’s the picture, that’s the deal. We know where we need to store the program so it can be loaded by the BIOS 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 instructions below are for MacOS.

You need to have Assembly compiler — nasm and emulator for testing our boot loader — QEMU. We can install these via brew:

brew install nasm
brew install qemu

That’s all what we need. At this time, for sure, let’s write some code!

Boot Signature

Create a file boot.asm in your testing folder, where you’re going to play with it. The simplest boot loader with its signature, so BIOS can locate it, will look like this:

; Our code
jmp $
; Magic numbers
times 510 - ($ - $$) db 0
dw 0xAA55

Why?

Remember the two “must have” rules for indicating your device as bootable:

  • Magic numbers are 0x55 and 0xAA;
  • Store them in 511 and 512 bytes in our boot sector;

dw 0xAA55 writes our magic numbers and times 510 — ($ — $$) db 0 makes sure that 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 times command.

We know, that boot sector must be:

  • 512 bytes in size;
  • 511 and 512 bytes must be 0x55 and 0xAA;

Based on that, we can make a mathematical formula, calculate how much 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.asm via 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.

“Hello, World”

Since we have boot.asm file with magic numbers, let’s modify it to print “Hello, World”. I’ll prepend each of command with comments, so you’ll be able to understand what exactly the command does:

Printing “Hello, World!”

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 out boot sector.

Goal achieved!

Bonus

In case you’re the laziest person in the world, I made a script you can use for running it on you 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 pretty lazy as well, so I didn’t perform any checks, just a linear execution. So, you need to have installed brew and curl commands on your Mac.

Thanks

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.

Follow me on Medium, Twitter, Facebook.

--

--

Eugene Obrezkov
Eugene Obrezkov

Software Engineer · elastic.io · JavaScript · DevOps · Developer Tools · SDKs · Compilers · Operating Systems