As a headcase, in my spare time (among other things) I’m writing an operating system kernel. There is nothing much at this moment because I’m digging into boot process of x86 system. And, to commit my knowledge so far, I’ll explain first simple but really important steps of booting trivial kernel.
The “kernel”
For illustrations I’m gonna use “Hello world” kernel that is written in NASM assembly (grab the source from github):
global start ; the entry symbol for ELF
MAGIC_NUMBER equ 0x1BADB002 ; define the magic number constant
FLAGS equ 0x0 ; multiboot flags
CHECKSUM equ -MAGIC_NUMBER ; calculate the checksum
; (magic number + checksum + flags should equal 0)
section .text: ; start of the text (code) section
align 4 ; the code must be 4 byte aligned
dd MAGIC_NUMBER ; write the magic number to the machine code,
dd FLAGS ; the flags,
dd CHECKSUM ; and the checksum
start: ; the loader label (defined as entry point in linker script)
mov ebx, 0xb8000 ; VGA area base
mov ecx, 80*25 ; console size
; Clear screen
mov edx, 0x0020; space symbol (0x20) on black background
clear_loop:
mov [ebx + ecx], edx
dec ecx
cmp ecx, -1
jnz clear_loop
; Print red 'A'
mov eax, ( 4 << 8 | 0x41) ; 'A' symbol (0x41) print in red (0x4)
mov [ebx], eax
.loop:
jmp .loop ; loop forever
This kernel works with VGA buffer - it clears the screen from the old BIOS messages and print capital ‘A’ letter in red. After it, it just loop forever.
Compile it with
nasm -f elf32 kernel.S -o kernel.o
nasm
generates an object file, which is NOT suitable for executing because its addresses need to be relocated from base address 0x0
, combined with other section, resolve external symbols and so on. This is a job of the linker program.
When compiling the program for userspace application gcc
will invoke linker for you with default linker script. But for kernel space code you must provide your own link script that will tell where to put various sections of the code. Our kernel code has only .text
section, no stack or heap, and multiboot header is hardcoded into .text
section. So link script is pretty simple:
ENTRY(start) /* the name of the entry label */
SECTIONS {
. = 0x00100000; /* the code should be loaded at 1 MB */
.text ALIGN (0x1000) : /* align at 4 KB */
{
*(.text) /* all text sections from all files */
}
}
I’ve already touched linking part in Restricting program memory article.
Basically, we’re saying “Start our code at 1MiB and put section .text
in the beginning with 4K alignment. The entry point is start
”.
Link it like this:
ld -melf_i386 -T link.ld kernel.o -o kernel
And run kernel directly with QEMU:
$ qemu-system-i386 -kernel kernel
You’ve got it:
The multiboot part
When the computer is being powered up it starts executing code according to its “reset vector”. For modern x86 processors it is 0xFFFFFFF0
. At this address, motherboard sets jump instruction to the BIOS code. CPU is in “Real mode” (16 bit addressing with segmentation (up to 1MiB), no protection, no paging).
BIOS does all the usual work like scan for devices and initializes it and finds a bootable device. After bootable device found it passes control to bootloader on this device.
Bootloader loads itself from disk (in case of multi-stage) finds the kernel and load it into memory. In the dark old days, every OS had its own format and rules, so there was a variety of incompatible bootloaders. But now there is a Multiboot specification that gives your kernel some guarantees and amenities in exchange to comply the specification and provide Multiboot header.
Dependence on Multiboot specification is a big deal because it helps make the life MUCH easier and this is how:
- Multiboot-compliant bootloader sets the system to well-defined state, most notably:
- Transfer CPU to protected mode to allow you access all the memory
- Enable A20 line - an old quirk to access additional segment in real mode
- Global descriptor table and Interrupt descriptor table are undefined, so OS must setup its own
- Multiboot-compliant OS kernels:
- Can (and should) be in ELF format
- Must set only 12 bytes to correctly boot
In general, booting multiboot compliant kernel is simple, especially if it’s in ELF format:
- Multiboot bootloader search first 8K bytes of kernel image for Multiboot header (find it by magic
0x1BADB002
) - If the image is in ELF format it loads section according to the section table
- If the image is not in ELF format it loads the kernel to address either supplied in the address field or in the flags field.
In our kernel’s text section we’ve done it:
MAGIC_NUMBER equ 0x1BADB002 ; define the magic number constant
FLAGS equ 0x0 ; multiboot flags
CHECKSUM equ -MAGIC_NUMBER ; calculate the checksum
; (magic number + checksum + flags should equal 0)
section .text: ; start of the text (code) section
align 4 ; the code must be 4 byte aligned
dd MAGIC_NUMBER ; write the magic number to the machine code,
dd FLAGS ; the flags,
dd CHECKSUM ; and the checksum
We didn’t specify any flags because we don’t need anything from bootloader like memory maps and stuff, and bootloader doesn’t need anything from us because we’re in ELF format. For other formats you must supply loading address in its multiboot header. Multiboot header is pretty simple:
Offset | Type | Field Name | Note
|
0 | u32 | magic | required
|
4 | u32 | flags | required
|
8 | u32 | checksum | required
|
12 | u32 | header_addr | if flags[16] is set
|
16 | u32 | load_addr | if flags[16] is set
|
20 | u32 | load_end_addr | if flags[16] is set
|
24 | u32 | bss_end_addr | if flags[16] is set
|
28 | u32 | entry_addr | if flags[16] is set
|
32 | u32 | mode_type | if flags[2] is set
|
36 | u32 | width | if flags[2] is set
|
40 | u32 | height | if flags[2] is set
|
44 | u32 | depth | if flags[2] is set
|
The booting
Now, let's boot our kernel like serious guys.
First, we create a ISO image with help of grub2-mkrescue
. Create a hierarchy like this:
isodir/
└── boot
├── grub
│ └── grub.cfg
└── kernel
Where grub.cfg is:
menuentry "kernel" {
multiboot /boot/kernel
}
And then invoke grub2-mkrescue
:
grub2-mkrescue -o hello-kernel.iso isodir
And now we can boot it in any PC compatible machine:
qemu-system-i386 -cdrom hello-kernel.iso
We’ll see grub2 menu, where we can select our “kernel” and see the red ‘A’ letter.
Isn’t it great?
Top comments (0)