SoC
  • Home
  • Arm
  • Arm Cortex M0/M0+
  • Arm Cortex M4
  • Arm Cortex M3
  • Contact
Reading: How to Write a Bootloader for x86
SUBSCRIBE
SoCSoC
Font ResizerAa
  • Home
  • Arm
  • Arm Cortex M0/M0+
  • Arm Cortex M4
Search
  • Home
  • Arm
  • Arm Cortex M0/M0+
  • Arm Cortex M4
Have an existing account? Sign In
Follow US
  • Looking for Something?
  • Privacy Policy
  • About Us
  • Sitemap
  • Contact Us
© S-O-C.ORG, All Rights Reserved.
Arm

How to Write a Bootloader for x86

Graham Kruk
Last updated: September 8, 2023 1:18 pm
Graham Kruk 7 Min Read
Share
SHARE

Writing a bootloader for x86 systems requires an understanding of x86 assembly language, the BIOS, and operating system boot processes. The goal of the bootloader is to load and transfer control to the operating system kernel. This guide will walk through the key steps and considerations when developing a simple x86 bootloader.

Contents
Overview of the x86 Boot ProcessMBR AnatomyWriting the MBR Assembly CodeAdditional ConsiderationsTesting the BootloaderConclusion

Overview of the x86 Boot Process

When an x86 system powers on, the processor starts executing instructions from the BIOS boot ROM. The BIOS performs a power-on self test (POST) to initialize hardware and ensure everything is working correctly. Once complete, the BIOS scans specified boot devices for a bootloader and executes the first sector (512 bytes) of the bootloader code.

This bootloader code is commonly referred to as the Master Boot Record (MBR). The MBR is responsible for loading the operating system bootloader which then loads the kernel. For this guide, we will focus on developing a simple MBR bootloader.

MBR Anatomy

The legacy MBR format contains executable boot code in the first 446 bytes. The next 64 bytes contain the partition table with 4 16-byte entries describing partitions on the boot disk. The last 2 bytes contain the magic number 0xAA55 indicating it is a bootable disk.

When loaded at linear address 0x7C00 in memory, the BIOS will execute the bootloader code. The bootloader must determine the active partition to load the OS bootloader from, load it into memory, and transfer execution to continue the boot process.

Writing the MBR Assembly Code

The MBR bootloader can be written in x86 assembly language using an assembler like NASM. Here are some key steps when developing the assembly code:

  1. Initialize registers and the stack pointer
  2. Enable protected mode to access over 1MB of memory
  3. Determine the boot disk and active partition from the partition table
  4. Read sectors from partition into memory using BIOS interrupts
  5. Transfer execution to loaded bootloader

First, the code must set up the segment registers and stack pointer. The CS register should point to the bootloader code segment. The stack can be set up at the end of the bootloader memory space: mov ax, 07C0h ; Set up 4Kb code segment mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov sp, 07C00h ; Set up stack pointer

Next, the bootloader enters protected mode to access over 1MB of memory. This requires loading the GDTR register and setting the PE bit in CR0: cli ; Disable interrupts lgdt [gdt_descriptor] ; Load GDT descriptor mov eax, cr0 or eax, 1 ; Set PE bit mov cr0, eax jmp CODE_SEG:pipeline ; Far jump to load CS

The bootloader must now parse the partition table to identify the active partition to read the OS bootloader from. The starting sector and size of each partition is loaded from the partition table into memory.

With the starting sector and size known, the bootloader can read the contents of the active partition into memory using BIOS interrupt 0x13. This interrupt allows accessing disk sectors similar to standard read/write operations. mov bx, 0x1000 ; Load partition sectors to 0x1000 mov dh, 0x10 ; Read 16 sectors call disk_load

Finally, the bootloader transfers execution to the loaded OS bootloader with a far jump: jmp 0x1000:0x0000 ; Transfer execution

Once complete, the OS bootloader takes over and continues the boot process. This covers the basic flow and steps when developing a simple MBR bootloader.

Additional Considerations

Here are some other factors to keep in mind when writing an x86 bootloader:

  • Handle BIOS and hardware initialization if needed
  • Support booting from disks larger than 2TB with GPT partitioning
  • Implement a backup bootloader in case of failure
  • Load kernel and modules into memory for boot
  • Pass boot parameters to the operating system

BIOS services and interrupts should not be relied on past the bootloader stage. The kernel takes over hardware initialization once booted.

For large >2TB disks, MBR partitioning is insufficient and GPT is used instead. GPT changes the partition table structure and boot process.

Having a fallback bootloader is useful in case the primary bootloader is corrupted or fails to load the OS for any reason.

The bootloader may need to handle loading the kernel image and any required modules into memory before jumping to the kernel entry point.

Boot parameters allow customizing kernel options during boot. The bootloader can pass arguments like the root disk, video mode, etc. to the kernel.

Testing the Bootloader

Thoroughly testing a bootloader is critical before deployment. Some tips for testing include:

  • Use an emulator like Bochs or QEMU to test without requiring actual hardware
  • Test on multiple generations of processors if possible
  • Verify proper operation with different BIOS versions
  • Test corrupt or invalid partition tables and input
  • Introduce faults or errors and ensure robust failure handling
  • Triple check for any issues that could result in boot loops

Emulators allow conveniently iterating and debugging bootloader code before even needing real hardware. Testing across different processors and BIOSes helps catch any subtle compatibility issues.

Fault injection and error testing is key to ensure the bootloader fails gracefully. Boot loops from any simple bug can be extremely disruptive in production.

Conclusion

Developing a bootloader requires careful assembly programming and robust testing. This guide covered the essential steps for an MBR bootloader – initializing the processor and hardware, parsing the partition table, reading the OS bootloader from disk, and transferring control.

With an understanding of the boot process and goals of a bootloader, you can continue to build more advanced bootloaders. Features like graphical menus, failover redundancy, and boot optimization can be added on top of the core loader functionality.

Newsletter Form (#3)

More ARM insights right in your inbox

 


Share This Article
Facebook Twitter Email Copy Link Print
Previous Article Cortex-M33 Bootloader
Next Article Registering and Configuring the ARM MSP in Depth
Leave a comment Leave a comment

Leave a Reply Cancel reply

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

2k Followers Like
3k Followers Follow
10.1k Followers Pin
- Sponsored-
Ad image

You Might Also Like

How to use Hi(r8-r12) register in Cortex-m0?

The Hi registers r8 to r12 in Cortex-m0 provide an…

6 Min Read

Debugging On-Chip Flash and RAM with Cortex-M1 and ULINK2

Debugging on-chip flash and RAM can be challenging for developers…

7 Min Read

Soft Float vs Hardware Floating Point Tradeoffs on Microcontrollers

When designing a microcontroller system that requires floating point math,…

14 Min Read

How much memory does the Cortex-M0 have?

The Cortex-M0 is an ARM processor core designed for microcontroller…

8 Min Read
SoCSoC
  • Looking for Something?
  • Privacy Policy
  • About Us
  • Sitemap
  • Contact Us
Welcome Back!

Sign in to your account