An Intro to Kernel Development - Bootloader
5 mins
Let's see a how bootloaders work...
In the previous blog, we assumed that bootloader will do its job and load the kernel into memory and jump into its starting point.
Here, let's write a small bootloader and an empty kernel to understand the whole process.
after the power-on, CPU starts executing from the reset address (PUT 0x00000000 into PC register), which is mostly 0x00000000, some vendors might set this address to different address, but in general we start executing from this predefined address.
real hardware will have multi stage bootloader from this point, the initial version will load itself from reset address and jump to the next bootloader that will initize more stuff and jump into the kernel.
in our case, qemu will load our bootloader code into this reset address, so we will directly jump into our bootloader code (our code is sitting on the address 0x00000000, which will be the _start label, PC 0x00000000)
we are at 0x0, now what? what excatly do we need to do here?
- we can initilize the env? because we might need to push and pop values into stack during the bootloader execution
- we can enable interrupts? handle IRQs, use the arch specific instruction, so interrupts dont crash the CPU.
- we can load the kernel into memory? because in the next step we need to jump into kernel
Setting up env, we can set the Stackpointer to some safe memory location, zero out .bss location, so Global variables are not set to garbage values.
Setup Interrupts, Use the arch specific instruction to enable/disable interrupts
msr daifset, #0xf // set D, A, I, F = mask everything
Load Kernel, we can load the kernel image into the predefined address, in qemu its usually at 0x40080000, and update the PC to the address to start the kernel code.
this will be the minimum code for bootloader/kernel, we can add things on top of this as required.
start.s
- setup stack pointer
- setup interrupts
- load the kernel into memory and jump
kernel.s
- write to UART (this is memory mapped to address), KERNEL OK message
start.s
.section .text.boot, "ax"
.globl _start
.equ KERNEL_LOAD_ADDR, 0x40080000
.extern _binary_kernel_bin_start
.extern _binary_kernel_bin_end
.extern __bss_start
.extern __bss_end
.extern _stack_top
_start:
/* Enable Interrupts */
msr daifset, #0xf
/* Set Stack Pointer */
ldr x0, =_stack_top
mov sp, x0
/* Zero bss */
ldr x1, =__bss_start
ldr x2, =__bss_end
sub x2, x2, x1
cbz x2, 2f
1:
str xzr, [x1], #8
subs x2, x2, #8
b.gt 1b
2:
/* Copy Kernel and Jump */
ldr x3, =_binary_kernel_bin_start
ldr x4, =_binary_kernel_bin_end
sub x5, x4, x3
ldr x6, =KERNEL_LOAD_ADDR
cbz x5, 3f
mem_copy:
ldr x7, [x3], #8
str x7, [x6], #8
subs x5, x5, #8
b.gt mem_copy
3:
mov x0, xzr
mov x1, xzr
mov x2, xzr
ldr x3, =KERNEL_LOAD_ADDR
br x3
loop:
wfe
b loop
kernel.s
.section .text,"ax"
.globl _start
.equ UART0, 0x09000000 // PL011 on QEMU virt
_start:
// x0 = UART base = 0x09000000
movz x0, #(UART0 & 0xFFFF)
movk x0, #((UART0 >> 16) & 0xFFFF), lsl #16
adr x1, msg // address of string (nearby)
1: ldrb w3, [x1], #1
cbz w3, 2f
// wait while TX FIFO full (FR bit5 == 1)
3: ldr w2, [x0, #0x18] // FR
tbz w2, #5, 4f // if bit5==0 -> ready
b 3b
4: str w3, [x0, #0x00] // DR
b 1b
2: b 2b
.section .rodata,"a"
msg: .asciz "KERNEL OK\r\n"
kernel.ld
ENTRY(_start)
SECTIONS {
. = 0x40080000;
.text : { *(.text*) }
.rodata : { *(.rodata*) }
.data : { *(.data*) }
.bss : { *(.bss*) *(COMMON) }
}
bootloader.ld
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
.text : ALIGN(0x1000) {
KEEP(*(.text.boot))
*(.text*)
}
.rodata : ALIGN(0x1000) {
*(.rodata*)
}
.data : ALIGN(0x1000) {
*(.data*)
}
.bss : ALIGN(0x1000) {
__bss_start = .;
*(.bss*)
*(COMMON)
__bss_end = .;
}
_stack_top = 0x40010000;
}
ld.lld -T kernel.ld -nostdlib -o kernel.elf kernel.o
llvm-objcopy -O binary kernel.elf kernel.bin
llvm-objcopy -I binary -O elf64-littleaarch64 -B aarch64 kernel.bin kernel_bin.o
clang -target aarch64-none-elf -c start.S -o start.o
ld.lld -T bootloader.ld -nostdlib -o start.elf kernel_bin.o start.o
llvm-objcopy -O binary start.elf bootloader.bin
qemu-system-aarch64 -machine virt,virtualization=off,secure=off -cpu cortex-a53 -accel hvf -nographic -serial mon:stdio -bios bootloader.bin
this will be the flow of a working bootloader that setups env, loads the kernel and jumps into the kernel