The exec() system call is the fundamental mechanism by which a new program begins its life within a running Linux process.

Let’s watch it happen. Imagine we have a simple C program, hello.c:

#include <stdio.h>

int main() {
    printf("Hello, world!\n");
    return 0;
}

We compile it: gcc hello.c -o hello. Now, to see exec() in action, we’ll use strace to trace the system calls made by hello:

strace ./hello

Here’s a snippet of what you’ll see, focusing on the execve() call and what follows:

execve("./hello", ["./hello"], 0x7ffc9a4e1a50 /* 59 vars */) = 0
brk(NULL)                               = 0x55a2f7619000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=123456, ...}) = 0
mmap(NULL, 123456, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f0f4a60b000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\2\0\3\0\1\0\0\0\320\22\1\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2345678, ...}) = 0
mmap(NULL, 2345678, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f0f4a1e8000
...
openat(AT_FDCWD, "/usr/lib/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 3
...
write(1, "Hello, world!\n", 14)          = 14
exit(0)                                 = ?
+++ exited with 0 +++

The execve() system call is the star here. It’s the kernel’s way of saying, "Okay, this process is going to stop being what it was and become this new program." It takes the path to the executable ("./hello"), the arguments (["./hello"]), and the environment variables (represented by a pointer, here 0x7ffc9a4e1a50, and 59 vars indicating the count). If execve() succeeds, it returns 0, but critically, it doesn’t return to the old code. Instead, control is transferred to the entry point of the new program.

Immediately after execve() returns (to the new program’s code, not the caller’s), the program’s runtime environment needs to be set up. For dynamically linked executables like our hello program (which relies on libc.so.6), this involves the dynamic linker/loader.

The first few system calls you see after execve() are typically related to this setup:

  • brk(NULL): This is often the very first syscall a program makes. It’s used to manage the program’s heap. brk(NULL) queries the current break point (end of the data segment).
  • access("/etc/ld.so.preload", R_OK): The dynamic linker checks for a file that might preload shared libraries, which would affect how other libraries are loaded.
  • openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC): It looks for the shared library cache, a pre-built index that speeds up finding library locations.
  • fstat and mmap: These are used to read the library cache into memory.
  • openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC): This is where the dynamic linker finds and opens the C standard library, which is essential for most C programs.
  • Subsequent read, fstat, and mmap calls load libc.so.6 into the process’s address space.

Only after the dynamic linker has done its work and mapped in necessary libraries can the program’s actual main function be executed. The printf("Hello, world!\n") call you see later is a function within libc.so.6, and the write(1, "Hello, world!\n", 14) is the system call that actually puts the characters onto standard output. Finally, exit(0) is called to terminate the process cleanly.

The entire sequence from execve to the first user-level function call (like printf internally calling write) is managed by the dynamic linker. It’s not part of your main function’s code, but it’s a crucial setup phase that happens "under the hood" every time a dynamically linked executable starts.

The surprising part is how much work goes into simply starting a program. The dynamic linker isn’t just finding libc; it’s also resolving symbols for other libraries your program might depend on, performing relocation, and setting up things like the stack and program headers.

The next step after your program has successfully executed its main and printed output is understanding how signals are delivered to a process.

Want structured learning?

Take the full Strace course →