How Linux Starts Your Program: Inside execve, the Loader, and the Dynamic Linker
From the moment a user types a command in the shell to the final exit of the program, this article walks through Linux’s execution pipeline—including shell command parsing, fork, execve, kernel loading via load_elf_binary, the dynamic linker’s role, and the glibc startup sequence—illustrated with detailed diagrams and code snippets.
When a user types a command in a Linux shell, the shell (itself a regular program with its own main) calls read_command to obtain the input, then invokes execute_command. The execution flow is visualized in a panoramic diagram that marks each code segment, its Git repository, source file, and function name.
The shell creates a child process using fork. In the child, it calls execve to replace the process image with the user program. Meanwhile, the parent shell blocks on waitpid, receiving the child’s exit status once the program finishes.
Inside the kernel, the execve system call eventually reaches load_elf_binary, the core routine for loading ELF binaries. This function maps code and data sections into the new process’s virtual address space, builds the initial stack with arguments, environment variables, and the program name, and prepares the registers for entry.
After setting up the stack, load_elf_binary calls start_thread, which in turn calls start_thread_common. These functions write the entry point address ( elf_entry)—the start address of the dynamic linker ( /lib64/ld-linux-x86-64.so.2)—into regs->ip and the new stack pointer into regs->sp. When control returns to user space, the CPU loads these values into RIP and RSP, transferring execution to the dynamic linker.
The dynamic linker’s _start routine receives the initial stack pointer in RDI, then calls _dl_start. This function eventually invokes _dl_start_final, which locates the real program entry address from the auxiliary vector and jumps to it.
Control now passes to the glibc startup code _start, which extracts argc and argv from the stack and calls __libc_start_main. This function finally invokes the user’s main. When main returns, its return value becomes the process exit code, and exit terminates the process.
After the child exits, the parent shell’s waitpid unblocks, receives the exit status (e.g., 99 in the illustrated example), and the shell loops back to read the next command, completing the execution cycle.
This end‑to‑end walkthrough clarifies every stage—from shell input, through fork and execve, kernel ELF loading, dynamic linking, to the glibc startup that finally calls the user’s main —providing a complete picture of how a program is launched and terminated on Linux.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITPUB
Official ITPUB account sharing technical insights, community news, and exciting events.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
