Today I solved an os-dev problem I’d been stuck on for a few days. In my hobby OS project, I want the bootable image to contain both the kernel code, and the code of the initial user-level application, packed into a single ELF file, which the bootloader loads before starting the kernel. In order for the kernel to (eventually) hand control over to the user program, it needs to know the user program’s entry point. I have linker script that describes the (virtual) address space layout of the user and kernel memory, which the bootloader instantiates. My original plan was to use the linker to make a symbol available in the kernel code which refers to the entry point of the user program (ie. it’s _start function’s address), but since user and kernel virtual addresses are conventionally very far apart, this turned out to be non-trivial.

I think I have a solution, though I’m yet to fully test it out (some other things have got in the way). The plan is the following: Add a new section to the ELF with a tiny bit of code at the start that just jumps to the user program’s entry point. Something like

; the user "_start" function is renamed to "__user__start" to avoid name conflicts with kernel code
extern __user__start

bits 64

section .user.trampoline

jmp __user__start

This solves the problem because unlike the user’s _start function, which could be located anywhere within the user program’s .text section, we can tell the linker to put the above code at the beginning of a brand new section dedicated just for it:

. = user_base;
.user.trampoline : ALIGN(0x1000) {
} :user

This introduces a user_base symbol which can be extern-ed in kernel code, so when the kernel is ready, it can switch to user mode and move execution to the address referred to by the symbol user_base. Execution will then bounce to the actual user entry point by means of the jmp __user__start instruction.

This technique is known as “trampolining”.