An in-depth Explanation - Makefile
Makefile source
Recall the source for the makefile
# Don't use normal gcc, use the arm cross compiler
CC = ./gcc-arm-none-eabi-6-2017-q2-update/bin/arm-none-eabi-gcc
# Set any constants based on the raspberry pi model. Version 1 has some differences to 2 and 3
ifeq ($(RASPI_MODEL),1)
CPU = arm1176jzf-s
DIRECTIVES = -D MODEL_1
else
CPU = cortex-a7
endif
CFLAGS= -mcpu=$(CPU) -fpic -ffreestanding $(DIRECTIVES)
CSRCFLAGS= -O2 -Wall -Wextra
LFLAGS= -ffreestanding -O2 -nostdlib
# Location of the files
KER_SRC = ../src/kernel
KER_HEAD = ../include
COMMON_SRC = ../src/common
OBJ_DIR = objects
KERSOURCES = $(wildcard $(KER_SRC)/*.c)
COMMONSOURCES = $(wildcard $(COMMON_SRC)/*.c)
ASMSOURCES = $(wildcard $(KER_SRC)/*.S)
OBJECTS = $(patsubst $(KER_SRC)/%.c, $(OBJ_DIR)/%.o, $(KERSOURCES))
OBJECTS += $(patsubst $(COMMON_SRC)/%.c, $(OBJ_DIR)/%.o, $(COMMONSOURCES))
OBJECTS += $(patsubst $(KER_SRC)/%.S, $(OBJ_DIR)/%.o, $(ASMSOURCES))
HEADERS = $(wildcard $(KER_HEAD)/*.h)
IMG_NAME=kernel.img
build: $(OBJECTS) $(HEADERS)
echo $(OBJECTS)
$(CC) -T linker.ld -o $(IMG_NAME) $(LFLAGS) $(OBJECTS)
$(OBJ_DIR)/%.o: $(KER_SRC)/%.c
mkdir -p $(@D)
$(CC) $(CFLAGS) -I$(KER_SRC) -I$(KER_HEAD) -c $< -o $@ $(CSRCFLAGS)
$(OBJ_DIR)/%.o: $(KER_SRC)/%.S
mkdir -p $(@D)
$(CC) $(CFLAGS) -I$(KER_SRC) -c $< -o $@
$(OBJ_DIR)/%.o: $(COMMON_SRC)/%.c
mkdir -p $(@D)
$(CC) $(CFLAGS) -I$(KER_SRC) -I$(KER_HEAD) -c $< -o $@ $(CSRCFLAGS)
clean:
rm -rf $(OBJ_DIR)
rm $(IMG_NAME)
run: build
qemu-system-arm -m 256 -M raspi2 -serial stdio -kernel kernel.img
Lets step through this line by line
Setting the Variables
CC = ./gcc-arm-none-eabi-6-2017-q2-update/bin/arm-none-eabi-gcc
This sets the compiler that we will be using to the cross compiler we downloaded when setting up our development environment
ifeq ($(RASPI_MODEL),1)
CPU = arm1176jzf-s
DIRECTIVES = -D MODEL_1
else
CPU = cortex-a7
endif
There are differences in hardware between raspberry pi models. Since I am developing for a Model 2 VM and a real Model 1, I make the Model 2 stuff default, and have to trigger the Model 1 stuff manually. I trigger this by passing RASPI_MODEL=1
as a parameter to make
.
In addition to setting the appropriate CPU, it also adds defines the preproccessor directive MODEL_1
. Any C code that depends on differing hardware, like the peripheral base address, can determine which hardware to compile for using #ifdef MODEL_1
as a preprocessor directive.
CFLAGS= -mcpu=$(CPU) -fpic -ffreestanding $(DIRECTIVES)
CSRCFLAGS= -O2 -Wall -Wextra
LFLAGS= -ffreestanding -O2 -nostdlib
These are the just the same compiler and linker flags that we used before, but extracted into variables
KER_SRC = ../src/kernel
KER_HEAD = ../include
COMMON_SRC = ../src/common
These lines specify the directories where the kernel source files, header files, and common source files reside, respectively. They start with ../
since the Makefile will be running out of the build directory.
OBJ_DIR = objects
Similarly, this line specifies the location of a folder to store all binary object files. This directory may not exist, and you should not create it. It will be created by the make file when it is needed.
KERSOURCES = $(wildcard $(KER_SRC)/*.c)
COMMONSOURCES = $(wildcard $(COMMON_SRC)/*.c)
ASMSOURCES = $(wildcard $(KER_SRC)/*.S)
...
HEADERS = $(wildcard $(KER_HEAD)/*.h)
This gets a list of all the kernel source files, common source files, assembly source files, and header files, respectively. wildcard
is a make command that enumerates all files that match the pattern. So $(wildcard $(KER_SRC)/*.c)
means enumerate all files in the KER_SRC
directory that ends with a .c
extension.
OBJECTS = $(patsubst $(KER_SRC)/%.c, $(OBJ_DIR)/%.o, $(KERSOURCES))
OBJECTS += $(patsubst $(COMMON_SRC)/%.c, $(OBJ_DIR)/%.o, $(COMMONSOURCES))
OBJECTS += $(patsubst $(KER_SRC)/%.S, $(OBJ_DIR)/%.o, $(ASMSOURCES))
This takes the lists of kernel source files, common source files, and assembly source files, and adds them all to a list of compiled object files that will be created. It does this using patsubst
, which matches each element in the list passed as the third argument to the pattern in the first argument. It discards anything matched and keeps anything covered by the %
. It then makes what remains match the pattern described in the second argument. For example, $(patsubst $(KER_SRC)/%.c, $(OBJ_DIR)/%.o, $(KERSOURCES))
changes all files that look like ../src/kernel/file.c
to be objects/file.o
.
IMG_NAME=kernel.img
This variable is the name of the kernel binary
The Make Targets
Makefiles work through a series of targets. Each target has a series of dependencies and a series of commands to execute. If any dependencies do not exist or are newer than the target, the commands are executed. A target is executed by running make target
. Our makefile has the following targets:
build: $(OBJECTS) $(HEADERS)
echo $(OBJECTS)
$(CC) -T linker.ld -o $(IMG_NAME) $(LFLAGS) $(OBJECTS)
This is the main target that compiles the kernel. It can be invoked by running make build
. It depends on all of the object files for the respective source code files, and all the header files. This means that all the object files must be compiled before this can execute. Its only command is to link all of the objects together into the final kernel binary.
$(OBJ_DIR)/%.o: $(KER_SRC)/%.c
mkdir -p $(@D)
$(CC) $(CFLAGS) -I$(KER_SRC) -I$(KER_HEAD) -c $< -o $@ $(CSRCFLAGS)
This target is any object file that depends on a kernel source file. It creates the objects directory if it does not exist, then compiles the source file into the object file. -I
allows source files to access include files by #include <kernel/header.h>
instead of #include <../../include/kernel/header/h>
. $<
is the name of the first dependency, so the c source file. $@
is the name of the target file itself.
There are two more targets that look very similar, but one is for kernel assembly files instead of kernel source files, and one is for common source files.
clean:
rm -rf $(OBJ_DIR)
rm $(IMG_NAME)
This target removes all of the compiled binaries so that no stale file is accidentally used when building
run: build
qemu-system-arm -m 256 -M raspi2 -serial stdio -kernel kernel.img
This ensures that the kernel is compiled, and then runs the VM with that compiled kernel
This reduces the build and test process down to
make build
make run