Loading and Running an ELF

Before we start, we’re going to apply a few restrictions to our program loader. These are things you can easily add later, but they only serve to complicate the process.

For a program to be compatible with our loader:

Steps Required

In the previous chapter we looked at the details of loading program headers, but we glossed over a lot of the high level details of loading a program. Assuming we want to start running a new program (we’re ignoring fork() and exec() for the moment), we’ll need to do a few things. Most of this was covered in previous chapters, and now it’s a matter of putting it all together.

If all of the above are done, then the program is ready to run! We now should be able to enqueue the main thread in the scheduler and let it run.

Verifying an ELF file

When verifying an ELF file there are few things we need to check in order to decide if an executable is valid, the fields to validate are at different points in the ELF header. Some can be found in the e_ident field, like the following:

Value Byte
0x7f 0
E 1
L 2
F 3

Then from the other fields that need validation (that area not in the e_ident field) are:

Be aware that most of the variables and their values have a specific naming convention, for more information refer to the ELF specs.

Another thing to be aware is that some compilers when generating a simple executable are not using the ET_EXEC value, but it could be of the type ET_REL (value 1), to obtain an executable we need to link it using a linker. For example if we generated the executable: example.elf with ET_REL type, we can use ld (or another equivalent linker):

ld -o example.o example.elf

For basic executables, we most likely don’t need to include any linker script.

If we want to know the type of an elf, we can use the readelf command, if we are on a unix-like os:

readelf -e example.elf

Will print out all the executable information, including the type.

Example: Creating a simple program for our kernel

For this example, let’s create a very basic assembly program, that is a simple infinite loop:

extern loop
[bits 64]
loop:
    jmp loop

The code, as we can expect is pretty simple, and self-explanatory, it declares a loop function, and mark it as global using the extern keyword.

The above code now can be compiled with nasm:

nasm -g -f elf64 -F dwarf example_file.s -o example_file.o

Where: * -f elf64 is the output format (in our case we use elf64, but this depend on the target architecture). * -g enable debug symbols * -F dwarf is the debug symbols format (for elf64 we use dwarf, but again it can depends on the target architecture).

The last step is to use a linker to link the file, in this example we are going to use ld:

ld -g example_file.o -o example_file.elf -e loop

Where -g is a parameter that instructs the linker to include the debugging symbols, and -e loop instructs the linker to look for the symbol called loop as entry point of the program.

Now the program is ready to be loaded by our kernel, either as a bootloader module or a file on a filesystem (or any other way that allow the kernel to reach this executable).

Caveats

As we can already see from the above restrictions there is plenty of room for improvement. There are also some other things to keep in mind: