DEV Community

Nebez
Nebez

Posted on

Building a Bootloader from Scratch

Table of Contents

My First Experience in the World of Low-Level Programming: Building My First Bootloader

Part 1

For a long time, one question had been stuck in my mind:

What actually happens inside a processor, and how do the code we write get executed at the lowest level?

I already knew that the code we write in high-level programming languages like Java, Python, Kotlin, and others eventually needs to be translated into a language that the processor can understand.

For example, in Java, the code we write is first compiled into Bytecode by the Compiler. Then, it is executed by the Java Virtual Machine (JVM) and eventually translated into instructions that the processor can execute.

This means that behind even the simplest programs we run, the processor is performing billions of operations on zeros and ones in a fraction of a second.

That is truly amazing!

This curiosity pushed me to challenge myself and enter a completely new world, a world much closer to the hardware than languages like Java and Python.


Starting My Learning Journey

The first thing I did was search through different resources. Like always, my first destination was YouTube.

I found a video titled:

How to build an Operating System from scratch

and opened it with a lot of excitement.

A few minutes later, I realized that I barely understood anything, so I respectfully closed the video with a little bit of fear and confusion πŸ˜„

I understood that building an operating system from scratch was still far beyond my current level, and I needed to start with more fundamental concepts.

I started reading articles, watching educational videos, and exploring different resources until I realized that many computer architecture and operating system tutorials start with simpler and older architectures, such as x86 8086.


My First Introduction to Intel 8086

The good news was that I did not need to manually write millions of zeros and ones inside a text file just to create a simple program.

The not-so-good news was that I had to learn Assembly πŸ˜„

I started by collecting information about the Intel 8086 processor.

The Intel 8086 was a 16-bit processor introduced by Intel in 1978 and became one of the most important processors in the x86 family.

To learn this architecture, educational tools like emu8086 allow us to write Assembly code, execute it, and observe the processor's Registers and Flags step by step.

This was truly fascinating for me.

Imagine being able to see exactly what instructions the processor executes, how Register values change, and what the CPU state looks like at every moment!

But there was one problem...

I had no idea what a Register was, what a Flag was, what Memory Addressing meant, or what the CPU was actually doing.

So I spent several days trying to understand the basics of x86 architecture first.

Of course, I have to admit that for someone who has just moved from:

print("hello world")
Enter fullscreen mode Exit fullscreen mode

into the world of processors, looking at the internal structure of a CPU can be a little scary πŸ˜„

A Simple Look at the Structure of 8086


Image credit: ElectronicsMind - Intel 8086 Microprocessor Architecture

The 8086 processor consists of different parts, but for a beginner, we can focus on the most important ones.

Execution Unit (EU)

The Execution Unit is responsible for interpreting and executing instructions.

This unit includes components such as:

  • ALU (Arithmetic Logic Unit) for performing arithmetic and logical operations
  • General-purpose Registers
  • Flag Registers

Bus Interface Unit (BIU)

The Bus Interface Unit is responsible for communication between the CPU and memory, as well as fetching instructions from RAM.

This unit includes:

  • Segment Registers
  • Instruction Pointer (IP)
  • Memory address generation logic

Registers

Registers are extremely small and extremely fast storage locations inside the CPU.

The processor cannot always work directly with RAM for operations such as calculations, data transfers, and instruction execution because RAM is slower compared to the CPU.

Because of this, the CPU uses Registers as a very fast workspace to keep the data it needs closer to the processing unit.

In Low-Level Programming, Registers are one of the most important tools for directly interacting with the CPU.

Some Registers are used for storing data, some for memory addressing, some for managing the Stack, and some for controlling the execution flow of a program.

In fact, we are going to use these tiny storage locations inside the CPU to control program execution step by step.


My First Assembly Code Execution

After getting a basic understanding of the processor architecture, I told myself:

"I will not truly understand these concepts until I start writing code."

So I decided to write my first Assembly program.

At first, I tried emu8086, but I could not really connect with its environment.

Instead, I decided to use my favorite editor, Vim.

To prepare my development environment, I installed the required tools:

Ubuntu / Debian

sudo apt update

sudo apt install nasm qemu-system-x86 gdb binutils
Enter fullscreen mode Exit fullscreen mode

Fedora

sudo dnf install nasm qemu-system-x86 gdb binutils
Enter fullscreen mode Exit fullscreen mode

To verify the installation:

nasm -version

qemu-system-i386 --version

gdb --version
Enter fullscreen mode Exit fullscreen mode

The main tools I used were:

  • NASM for converting Assembly code into Binary files
  • QEMU for running the Bootloader in an emulated environment
  • GDB for debugging the CPU and inspecting Registers
  • Binutils for analyzing Binary files

From Turning On the Computer to Executing My First Bootloader

After getting familiar with the CPU and the 8086 architecture, it was time to understand what exactly happens when a computer starts.

How can a computer that has not executed any program yet, run its first piece of code?

This is where I learned about the concept of the Boot Process.


What is a Bootloader?

Simply put, a Bootloader is a small program that is executed during the early stages of system startup and prepares the system for the next steps.

In real operating systems, the Bootloader is usually responsible for finding the Kernel from storage and loading it into RAM.

However, to start learning, I decided to write a very simple Bootloader that does only one thing:

Display a character on the screen.

