This page looks best with JavaScript enabled

MercuryOS Building the OS image using gmake

 ·  🎃 kr0m

Until now, every time we made any modification to the bootloader or our OS, we had to manually recompile and link the files. Throughout history, previous programmers have faced this situation, so they created a tool called make. With make, we can indicate dependencies between files and commands to execute in case any of them have been modified.

Before we start, it is recommended that you read these previous articles:


As we are going to use GNU make, we will have to install gmake:

pkg install png gmake pkgconf

A basic example could be this:

vi Makefile

kernel.o : kernel.c
    cc -m32 -ffreestanding -c kernel.c -o kernel.o

NOTE: Be careful with spaces and tabulations because Make requires that they are real tabulations.

We can compile the kernel with the command:

gmake kernel.o
cc -m32 -ffreestanding -c kernel.c -o kernel.o

The beauty of Make is that it will only execute the command if any of the dependencies have been modified. If we run make again, it will indicate that we already have the latest version and there is no need to recompile:

gmake kernel.o

gmake: 'kernel.o' is up to date.

A more advanced Makefile could be this one, when running make kernel.bin, if any of the dependencies need to be recompiled, it will do so automatically:

vi Makefile

# Build the kernel binary
kernel.bin : kernel_entry.o kernel.o
    ld -o kernel.bin -Ttext 0x1000 kernel_entry.o kernel.o --oformat binary

# Build the kernel object file
kernel.o : kernel.c
    cc -m32 -ffreestanding -c kernel.c -o kernel.o

# Build the kernel entry object file .
kernel_entry.o : kernel_entry.asm
    nasm kernel_entry.asm -f elf -o kernel_entry.o

Make supports several special variables, making script maintenance easier as the project grows:

  • $^: Indicates all dependencies
  • $<: Indicates the first dependency
  • $@: Indicates the file to be generated

Below is the final version of my Makefile:

vi Makefile

# Run Qemu to simulate booting of our code .
run : all
    qemu-system-x86_64 os-image

# Compile all elements
all : os-image

# This is the actual disk image that the computer loads ,
# which is the combination of our compiled bootsector and kernel
os-image : boot_sect.bin kernel.bin
    cat $^ > os-image

# This builds the binary of our kernel from two object files :
# - the kernel_entry , which jumps to main () in our kernel
# - the compiled C kernel
kernel.bin : kernel_entry.o kernel.o
    ld -o kernel.bin -Ttext 0x1000 $^ --oformat binary

# Build our kernel object file .
kernel.o : kernel.c
    cc -m32 -ffreestanding -c $< -o $@

# Build our kernel entry object file .
kernel_entry.o : kernel_entry.asm
    nasm $< -f elf -o $@

# Assemble the boot sector to raw machine code
# The -I options tells nasm where to find our useful assembly
# routines that we include in boot_sect . asm
boot_sect.bin : boot_sect.asm
    nasm -f bin $< -o $@

# Clear away all generated files .
clean :
    rm -fr *.bin *.dis *.o os-image *.map

# Disassemble our kernel - might be useful for debugging .
kernel dis : kernel.bin
    ndisasm -b 32 $< > $@

We clean up and check that the whole process works:

gmake clean
rm -fr *.bin *.dis *.o os-image *.map

We compile everything necessary, generate the disk image, and load it into Qemu, all in one command.

gmake run

Excellent, everything should have worked smoothly and we will have the latest version of our bootloader/kernel running in Qemu. Whenever we want to modify something, we just need to run make run and our script will take care of recompiling everything necessary.

If you liked the article, you can treat me to a RedBull here