Rose-OS - Makefile teardown
Table of Contents
In this post, I will be describing programs and resources used to build Rose-OS and how we combine then into a single, automated Makefile.
Programs used to generate the kernel
- gcc
- nasm
- bochs (Emulator)
- ld
- cat
- rm
gcc
Is the compiler we use to compile our code. Normally, this command preprocesses, compiles, assembles and links our code, but we can stop it from doing this with some command-line options.
The command:
gcc -m32 -ffreestanding -c kernel/kernel.c -o kernel/kernel.o -fno-pie
With the parameters:
-m32
: Compile 32-bit objects instead of the default 64-bit.-c
: Do not run the linker, instead, output object files outputted from the assambler.-ffreestanding
: Directs the compiler to not assume that standard functions have their usual definitions.-fno-pie
: Negative form of-fpie
, which stands for Position Independent Code. This option disables the random code positioning security feature.-o
: Describes the name and location of the object file being generated.
ld
ld
combines a number of object and archive files, relocates their data and ties up symbol references.
Usually the last step in compiling a program is to run ld.
The command:
ld -m elf_i386 -o kernel/kernel.bin -Ttext 0x1000 asm/kernel_entry.o kernel/kernel.o --oformat binary
With the parameters:
-m elf_i386
: The emulation mode. In our case, we want to emulate for the Intel 80386 architecture.-Ttext 0x1000
: Locate a section in the output file at the absolute address given by org. Start the .text section at address 0x1000.--oformat binary
: The output format of the file being generated.
Here we are providing initially the kernel_entry object file because this file contains the definition for the main kernel function. This is the first method we are going to execute.
In our Makefile, we provide a list of .o
files after specifying the kernel_entry.
nasm
The Netwide Assembler, a portable 80x86 assembler
The commands:
nasm rose-os_boot_sector.asm -f bin -o rose-os_boot_sector.bin
nasm asm/kernel_entry.asm -f elf -o asm/kernel_entry.o
With the parameters:
-f bin | elf
: Specifies the output file format.-I './boot/'
: Adds a directory to the search path for include files
Bochs
Bochs is a 32-bit emulator that also serves as a debugger.
We have chosen to work with this emulator because it provides greater control over the executables being run. It provides out-of-the-box features to break, continue, step into, or step out of the run binary code.
The configuration of this program is managed by a .boshrc
file located in the directory the program is being run from.
In my case, the .boshrc
file looks like this:
floppya: 1_44=os-image, status=inserted
log: ./bochs/bochsout.txt
debugger_log: ./bochs/debugger.out
boot: floppy
In this file, we are stating that we are loading our system with a inserted floppy disk containing the binary os-image
.
The log and debugger_log files are saved to a custom directory, to keep things organised.
Cat
Although being a very basic GNU/Linux command, this command is invaluable for kernel development, because it lets us create a kernel image binary file.
This file contains the boot sector and the kernel in one file.
The command:
cat boot/rose-os_boot_sector.bin kernel/kernel.bin > os-image
Other commands
They are used to clean the working directory.
Combining these commands
Make
With the make command, we automate the build, execution and clean process.
Otherwise, when we change a file, we have to execute a bunch of commands to compile, build and link our code. Make takes care of all of this in an automated way.
My Makefile is as follows:
# Automatically generate lists of sources using wildcards.
C_SOURCES = $(wildcard kernel/*.c drivers/*.c)
HEADERS = $(wildcard kernel/*.h drivers/*.h)
# TODO: Make sources dep on all header files.
# Convert the *.c filenames to *.o to give a list of object files to build
OBJ = ${C_SOURCES:.c=.o}
# Defaul build target
all: os-image
# Run bochs to simulate booting of our code.
run: all
bochs
# This is the actual disk image that the computer loads
# which is the combination of our compiled bootsector and kernel
os-image: boot/rose-os_boot_sector.bin kernel/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/kernel.bin: boot/kernel_entry.o ${OBJ}
ld -m elf_i386 -o $@ -Ttext=0x1000 $^ --oformat binary
# Generic rule for compiling C code to an object file
# For simplicity, we C files depend on all header files.
%.o : %.c ${HEADERS}
gcc -ffreestanding -fno-pie -m32 -c $< -o $@
# Assemble the kernel_entry.
%.o : %.asm
nasm $< -f elf -o $@
%.bin : %.asm
nasm $< -f bin -I ’./boot/’ -o $@
clean:
rm -fr *.bin *.dis *.o os-image
rm -fr kernel/*.o kernel/*.bin boot/*.bin drivers/
Upon inspecting this file, we can see the commands listed above are in this file, but we use regular expressions and macros to shorten the needed commands further.
If we run make
without arguments, then the first instruction will be run, this is why the first instruction
all: os-image
has as a dependency another instruction,
os-image: boot/rose-os_boot_sector.bin kernel/kernel.bin
that triggers the whole make process.
Project structure
I have decided, based of the os-dev book, to create several directories.
- boot: All the assambly code needed to boot the OS is located here
- kernel: The C code that compiles the kernel is stored here
- drivers: Any hardware-specific code