It may sound simple, but this small action means that I am executing my first piece of code that runs directly on the CPU before any operating system exists.


What Happens When a Computer Starts?

The boot process of a BIOS-based system can be summarized like this:

Power On
   |
   ↓
CPU Reset
   |
   ↓
BIOS starts executing
   |
   ↓
POST (Power-On Self Test)
   |
   ↓
Find Boot Device
   |
   ↓
Load Bootloader into RAM
   |
   ↓
Jump to Bootloader Code
   |
   ↓
Bootloader executes
Enter fullscreen mode Exit fullscreen mode

Now let's look at these steps in more detail.


1. Turning on the system

When we press the Power button, the CPU exits the Reset state and starts executing its first instructions.


2. Executing BIOS

In older systems based on BIOS, the initial control is transferred to the system Firmware.

BIOS stands for:

Basic Input/Output System

and its job is to check and initialize the main hardware components of the system.

During this stage, a test called:

POST (Power-On Self Test)

is performed to check components such as:

  • RAM
  • Keyboard
  • Display
  • Storage Devices

3. Finding the Boot Device

After POST is completed successfully, BIOS searches for a bootable device according to the configured Boot Order.

For example:

  • Hard Drive
  • USB
  • CD/DVD

4. Finding the Bootloader

In traditional BIOS-based systems, BIOS checks the first sector of the bootable device.

This sector must contain a special signature in its last two bytes:

0x55AA
Enter fullscreen mode Exit fullscreen mode

This is called:

Boot Signature

If this signature exists, BIOS recognizes this sector as a valid boot sector.

Then, it loads these 512 bytes into a specific memory address:

0x7C00
Enter fullscreen mode Exit fullscreen mode

and transfers control of execution to the CPU.

From this moment, the code we wrote starts running.

My First Bootloader

After understanding the concept of the Boot Process, it was time to write my first piece of code.

The goal was very simple:

Just display the character:

N
Enter fullscreen mode Exit fullscreen mode

on the screen.

The code:

[bits 16]

org 0x7c00

mov ah, 0x0e
mov al, 'N'

int 0x10

times 510 - ($ - $$) db 0

dw 0xAA55
Enter fullscreen mode Exit fullscreen mode

Understanding the Code

This simple Bootloader runs in:

16-bit Real Mode


Defining the CPU Mode

[bits 16]
Enter fullscreen mode Exit fullscreen mode

This tells the Assembler that our code is written for a 16-bit processor mode.


Bootloader Location

org 0x7c00
Enter fullscreen mode Exit fullscreen mode

This tells the Assembler that this code will be loaded and executed at memory address 0x7C00.

This is the address where BIOS usually places the Boot Sector.


Loading Values into Registers

mov ah, 0x0e
mov al, 'N'
Enter fullscreen mode Exit fullscreen mode

In this section, we place the required values into CPU Registers.

For a simpler understanding, we can compare this idea to assigning values to variables in high-level programming languages:

int value = 0x0e;
Enter fullscreen mode Exit fullscreen mode

However, the important difference is that in Assembly, we do not work with variables created by a Compiler.

Instead, we directly control real Registers inside the CPU.

Here:

  • The value 0x0E is placed inside the AH Register.
  • The character N is placed inside the AL Register.

The value 0x0E is a Service Number for BIOS that tells it we want to display a character using Teletype Mode.


Calling BIOS Interrupt

int 0x10
Enter fullscreen mode Exit fullscreen mode

This instruction calls a:

BIOS Video Interrupt

The control is transferred to BIOS video services, which perform the character display operation.


Completing the Boot Sector

A Boot Sector must be exactly 512 bytes in size.

For this reason:

times 510 - ($ - $$) db 0
Enter fullscreen mode Exit fullscreen mode

fills the remaining space of the file with zeros.

At the end:

dw 0xAA55
Enter fullscreen mode Exit fullscreen mode

adds the Boot Signature so that BIOS can recognize this sector as a valid Bootloader.


Building and Running the Bootloader

After writing the code, I saved the file as:

boot.asm
Enter fullscreen mode Exit fullscreen mode

Then, using NASM, I converted it into a Binary file:

nasm -f bin boot.asm -o boot.bin
Enter fullscreen mode Exit fullscreen mode

This command creates a raw Binary file that can be executed as a Boot Sector.

To run it, I used QEMU:

qemu-system-i386 -fda boot.bin
or
qemu-system-x86_64 -fda boot.bin
Enter fullscreen mode Exit fullscreen mode

And the result?

A simple character appeared on the screen:


N
Enter fullscreen mode Exit fullscreen mode

But for me, it was much more than just a character.

This was the first time I had written code that ran directly on the CPU without an operating system, without any Runtime, and without any Framework.

At that moment, I finally understood why the world of Low-Level Programming is so fascinating.

At the lowest level, there are no ready-made layers hiding the details.

There is only:

You, the CPU, memory, and a few Assembly instructions.


Continuing the Journey

This experience was only a small beginning.

By writing this simple Bootloader, my interest in Low-Level Programming became much stronger, and I decided to continue this learning journey.

My next goals are:

  • Learning Assembly more deeply
  • Building more advanced Bootloaders
  • Eventually writing a simple Kernel

This was only the first step...


My First Bootloader on GitHub

You can find the source code of my first Bootloader here:

https://github.com/Nebez-Berzency/simple-bootloader

Top comments (0)