Skip to content

Lesson 1 of 9 · 30m

What the Kernel Does When You Run a Program

fork, execve, ELF loading, and the syscall boundary — the machinery behind `./server` actually starting.

The two-syscall birth

You deploy a Go binary, SSH in, and run ./server. Between your shell printing a prompt and your main() executing, the kernel does a remarkable amount of work on your behalf — and understanding it is the difference between guessing and knowing when startup misbehaves.

A new process is never "created" directly. Your shell calls fork(), which clones the calling process: same memory (copy-on-write), same file descriptors, same credentials. The child then calls execve("./server", argv, envp), which is the syscall that actually throws away that copied address space and loads your program image. This fork-plus-exec split is why you can do things between the two calls — the shell sets up redirections, pipes, and environment there.

The kernel then parses the ELF header (peek at yours with readelf -h ./server), finds the PT_INTERP program header naming the dynamic loader — usually /lib64/ld-linux-x86-64.so.2 — and maps the loader plus your executable's segments into the new address space. Nothing is loaded yet: the pages are mapped but empty, and the CPU demand-faults them in from disk as they're touched. This is why a 500 MB binary starts instantly.

Try this

Watch the whole birth with strace:

strace -f -e trace=execve,mmap,openat /bin/ls 2>&1 | head -40
readelf -l /bin/ls | grep INTERP -A1
cat /proc/self/maps | head -20

The first line of the maps output shows your own address space layout: text, heap, stack, mapped libraries, and the [vdso] region — a kernel-provided library mapped into every process that implements gettimeofday and friends without a context switch into ring 0.

The syscall boundary

Everything a process asks the kernel to do — open a file, allocate memory, send a packet — crosses one boundary: the syscall instruction. Your process runs in ring 3 (user mode); the kernel runs in ring 0. There is no other way in. Every permission check, every cgroup limit, every audit hook lives on that boundary, which is why it's the foundation of Linux security: a compromised userspace process can only ever ask for things the kernel is willing to grant.

Field notes

  • file: cannot execute: No such file or directory on a binary that clearly exists almost always means the PT_INTERP loader is missing — classic on Alpine containers running glibc binaries.
  • LD_PRELOAD and LD_LIBRARY_PATH are honored by the loader, not the kernel — which is why they don't work on setuid binaries (the loader drops them for security).
  • Go binaries don't use libc by default, so ldd says "not a dynamic executable". That's normal, not broken.