Skip to content

Latest commit

 

History

History
73 lines (48 loc) · 6.32 KB

02_Loading_And_Running.md

File metadata and controls

73 lines (48 loc) · 6.32 KB

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:

  • It cannot contain any relocations. We don't care about static linking or position independent code (PIC) however, as that doesn't affect the loader.
  • All libraries must be statically linked, we won't support dynamic linking for now. This feature isn't too hard to implement, but we will leave this as an exercise to the reader.
  • The program must be freestanding! As of right now we don't have a libc that targets our kernel. It can be worth porting (or writing) a libc later on.

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.

  • First a copy of the ELF file to be loaded is needed. The recommended way is to load a file via the VFS, but it could be a bootloader module or even embedded into the kernel.
  • Then once the ELF is loaded, we need verify that its header is correct. Also check the architecture (machine type) matches the current machine, and that the bit-ness is correct (don't try to run a 32-bit program if you don't support it!).
  • Find all the program headers with the type PT_LOAD, we'll need those in a moment.
  • Create a new address space for the program to live in. This usually involves creating a new process with a new VMM instance, but the specifics will vary depending on your design. Don't forget to keep the kernel mappings in the higher half!
  • Copy the loadable program headers into this new address space. Take care when writing this code, as the program headers may not be page-aligned:. Don't forget to zero the extra bytes between memsz and filesz.
  • Once loaded, set the appropriate permission on the memory each program header lives in: the write, execute (or no-execute) and user flags.
  • Now we'll need to create a new thread to act as the main thread for this program, and set its entry point to the 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 wasnt 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.

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:

  • The first thing we want to check is the magic number, this is the ELFMAG part. It is expected to be the following values:
Value Byte
0x7f 0
E 1
L 2
F 3
  • We need to check that the file class match with the one we are supporting. There are two possible classes: 64 and 32. This is byte 4
  • The data field indicates the endiannes, again this depends on the architecture used. It can be three values: None (0), LSB (1) and MSB (2). For example x86_64 architecture endiannes is LSB, then the value is expected to be 1. This field is in the byte 5.
  • The version field, byte 6, to be a valid elf it has to be set to 1 (EVCURRENT).
  • The OS ABI and ABI version they identify the operating system together with the ABI to which the object is targeted and the version of the ABI to which the object is targeted, for now we can ignore them, the should be 0.

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 62

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

Beware 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.

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:

  • If the program is going to be loaded into userspace (rather than in the kernel) we will need to map all the memory we want to allow the program to use as user-accessible. This means not just the program headers but also the stack. We'll want to mark all this memory as user-accessible after copying the program data in there though.
  • Again if the program being loaded is a user program the scheduler will need to handle switching between different privilege levels on the cpu. On 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.
  • As mentioned earlier in the scheduling chapter, don't forget to call a function when exiting the main thread of the program! In a typical userspace program the standard library does this for us, but our programs are freestanding so it needs to be done manually. If coming from userspace this will require a syscall.