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:
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.
PT_LOAD,
we’ll need those in a moment.memsz and filesz.e_entry field
in the ELF header. This field is the start function of the program.
You’ll also need to create a stack in the memory space of this program
for the thread to use, if this wasn’t already done as part of our thread
creation.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.
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:
ELFMAG part. It is expected to be the following
values:| Value | Byte |
|---|---|
0x7f |
0 |
E |
1 |
L |
2 |
F |
3 |
x86_64 architecture endiannes is LSB, then
the value is expected to be 1. This field is in the byte 5.Then from the other fields that need validation (that area not in the
e_ident field) are:
e_type: this identifies the type of elf, for our
purpose the one to be considered valid this value should be 2 that
indicates an Executable File (ET_EXEC) there are other values that in
the future we could support, for example the ET_DYN type
that is used for position independent code or shared object, but they
require more work to be done.e_machine: this indicates the required architecture for
the executable, the value depends on the architectures we are
supporting. For example the value for the AMD64 architecture is
62Be 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.elfFor 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.elfWill print out all the executable information, including the type.
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.oWhere: * -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 loopWhere -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).
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:
x86_64 these are called rings (ring
0 = kernel, ring 3 = user), other platforms
may use different names. See the userspace chapter for more detail.