Skip to content

Documentation

Lance Goodridge edited this page Mar 16, 2017 · 8 revisions

Project Structure

Our project is modeled on the Linux kernel project, and thus loosely shares its code structure.

At the top layer, we have the Makefile, which builds the project, along with main.c and the other non-architecture-specific source files. It also contains the subdirectories arch, drivers, and include. Building the project will add the output file(s) for each specified target (kernel.elf and/or kernel7.img), and add build subdirectories for each target (build_pi and build_qemu). Finally, the top layer also holds the non-kernel-related files, such as the README, Vagrantfile, and commands.sh file.

The arch directory holds subdirectories for each target architecture supported by the project (currently arm for the Raspberry Pi, and x86 for QEMU). These subdirectories hold essential source files specific to that architecture; namely, boot.s and linker must be present, and any other assembly helper files must be placed here as well.

The drivers directory holds the device driver implementations, which controls how the kernel interfaces with a hardware device for a specific architecture. "Stub" files are also used here for devices which aren't supported by a particular architecture.

The include directory holds the header files for all of the kernel modules. The files in this directory provide a complete interface for our kernel.

Finally the build_pi and build_qemu directories hold the intermediate build files for each of the targets. Namely, you can find the .o files, and alternate formats of the kernel here.

Non-source code files

Makefile

Our Makefile is also (loosely) modeled on the Makefiles for the Linux kernel project, and leverages several of make's more advanced features. For further reading on how to properly construct Makefiles, we recommend browsing through the Make Manual.

The first section of the Makefile defines variables that the user can modify to customize how the project gets built. The ?= operator sets the variable only if it is not already defined, so the first line of our Makefile sets a default value for ARMGNU if the user does not already provide one. The next four lines define the compiler options to use when building from .c or .s files for each architecture. The ARM architecture variables are used when building for the Raspberry Pi, and the x86 architecture variables are used when building for QEMU. The next six lines define where the source/build files are kept. Finally, the PI_SRC and QEMU_SRC variables all of the source files needed to build a particular target. NOTE: After writing a new driver or kernel feature, the corresponding .c or .s files must be added to the appropriate SRC variable(s), or it will not get linked in while building.

The next section of the Makefile defines intermediate variables that are calculated from the user-defined variables above. It is not recommended to change these unless you know what you are doing. The INTER variables are intermediate variables that determine all the .o files that need to be built. To do this, it simply replaces the .c and .s extensions of all the source files with .o. The BUILD_OBJS variables calculate the actual location of the object files, by prepending the corresponding build directory. The TARGET variables define where the alternate kernel build files will be located.

The next section defines the runnable make commands. The .PHONY label indicates that the following targets are not associated with any resultant files, and should be run from scratch every time the command is executed. The commands themselves should be self-explanatory.

Next are the recipes that generate the target output files. The first line of each recipe uses a version of ld, the linker, to link the object files and use the architecture specific linkscript, saving the result as a kernel .elf file. This file is then used to generate other versions of the kernel, which are helpful for running / debugging.

The next section creates and includes and the dependency file for each build target. Since we are using static pattern rules (see next section) to build our object files, we need to generate the dependencies for each source file independently (otherwise, make has no idea when an object file needs to be rebuilt). Each depend recipe uses the appropriate gcc to build the depend file, then performs some text replacement with sed to prepend the correct build directory to the object files. The -include line then inserts the lines from the depend files into the Makefile and executes them. Since we labeled these files as PHONY targets, they will be re-built each time the Makefile is used (which is what we want).

The next section uses static pattern rules to generate required object files. Since the object files are separated by build target, we can use their output directory to determine which compiler / flags to use. Since we need the build directory to exist before we create the object file, we list it as an order-only dependency, by placing it after the pipe symbol (|).

Finally, the last section lists the build directory recipes, which create the required directories if they do not already exist.

Vagrantfile

Vagrant is a lightweight wrapper for VirtualBox that enables easy setup of virtual work environments. The Vagrantfile our project uses creates a virtual environment with all necessary packages installed, so all you have to do is start and log into it.

The first half of the Vagrantfile configures an Ubuntu 14.04 image to be logged into via ssh using X-forwarding. The box uses port 2223 for port-forwarding, and the project directory is mounted to the default location, /vagrant, on the virtual environment. This means that changes you make to the project on your local machine will appear in the virtual environment you sshed into (and vice-versa).

The second half of the Vagrantfile provisions the environment, which installs the packages necessary for compilation, and also moves into the correct directory for convenience.

commands.sh

This file holds bash aliases for common commands. Currently, it only holds run-qemu which starts up the QEMU emulator with a provided kernel .elf file, and run-qemu-gdb, which starts a gdb server for debugging the kernel.

Architecture specific files

linker

The linker script tells the linker how to arrange memory for the kernel. In more extreme cases, you can also have a linker script specify how to link files together or set output formats, but for our purposes, we only need the most basic commands. Here we will only detail the linker script for the ARM architecture, but the x86 version is functionally equivalent, and is written similarly.

The first command, ENTRY, establishes the entry point for the kernel, which is the code the kernel will run first upon startup. This can be set to either a C function, or an assembly label, however the latter is recommended, as it is easier to perform the initial setup in assembly than in C. In our scripts, we set it to the _start label, which we define in boot.s.

The remainder of the script defines the layout of the kernel sections. The special symbol, ., refers to the location counter, which keeps track of the current address as we layout the sections in memory. The location counter is automatically updated each time we specify a section, so we do not need to set it explicitly after giving it an initial value.

The first line provides an initial value for the location counter, which specifies where in memory to start placing kernel sections. We also establish a __start and later an __end label, which hold the start/end addresses of the kernel.

Next we define the .text section, which holds the kernel source code. We set this section (and the others) to be aligned on a 4 Kilobyte boundary with 4 Kilobyte blocks; we do this because some modules rely on byte alignments to work properly, and because doing so keeps our product consistent with other kernels. We then place .text section data at that location, using KEEP to ensure our boot.s file is placed first. Finally, we also define __text_start and __text_end labels, in case knowing the start/end address of the text section proves useful to us later.

The other sections are defined similarly, and are explained below:

  • rodata: Holds the read-only data.
  • data: Holds the static or global variables with pre-initialized values.
  • bss: Holds the static or global variables with uninitialized values (are set to 0).

boot.s

This file performs initial assembly set up for the OS before jumping to the kernel proper. Here, we will only detail the boot.s for the ARM architecture, but the x86 version is functionally equivalent, and describes each step through comments.

First, we define this section of code as the .text.boot section, which is added first to the .text section in the linker script. Next we define the _start label, which is set as the kernel's entry point in the linker. The "entry point" is the first lines of code that will be run when the OS starts.

We reserve 0x8000 bytes of space for the stack by moving the stack pointer, %sp% to that location. Since the stack grows towards lower addresses, the stack is "full" when %spis at0x8000, and "empty" when %spis at0x0`.

The next bit of code sets the bss section (uninitialized data) to zero. (TBC)

Kernel

Drivers