Follow a Program: from `./server` to the First Instruction
Typing ./server triggers a chain — shell, fork, exec, loader, address space, dynamic linker, stack, heap, registers, scheduler — and the first instruction of your main runs only after every one of those steps has succeeded.
The problem
server sits on disk as a few megabytes of bytes. Pressing Enter after ./server results, some milliseconds later, in a CPU core executing the first instruction of your main. Nothing in between is magic; what exactly has to happen, in what order, and what state exists at each step?Progressive depth
The same mechanism at different altitudes — start where you are.
The shell asks the kernel to run the file. The kernel creates a process with its own memory, loads the program and its libraries into it, and the scheduler puts it on a core. Your main runs.
The whole ladder
Here is the sequence on a Unix-style system. Each step is a place where the launch can fail with a specific error, and each step leaves state behind that the rest of the program’s life depends on. The lessons linked from each rung go one level deeper.
- Shellparses the line, resolves `./server` to a path, calls `fork()` then, in the child, `execve()`↓
- Executablethe kernel reads the file header (ELF on Linux, Mach-O on macOS) and checks the execute permission bit↓
- Program loaderthe kernel’s `execve` tears down the child’s old image and reads the program headers: which segments to map, at what addresses, with what permissions↓
- Create processstrictly the process already exists from `fork`; `exec` replaces its image while keeping its PID, parent and open descriptors↓
- Virtual address spacea fresh page table; nothing physical is allocated yet, only mappings↓
- Load codetext and read-only data are mapped from the file, demand-paged: bytes reach RAM on first touch↓
- Load librariesthe dynamic linker (`ld-linux.so`) maps `libc` and friends and patches symbol references↓
- Create stacka stack region is mapped and `argv`, `envp` and the auxiliary vector are copied onto it↓
- Create heapthe initial program break is set; the heap is empty until the first `malloc`↓
- Set registersinstruction pointer at the entry point (`_start`), stack pointer at the top of the stack↓
- Schedule processthe process is put on a ready queue; it runs when the scheduler picks it↓
- CPU executes`_start` calls the C runtime init, which calls your `main`
The shell: fork, then exec
The shell is an ordinary process. It reads ./server, sees a path (not a builtin like cd), and asks the kernel for a copy of itself with fork(). The kernel creates a second process that is, at that instant, an exact duplicate: same code, same open descriptors, same environment. Copying is lazy — the pages are shared copy-on-write, so a fork of a large shell costs microseconds, not a memory copy (see Copy-on-Write).
In the child, the shell calls execve("./server", argv, envp). This is the step that replaces the shell’s image with the server’s. The PID does not change, the parent does not change, and descriptors 0, 1 and 2 survive — which is exactly why redirections like ./server > log.txt work: the shell opens the file and moves it to descriptor 1 *before* exec, and the server inherits it without ever knowing.
The parent shell then calls wait() and blocks until the child exits, which is why your prompt does not come back until the server stops (or until you append &, in which case the shell skips the wait and the child runs as a background job).
1pid_t pid = fork(); // two processes return from this call2if (pid == 0) { // child3 execve("./server", argv, envp); // only returns on failure4 perror("execve"); _exit(127); // e.g. ENOENT, EACCES, ENOEXEC5}6int status;7waitpid(pid, &status, 0); // parent blocks until the child exits8// WEXITSTATUS(status) is the child's exit codeThe loader: from file bytes to an address space
The kernel reads the first bytes of the file. \x7fELF means an ELF executable; #! means a script and the interpreter on that line is exec’d instead, with the script path as an argument. A file without a recognised header fails with ENOEXEC ("Exec format error"), and a file without the execute bit fails with EACCES before any bytes are looked at.
ELF program headers list segments: a read-execute segment for machine code, a read-only one for constants, a read-write one for initialised globals, and the size of the zero-initialised region (BSS). The kernel does not copy these into RAM; it creates *mappings* in a brand-new page table saying "virtual address 0x401000 corresponds to file offset 0x1000, permission r-x". The first time an instruction at that address executes, the CPU takes a page fault and the kernel reads that page from the file. A 50 MB binary that only runs a small path touches only a small fraction of its pages.
If the binary is dynamically linked — which is the default — the program header names an interpreter, typically /lib64/ld-linux-x86-64.so.2. The kernel maps that too and sets the entry point to the interpreter, not to your program. The dynamic linker then runs in user space: it reads the list of needed libraries (libc.so.6, libssl.so.3 …), maps each one, resolves symbols, and fills in the jump tables (the GOT/PLT) so that a call to printf lands in libc. Only then does it jump to your program’s _start. A missing library fails here, after exec succeeded, with "error while loading shared libraries".
Stack, heap, registers, and the first instruction
Before handing control to user space the kernel maps a stack region near the top of the address space and writes onto it the things main will need: argc, the argv strings, the environment strings, and the auxiliary vector (page size, the address of the vDSO, a random canary seed). It records the initial program break — the end of the data segment — which is where the heap will grow from once malloc first calls brk. Both the stack and the heap are nearly empty at this point; they are reservations of virtual addresses, not RAM.
Finally the kernel sets the new process’s saved register state: instruction pointer to the entry point, stack pointer to the top of that stack, everything else zeroed. The process is marked runnable and placed on a ready queue. It does not run yet. When the scheduler picks it (typically within a millisecond on an idle machine, but under load it could wait a full time slice), the kernel loads those registers and returns to user mode, and the CPU executes the first instruction at _start.
_start is not your code: it is the C runtime’s startup stub, which sets up stdio, runs static constructors, and calls main(argc, argv, envp). When main returns, the same stub calls exit(), which flushes buffers, runs atexit handlers and makes the exit_group system call. The kernel tears the address space down, closes descriptors, and keeps only the exit status for the parent to collect — the zombie state described in Process States.
Windows: one call, not two
Windows has no fork. CreateProcess(path, cmdline, …) does the whole thing in one system call family: it opens the executable (PE format, not ELF), creates a new process object and a new address space, maps the image, creates the initial thread, and starts it. Handle inheritance is opt-in per handle rather than everything-by-default, and the child receives the command line as one string that the C runtime later splits into argv.
The consequences show up in portability layers. Python’s multiprocessing defaults to fork on Linux (cheap, inherits everything) but must use spawn on Windows (start a fresh interpreter, re-import the module, pickle the arguments) — which is why a script that "works on Linux" can fail with pickling errors on Windows. Node’s child_process.fork() is a spawn with an IPC channel on every platform and has nothing to do with the Unix fork.
Key points
- Unix-style: the shell forks a copy of itself, the child execs the new program; the PID and open descriptors survive exec, which is what makes redirection work.
- The loader creates mappings, not copies: code pages reach RAM on first touch via page faults.
- A dynamically linked program’s first user-space instructions belong to the dynamic linker, which maps libraries and resolves symbols before your
_startruns. - Stack and heap are nearly empty reservations at start; the kernel writes argv, envp and the auxiliary vector onto the initial stack.
- The new process is placed on a ready queue with its instruction pointer at the entry point; it runs when the scheduler chooses.
- Windows uses
CreateProcess— one call, no fork — which is whymultiprocessingand process-inheritance behaviour differ across platforms.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why fork and then exec, instead of one "start program" call?
Because the gap between them is where the shell customises the child — redirect descriptors, change directory, set environment, drop privileges — using ordinary code, without a giant options struct. Windows chose the giant options struct instead.
▸Why map the executable instead of reading it into memory?
Mapping is lazy and shared: only touched pages are read, and every process running the same binary shares the same physical pages for its code.
▸Why does a dynamic linker exist?
So that one copy of libc on disk and in RAM serves every process, and a security fix to libc does not require relinking every program on the machine.
▸Why doesn’t the process run immediately after exec?
Because the CPU may be busy; making the process runnable and letting the scheduler decide is what keeps the system fair and the launch cost bounded.
Follow ./server to the CPU
- ↓
- ↓
- ↓
- ↓
- ↓
- ↓
- ↓
- ↓
- ↓
- ↓
- ↓
How it fails
What the failure looks like from inside real software.
./serverprintsPermission deniedalthough you own the file: the execute bit is missing (chmod +x), or the file system is mountednoexec.- A binary copied from another machine fails with "error while loading shared libraries: libssl.so.1.1": exec succeeded, the dynamic linker failed.
lddshows the missing name. - A container image built from
scratchcannot run a dynamically linked binary at all: there is nold-linux.soand no libc inside it; the symptom is a confusing "no such file or directory" for a file that exists. - A process starts, and
psshows it, but nothing happens for seconds: it is stuck resolving a hostname or opening a device in its constructors, i.e. blocked beforemain. - A script with Windows line endings fails with
bad interpreter: /bin/bash^M: the shebang line is parsed byte-for-byte by the kernel.
Follow it through every layer
This lesson is one node of a longer journey. Zoom out, then zoom back in.