Writing a "bare metal" operating system for Raspberry Pi 4 (Part 2) =================================================================== [< Go back to part1-bootstrapping](../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 clean: /bin/rm kernel8.elf *.o *.img > /dev/null 2> /dev/null || true ``` * CFILES is a list of the _.c_ files already existing in the current directory (our input) * OFILES is that same list but replacing _.c_ with _.o_ in each filename - these will be our **object files** containing the binary code, and they'll be generated by the compiler (our output) * GCCFLAGS is a list of parameters that tell the compiler we're building for bare metal and so it can't rely on any standard libraries that it might normally use to implement simple functions - nothing is for free on bare metal! * GCCPATH is the path to our compiler binaries (the location where you unpacked the Arm tools you downloaded previously) 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 _Makefile.gcc.windows_ just as an example. Building -------- 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](https://www.raspberrypi.org/documentation/configuration/config-txt/boot.md). Booting our OS into 64-bit mode will mean that we can take advantage of the larger memory capacity available to the RPi4. Booting ------- 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 >](../part3-helloworld)