This content originally appeared on DEV Community and was authored by Mr 3
This is the first part of my "Multi Part series of articles" about making my own custom OS
Building a custom bootloader from scratch can feel like solving a puzzle with pieces that barely fit together. The bootloader is the first step in getting your operating system up and running, and it does this by loading your kernel into memory and switching the CPU from 16-bit real mode to 32-bit protected mode. This process involves a lot of low-level work, but here’s the detailed breakdown of everything you need to know.
Table of Contents
- Introduction
- 16-bit Real Mode: Where Everything Begins
- Loading the Kernel: Dealing with Disk Sectors
- Switching to Protected Mode
- Setting Up the GDT
- Switching to Protected Mode: The Jump
- Bootloader Full Code
- Wrapping Up
Introduction
So, I was thinking while I was putting this bootloader together: why don't I just grab something pre-made and save myself a week of stress? But then again, where’s the fun in that, right? Have you ever been so deep into coding that you start wondering if your keyboard is secretly judging you? Yeah, that’s kinda how this whole experience felt. Anyway, back to the point – making a bootloader from scratch is the kind of challenge that builds character... or at least keeps you humble. Let's break it down.
Oh, by the way, what got you into OS development? Did you wake up one day and decide that writing a bootloader sounded like a good time? I mean, I get it. It's either that or trying to understand JavaScript promises.
16-bit Real Mode: Where Everything Begins
First, you start in 16-bit real mode. Yeah, it's ancient, but we gotta deal with it. The BIOS loads your bootloader in real mode, which restricts you to 1MB of memory. It’s like the brain freeze you get from chugging ice-cold water – slow, painful, and you just want to get through it so you can get to the good stuff (like protected mode).
You’ve got the usual setup with your segment registers, ds
, es
, and ss
, setting them up so you can handle memory properly. Nothing fancy here, but you have to do it.
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
That’s the stack setup, sitting at 0x7C00
, which is where BIOS plops the bootloader when it loads it into memory. From there, the magic starts happening.
Loading the Kernel: Dealing with Disk Sectors
This is where you’ll be glad you read up on BIOS interrupts. The main job of the bootloader is to load the kernel from disk. We do this by reading sectors using the int 0x13
interrupt.
mov ah, 0x02 ; BIOS read sectors function
mov al, 1 ; Number of sectors to read
mov ch, 0 ; Cylinder number
mov dh, 0 ; Head number
mov dl, [BOOT_DRIVE] ; Drive number (0x00 for floppy, 0x80 for hard drive)
int 0x13 ; Call BIOS to read sector
jc disk_error ; Jump if there’s a carry flag (read error)
This reads the kernel off the disk, sector by sector, and loads it into memory at the correct location. You’ve got to make sure the carry flag is checked after each read, otherwise, you might end up loading garbage into memory.
By the way, how do you feel about debugging bootloaders? I swear, it’s like searching for a needle in a haystack... in the dark... with no hands.
Switching to Protected Mode
Once the kernel is loaded, it’s time to switch from 16-bit real mode to 32-bit protected mode. That’s when the CPU really wakes up and you get access to more than just the 1MB of memory.
First, you disable interrupts using cli
, because the last thing you need during a mode switch is a stray interrupt messing up your day.
cli ; Clear interrupts
Then, you set up the Global Descriptor Table (GDT). The GDT tells the CPU how to handle memory segments, which are critical in protected mode.
Setting Up the GDT
The GDT is a fancy table that tells the CPU where each segment starts and how big it is. We define a null descriptor (which is mandatory), a code segment for instructions, and a data segment for, well, data.
gdt_start:
dq 0x0 ; Null descriptor (required)
gdt_code:
dw 0xFFFF ; Limit (low)
dw 0x0000 ; Base (low)
db 0x00 ; Base (middle)
db 10011010b ; Access byte (32-bit code segment)
db 11001111b ; Flags (4 KB granularity)
db 0x00 ; Base (high)
gdt_data:
dw 0xFFFF ; Limit (low)
dw 0x0000 ; Base (low)
db 0x00 ; Base (middle)
db 10010010b ; Access byte (data segment)
db 11001111b ; Flags (4 KB granularity)
db 0x00 ; Base (high)
gdt_end:
Then, we use the lgdt
instruction to load the GDT into the CPU.
gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start
lgdt [gdt_descriptor]
The GDT setup is pretty much a "do it right or you’re doomed" situation. If you mess this up, you’re gonna end up in a bootloop or a triple fault.
Switching to Protected Mode: The Jump
To officially switch to protected mode, you need to set the PE (Protection Enable) bit in the CR0
register.
mov eax, cr0
or eax, 0x1 ; Set the PE bit
mov cr0, eax
Now, here’s the trick: after enabling protected mode, you need to do a far jump to reload the code segment (cs
). This jump pushes the CPU into 32-bit mode for real.
jmp 08h:protected_mode_start
Once the jump happens, you’re in protected mode, and the CPU is ready to take on your kernel like it’s ready for war. At this point, BIOS interrupts stop working, so everything from here on needs to be set up by you – no more hand-holding.
Bootloader Full Code
Alright, here’s the full bootloader.asm code, stripped of all comments and explanations. It’s clean and simple for My Ctrl+C Ctrl+V people.
[org 0x7c00]
[bits 16]
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
mov [BOOT_DRIVE], dl
call load_kernel
cli
lgdt [gdt_descriptor]
mov eax, cr0
or eax, 0x1
mov cr0, eax
jmp 08h:init_pm
[bits 32]
init_pm:
mov ax, 10h
mov ds, ax
mov ss, ax
mov es, ax
mov ebp, 0x90000
mov esp, ebp
jmp KERNEL_OFFSET
gdt_start:
dq 0x0
gdt_code:
dw 0xFFFF
dw 0x0
db 0x0
db 10011010b
db 11001111b
db 0x0
gdt_data:
dw 0xFFFF
dw 0x0
db 0x0
db 10010010b
db 11001111b
db 0x0
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start
BOOT_DRIVE db 0
times 510-($-$$) db 0
dw 0xAA55
Wrapping Up
So, after all the setting up, coding, and messing around with interrupts and disk sectors, the bootloader’s job is done when it hands control to the kernel. A bootloader doesn’t do much – it just gets the system ready and moves on.
This process, while straightforward on paper, is super critical for getting your OS off the ground. Mess up the bootloader, and your system isn’t booting anything. At the end of the day, the satisfaction of seeing the kernel loaded makes it all worth it.
That said, debugging a bootloader is like trying to find that one piece of LEGO under the couch – it’s tough and you’ll step on it a few times before getting it right.
This content originally appeared on DEV Community and was authored by Mr 3
Mr 3 | Sciencx (2024-09-19T06:43:51+00:00) Making a custom Bootloader for a Custom OS.. Retrieved from https://www.scien.cx/2024/09/19/making-a-custom-bootloader-for-a-custom-os/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.