Skip to the content.

Writing a “bare metal” operating system for Raspberry Pi 4 (Part 2)

< Go back to part1-bootstrapping

Making a makefile

I could now just tell you the commands required to build this very simple kernel one after the other, but let’s try to future-proof a little. I anticipate that our kernel will become more complex, with multiple C files needing to be built. It therefore makes sense to craft a makefile. A makefile is written in (yet another) language that automates the build process for us.

If you’re using Arm gcc on Linux, save the following as Makefile (in the repo as Makefile.gcc):

CFILES = $(wildcard *.c)
OFILES = $(CFILES:.c=.o)
GCCFLAGS = -Wall -O2 -ffreestanding -nostdinc -nostdlib -nostartfiles
GCCPATH = ../../gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin

all: clean kernel8.img

boot.o: boot.S
	$(GCCPATH)/aarch64-none-elf-gcc $(GCCFLAGS) -c boot.S -o boot.o

%.o: %.c
	$(GCCPATH)/aarch64-none-elf-gcc $(GCCFLAGS) -c $< -o $@

kernel8.img: boot.o $(OFILES)
	$(GCCPATH)/aarch64-none-elf-ld -nostdlib boot.o $(OFILES) -T link.ld -o kernel8.elf
	$(GCCPATH)/aarch64-none-elf-objcopy -O binary kernel8.elf kernel8.img

	/bin/rm kernel8.elf *.o *.img > /dev/null 2> /dev/null || true

There then follows a list of targets with their dependencies listed after the colon. The indented commands underneath each target will be executed to build that target. It’s hopefully easy to see that to build boot.o, we depend on the existence of the source code file boot.S. We then run our compiler with the right flags, taking boot.S as our input and generating boot.o.

% is a matching wildcard character within a makefile. So, when I read the next target, I see that to build any other file that ends in .o we require its similarly-named .c file. The command list underneath is then executed with $< being replaced by the .c filename and $@ being replaced by the .o filename.

Carrying on, to build kernel8.img we must first have built boot.o and also every other .o file in the OFILES list. If we have, we run the ld linker to join boot.o with the other object files using our linker script, link.ld, to define the layout of the kernel8.elf image we create. Sadly, the ELF format is designed to be run by another operating system so, for a bare metal target, we use objcopy to extract the right sections of the ELF file into kernel8.img. This is the kernel image that we’ll eventually boot our RPi4 from.

I would now hope that the “clean” and “all” targets are self-explanatory.

Makefiles on other platforms

If you’re using clang on Mac OS X, the file already named Makefile in the repo will be the one you need. Ensure the LLVMPATH is correctly set, of course. It doesn’t look much different to the Arm gcc one, so the above explanation mostly applies.

Similarly, if you’re using Arm gcc natively on Windows, part8-breakout-ble has a just as an example.


Now that we have our Makefile in place, we simply type make to build our kernel image. Since “all” is the first target listed in our Makefile, make will build this unless you tell it otherwise. When building “all”, it will first clean up any old builds and then make us a fresh build of kernel8.img.

Copying our kernel image to the SD card

Hopefully you already have a micro-SD card with the working Raspbian image on it. To boot our kernel instead of Raspbian we need to replace any existing kernel image(s) with our own, whilst taking care to keep the rest of directory structure intact.

On your dev machine, mount the SD card and delete any files on it that begin with the word kernel. A more cautious approach may be to simply move these off the SD card into a backup folder on your local hard drive. You can then restore these easily if needed.

We’ll now copy our kernel8.img onto the SD card. This name is meaningful and it signals to the RPi4 that we want it to boot in 64-bit mode. We can also force this by setting arm_64bit to a non-zero value in config.txt. Booting our OS into 64-bit mode will mean that we can take advantage of the larger memory capacity available to the RPi4.


Safely unmount the SD card from your dev machine, put it back into your RPi4 and power it up.

You’ve just booted your very own OS!

As exciting as that sounds, all you’re likely to see after the RPi4’s own “rainbow splash screen” is an empty, black screen. However, we shouldn’t be so surprised: we haven’t yet asked it to do anything other than spin in an infinite loop.

The foundations are laid though, and we can start to do exciting things now. Congratulations for getting this far!

Go to part3-helloworld >