This is a shell for a custom bare metal operating system on the Raspberry Pi. It is intended to be run inside qemu
on the Raspberry Pi 3 target (when this doc was written, there was no Pi 4 target).
You first need to upgrade all your system's software:
pi@raspberrypi:~ $ sudo apt update && sudo apt upgrade
pi@raspberrypi:~ $ sudo apt install qemu-system-arm vim screen gcc-aarch64-linux-gnu gdb-multiarch make git
On most ARM systems, we use u-boot
to load the OS. The Pi does not do that---it has a weird boot process.
- On power up, the ARM CPU is held in reset while the GPU loads and begins executing a hard-coded bootloader.
- The GPU bootloader loads some firmware files (bootcode.bin) out of the first partition of the SD card.
bootcode.bin
runs on the GPU and its job is to load the Linux kernel, which is also located on the first partition of the SD card. The kernel image must be calledkernel8.img
so the GPU bootloader can identify it. The Linux kernel image has a special Raspberry Pi-specific header (see table below) that tells the GPU bootloader where it should be placed in memory. - After the GPU bootloader is done loading the kernel into main memory, it brings the ARM CPU out of reset, and the ARM begins running the kernel.
Field Contents | Field Length | Notes |
---|---|---|
ASM Instruction | 32 bits | The first two fields in the header give us |
------------------- | -------------- | space to write two assembly instructions. These |
ASM Instruction | 32 bits | are used to jump to the kernel's entry point. |
------------------- | -------------- | ------------------------------------------------- |
Kernel Image Size | 64 bits | Tells the bootloader size of the kernel image. |
------------------- | -------------- | ------------------------------------------------- |
Flags | 64 bits | Tells endianness, page size, and physical |
placement of the kernel. PiOS uses flags = | ||
0x0000000000000002 which gives us little endian | ||
kernel with 4kb page size aligned as close as | ||
possible to the base of DRAM. | ||
------------------- | -------------- | ------------------------------------------------- |
Reserved | 64 bits | Unused |
------------------- | -------------- | ------------------------------------------------- |
Reserved | 64 bits | Unused |
------------------- | -------------- | ------------------------------------------------- |
Reserved | 64 bits | Unused |
------------------- | -------------- | ------------------------------------------------- |
Magic Number | 64 bits | Set to 0x00000000644D5241 to indicate valid |
kernel image. | ||
------------------- | -------------- | ------------------------------------------------- |
Reserved | 64 bits | Unused |
------------------- | -------------- | ------------------------------------------------- |
This shell code in this repository creates kernel8.img
that consists of a Linux kernel header at the beginning followed by our code. Even though we are not writing the Linux kernel, we still use the same header so our OS can be loaded by the Pi's bootloader. The Linux kernel header is a data structure that tells the Raspberry Pi bootloader where to load our OS image in memory and where to start executing code inside the OS image. The kernel header is located at the top of boot.s
, and you can find a document that explains the header format here. We use a special linker script (kernel.ld
) to force the kernel header to be located at the beginning of our binary file.
The Makefile in this repo has a bunch of useful recipes that you can use:
make
ormake bin
builds the kernel binarykernel8.img
along withkernel8.elf
. Both are binary files that contain the compiled code of our operating system. The difference is thatkernel8.img
can be loaded by the Pi bootloader, andkernel8.elf
is in a standard format that is recognized by tools likegdb
.make disassemble | less
disassembles the kernel binary. Useful if you need to see where functions or variables are located in memory.make debug
runs the kernel in qemu while allowing you to step through it line-by-line in gdb.make run
runs your kernel in qemu with no debugger.make clean
removes all compiled object files.
The best way to add features is to create a new source file in the src
directory. If you create a new source file, you will need to add it to the OBJS
list in the Makefile (starting around line 15). For example, say you create a new file called src/neil.c
. You will need add a new line in the Makefile:
OBJS = \
boot.o \
kernel_main.o \
neil.o \
Note that the new line we added to the OBJS
list was neil.o
, not neil.c
. Also, you need to make sure you have an empty line after the last element of the OBJS
list, otherwise make
will complain.