shedOS Toolchain and Makefiles

The bulk of our build system consists of Makefiles and other small tools like the compiler, linker and assembler being invoked by those Makefiles to form a single build system. The root Makefile defines some variables needed by all subprojects, namely

We then have a list of rules which describe how specific parts of the project are to be built/run:

The qemu rule configures and starts our virtual machine. The hard disk image rule creates a .hdd image. The sysroot contains all of our operating system; subprojects will be gradually installed there by looping through a list of all the subprojects, installing their headers and then finally installing their executables and libraries. At last, we have a rule to configure and build our entire toolchain. The clean rules are used to clean up our directories, i.e. delete previously built targets like object files. We use an in-source build which means our directories will be flooded which .o files and the like once we run our Makefiles. This might be changed in the future.

The core of the build process is made up by the cross toolchain. It contains the GNU binutils and gcc for compilation. We use a generic x86_64-elf target which we can replace with a custom one later one once our OS becomes more sophisticated. There is not much one needs to modify, but we need to take extra care to disable the red zone. You can read more about what the red zone is, why we need to and how we can disable it on this osdev article. Basically, we just add a new config file which adds mno-red-zone to the multilib options. This creates another libgcc with red zone disabled. We can link with this instead of the default one simply by specifying -mno-red-zone as a compilation option.

Step-by-step guides and additional information can be found on this osdev article about GCC cross-compilation.

We will first take a look at how the kernel is built. Its Makefile defines the

, although we only use the compiler and assembler. Linking will be done through gcc itself.

Each project has two top-level rules, install-headers and install-exec. Install-headers will be run first for each project which will install all the public headers into the sysroot/include directory. This is necessary because other projects might need certain headers for their own compilation process. The install-exec rule will then actually take all source files, be it C or assembly or brainfuck, compile it to an object file and link all of this together to a final executable. While this is pretty simple, there are some flags needed to actually make this work properly.

The most important one is -ffreestanding, making sure that we target a freestanding environment. Together with the -nostdlib linker flag, we completely separate ourselves from all host libraries and make sure no unwanted dependencies are accidentally inserted.

Because we load the kernel at the higher half later on, we need to create a position-independent executable. This is done using the compiler and linker flags -pie -fno-pic -fpie.

We also need to create a custom linker script. An ELF file consists of multiple sections, each of which containing different data. The most important ones are

We also have a special .stivale2hdr section which is needed by the bootloader. What it does and how it works will be discussed in another post. The linker script tells the linker where to put those sections in the final executable. It also puts it at an offset of 0xfffffff80000000 + 2 MiB, i.e. the final 2 GiB of our 64 bit address space. This is called higher half linking.

The actual compilation process is very simple. We just invoke the corresponding program (gcc for compilation and as for assembling) and link all of the objects together.

As you see, setting up a build system in and of itself can present a challenging task. I didn’t have much experience in cross compilation and all the gcc options when I started so I had to do lots of learning and experimentation. One could use their host gcc and skip this whole process, but this is a bad idea as you need to be sure your toolchain doesn’t depend on host libraries AND you also understand your toolchain well enough to solve problems on your own.

Doing things the ‘easy way’ and hiding complexity just leads to more problems later on, so always try to properly learn what you are doing. This holds true for everything, not just for OS development!.


We are now able to compile our whole kernel using just a single make command. Great! But we still can’t run it and also can’t do much as we have no way of printing to the screen or doing any other usable form of I/O. We need a bootloader to load our kernel and give it some information. How we can accomplish this will be presented in part 2 of this series.