This page describes how to build an AArch64 kernel that the Neutron bootloader can load: link address, entry point, and minimal layout. The bundled test kernel under test_kernel/ serves as a reference.
Load and Entry Address
The bootloader copies the NKRN payload to the address specified in the NKRN header field load_addr and then jumps to entry_addr (both specified in the header). In the default setup, both are 0x200000, but these are configurable per kernel. The actual load and entry addresses are discoverable at runtime via the boot_info_t structure. Your kernel must be linked so that:
- Its code and data are placed assuming the image is loaded at 0x200000 (or whatever load_addr you specify in the NKRN header).
- The first executable instruction of the binary is the kernel entry point (the instruction that will run at the address specified as
entry_addrin the NKRN header).
So the linker script must set the load address (VMA/LMA) to 0x200000 (or your chosen address), and the entry symbol must be the first executable code in the linked image.
Linker Script
Use a linker script that sets the load address and orders sections so the entry is first. Example (from test_kernel/linker/kernel.ld):
ENTRY(kernel_start)to mark the entry symbol explicitly.. = 0x200000;(or your chosen address) so all addresses are based at that location.- Code sections in order:
.textwith your entry point first (or use.text.entryto ensure it comes first). - Then
.rodata,.data,.bssas needed.
The kernel does not run from ELF; it runs from the raw binary. So the linker script defines where the code will sit in memory when the bootloader has copied the payload to 0x200000. Use objcopy -O binary kernel.elf kernel.bin to produce the payload. That binary is what gets packed with the NKRN header.
Entry Point (Assembly)
The bootloader jumps to the kernel with the following parameters:
- x0 = address of
boot_info_t(0x1000) - x1 = Device Tree Blob (DTB) address (from ARM firmware)
Your first instruction must preserve both x0 and x1 (or pass them along) and set up a stack before calling C code. The x0 parameter is required; the x1 (DTB) parameter is optional and can be ignored if your kernel does not use device trees.
Example pattern (from test_kernel/boot/kernel_start.S):
- Save x0 and x1 immediately in callee-saved registers (e.g. x19 for boot_info, x20 for DTB address) before clobbering them.
- Set up the stack in a safe region below the kernel image (e.g. 0x1F0000, giving ~1 MiB of stack space if kernel is at 0x200000). Make sure the stack region does not overlap the bootloader (0x80000) or boot_info (0x1000).
- Restore the boot parameters (x0 from x19, x1 from x20) and call the C entry point (e.g.
bl kernel_main). The C function will receive the boot_info pointer as its first argument (x0). If your kernel uses the device tree, extract the DTB address from x20 and parse it if needed. - Never return from the C entry; implement an infinite loop or system halt if the kernel is done (e.g. WFE loop).
The kernel runs at EL1 with MMU and caches off. It is responsible for initialising any hardware it needs (e.g. UART, GPIO, exception handling, page tables for MMU). The bootloader does not guarantee peripheral state after the jump; assume all peripherals are in a minimal or undefined state.
Compiler and Linker Flags
Use the same kind of flags as the Neutron build: -ffreestanding -fno-builtin -nostdlib -nostdinc, and for the linker -nostdlib -T your_kernel.ld. You can share the same cross-compiler (e.g. aarch64-none-elf-gcc, aarch64-none-elf-ld). Do not link the C library or startup files.
Build Steps
- Assemble and compile all source files (
.S,.c). - Link with your kernel linker script to produce
kernel.elf(ensure entry symbol is correct). - Extract binary using
objcopy -O binary kernel.elf kernel.binto produce the raw binary payload. - Pack the kernel using
pack_kernel.py(or equivalent) to prepend the NKRN header. Specify the load address (--load 0x200000) and entry address (--entry 0x200000) in the header. The tool produces the final packed image. - Deploy the packed image: if using the embedded kernel path, link it into the bootloader; if using the SD/FAT32 path, place it on the FAT32 root with the filename specified in
build.cfg(default:kernel.bin).
Run
pack_kernel.py --helpto see all options. The kernel version can be set via--version MAJOR.MINORto record it in the packed header.
Using a Different Load Address
If you want the kernel at a different address (e.g. 0x300000):
- Change the linker script so the image is linked for that address.
- When packing, pass
--load 0x300000and--entry 0x300000(or your entry symbol address) topack_kernel.py. - Ensure the chosen region does not overlap the bootloader (0x80000), staging (0x100000), or boot_info (0x1000). Stay within RAM (e.g. below 0x3F000000 on this platform).
The staging area at 0x100000 is overwritten when the bootloader reads the packed kernel from SD (SD path). Do not link the kernel to run from 0x100000; the payload is copied out of that region to the load address.
For the exact NKRN header format and what the bootloader checks, see NKRN Image Format. For the structure passed in x0, see Boot Info Structure.