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