Kernel Implementation: Load an Executable

Our next major implementation task is to load and run an executable. We’ll approach this in three steps:

  1. We need an executable to load, so we’ll have to write some code and update our build system to create an executable.
  2. We need to get the executable to our kernel. We will use our bootloader’s support for kernel modules to pass the executable file to the kernel.
  3. Finally, we need to read the executable file, load it into memory, and jump to its entry point.

We’ll discuss steps 1 and 2 in class, working from the code available on this page. Once we’ve done that, we’ll spend time figuring out a general approach to step 3.

Create an executable to run

Our kernel is going to load and run a program named init. Download and extract the init.tar.gz file to get the provided code for this executable. Once you’ve extracted the archive you should have an init directory; move this directory into your kernel project (so it sits at the same level as the kernel and limine directories).

If you open a terminal and change into the init directory you should be able to run make to build it. If that fails for any reason please ask for help!

We want init to build as part of our full build of the os project, so we’re going to have to modify the top-level Makefile as well. First, we’ll create a target in the top-level Makefile that recursively builds in the init directory. Add this below the similar rule for kernel:

.PHONY: init
init:
	$(MAKE) -C init

We also want our clean target to recursively clean the init build as well. You already have a clean target, but you should update it so it looks like this:

.PHONY: clean
clean:
	rm -f iso_root boot.iso
	$(MAKE) -C kernel clean
	$(MAKE) -C init clean

Finally, we need to update the boot.iso target so it depends on the init program. The first line of that target lists dependencies; add init to the list so the full line looks like this:

boot.iso: limine kernel init limine.cfg

Now when you run make at the top level you should build the init program along with everything else. Run make clean, verify that init/init is gone, then run make, and verify that init/init exists before moving on.

Getting the executable to our kernel

Our bootloader has support for kernel modules, which are really just files that the bootloader copies into memory along with our kernel. We’re going to edit the bootloader configuration in limine.cfg to include our init program as a kernel module, and then access that module using the bootloader’s modules structure tag.

First, we need to put the init program on the disk image that our OS boots from. To do this, modify the cp command that runs under the boot.iso target in your top-level Makefile. Just add init/init right after kernel/kernel.elf on that line so the full line looks like this:

	cp kernel/kernel.elf init/init limine.cfg limine/limine.sys limine/limine-cd.bin limine/limine-eltorito-efi.bin iso_root/

All that change does is copy the init program into a temporary directory that is used to create the CD image. Next up, we’ll need to update the bootloader configuration to tell it to load init as a module. Add these lines to the end of limine.cfg:

# Load the init program as a module
MODULE_PATH=boot:///init
MODULE_STRING=init

Finally, you’ll need to update your boot.c file to find the STIVALE2_STRUCT_TAG_MODULES_ID tag and locate the loaded module file. For now you can just print out the module string and its start and end addresses:

Modules:
  init 0xffff80007f3f3000-0xffff80007f5f32f0

Loading an executable

We discussed the procedure for loading an ELF file in class. At a high leve, the process for loading an ELF file is:

  1. Find the module that holds our ELF file by navigating the bootloader’s provided tags
  2. The module is loaded in memory at the begin address, which we can cast to an ELF header pointer.
  3. From the ELF header, locate the program header table. The ELF header specifies an offset, which you add to the starting address of the ELF file (note that all offsets in ELF are offsets from the start of the ELF file).
  4. Loop over all entries in the program header table. The size of each entry and the number of entries are both recorded in the ELF header.
  5. For each program header entry that has type LOAD and a size greater than zero: A. Use vm_map to map the page(s) necessary to hold the program header at the requested virtual address. B. Use memcpy to copy data from the ELF file (beginning of file + offset) to this virtual address. C. Update the permissions of the mapped memory to reflect the permissions specified in the program header table. You’ll need to map memory as writable at first, but you should use vm_protect to make it read-only and/or executable as requested.
  6. Once you’ve mapped and copied in all program headers, cast the entry address (stored in the ELF header) to a function pointer and call it.