Chapter 2: Build infrastructure¶
aarch64-none-elf toolchain¶
If you’ve done other bare-metal ARM projects, you might be used to the
arm-none-eabi toolchain. For this project, we’ll need the arm64
equivalent: aarch64-none-elf. You’ll probably need to fetch this
from the ARM toolchain download page and add it to your PATH. I
unpacked it into ~/.local/aarch64-none-elf, and added the bin
dir to my PATH.
Makefile¶
Later on, this might get more complicated; for now, a short Makefile will suffice. The code will be laid out as
.
├── build
├── inc
└── src
First, we’ll set up some basic tooling and variables:
## VARIABLES
# B - build dir
# D - disk (SD) image
# H - header file / include dir
# I - output image (place this on µSD card)
# L - linker script
# M - map file
# P - path to sources
# S - assembler listing
# T - toolchain
B ?= build
D ?= $(B)/sd.img
H ?= inc
I ?= $(B)/kernel8.img
L ?= pi4.ld
M ?= $(B)/kernel8.map
P ?= src
S ?= $(B)/kernel8.list
T ?= aarch64-none-elf
PLATFORM := pi4
ELF := $(B)/kospi64.elf
AS := $(T)-as
CC := $(T)-gcc
CXX := $(T)-g++
LD := $(T)-ld
OC := $(T)-objcopy
OD := $(T)-objdump
The build flags will be fairly standard using compile flags targeting the Cortex A72; we will be providing out own startfile and standard library.
BUILD_FLAGS := -O2 -Wall -Werror -ffreestanding -march=armv8-a+crc \
-nostartfiles -nostdinc -nostdlib \
-Wno-unused-command-line-argument -I$(H)
LDFLAGS := -nostdlib --no-undefined
CFLAGS := $(BUILD_FLAGS)
CXXFLAGS := -std=c++17 $(BUILD_FLAGS)
Next, collect all the source files and collect them into a variable describing the objects that need to be built.
SRCXX := $(wildcard $(P)/*.cc)
SRCCC := $(wildcard $(P)/*.c)
SRCAS := $(wildcard $(P)/*.S)
OBJS := $(patsubst $(P)/%.S,$(B)/%.o,$(SRCAS))
OBJS += $(patsubst $(P)/%.cc,$(B)/%.o,$(SRCXX))
OBJS += $(patsubst $(P)/%.c,$(B)/%.o,$(SRCCC))
Later, we’ll use podman to build and test this with an image I’ve prepared with the development tooling needed to cross-compile.
PODMAN ?= podman
PIMAGE ?= git.wntrmute.dev/kyle/armdev:latest
Make’s default target should be the kernel image we’re building.
all: $(I)
The list target produces an assembly language listing of the
kernel.
list: $(S)
$(S): $(ELF)
$(OD) -d $(ELF) > $@
The contents of the ELF will be dumped into a memory dump. All of its symbols and relocation information are discarded.
$(I): $(ELF)
$(OC) $(ELF) -O binary $@
The ELF file is the linked output of all the object files; the build should also produce a remapping list. In order to build it, we need a linker map describing how memory on the Raspberry Pi is laid out. The object files are built in a pretty standard way.
$(ELF): $(OBJS) $(L)
$(LD) -o $@ $(LDFLAGS) $(OBJS) -Map $(M) -T $(L)
$(B)/%.o: $(P)/%.S $(B)
$(CC) -o $@ $(BUILD_FLAGS) -c -I $(P) $<
$(B)/%.o: $(P)/%.cc $(B)
$(CXX) -o $@ -c $(CXXFLAGS) -I $(P) $<
$(B)/%.o: $(P)/%.c $(B)
$(CC) -o $@ -c $(CFLAGS) -I $(P) $<
$(B):
mkdir $@
Finally, we have a few utility targets. Right now, the disk image
isn’t actually being used for anything — later on, I’d like to get
qemu emulation support working. The print- target shows the value
of whatever variable is supplied; e.g.
% make print-OBJS
OBJS= build/main.o
The devbox targets use podman to build a development image for the
kernel. The devdeps target assumes a Debian-like system.
$(D): $(B)
dd if=/dev/zero of=$@ bs=128k count=8192
mkfs.msdos -n boot $@
print-%: ; @echo '$(subst ','\'',$*=$($*))'
clean :
-rm -rf $(B) $(I) $(S) $(M) $(D)
devbox-build:
$(PODMAN) build -t $(PIMAGE) -f Containerfile
devbox-run:
$(PODMAN) run -i -t -v $(PWD):/build $(PIMAGE)
devdeps:
sudo apt install binutils-arm-none-eabi gcc-arm-none-eabi \
libnewlib-arm-none-eabi libstdc++-arm-none-eabi-dev \
libstdc++-arm-none-eabi-newlib \
libstdc++-arm-none-eabi-picolibc picolibc-arm-none-eabi
.PHONY: all list clean emulate emulate-vga devbox-build devbox-run devdeps
The next step is to supply the bare minimum to build the kernel:
a main souce file
an assembly-language bootloader
a linker script