Rose-OS - Makefile teardown

Posted on
OS-Development Assambler Kernel Makefile

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

Bibliography: