Linux System Programming · advanced · ~12 min

exec — replacing the process image

Run a different program inside the current process.

Overview

The exec* family of functions replaces the program running inside the current process with a different program.

Here is what stays and what changes:

  • The PID (process ID) stays the same.
  • The code, stack, and heap are wiped and reloaded from a new executable file.

On success, exec does not return. Control passes straight to the main function of the new program.

Why it matters

fork alone only gives you two copies of the same program.

exec is the second half of the story. After forking, the child calls something like exec("/bin/ls") to become the new program.

This pair, fork then exec, is how every shell launches every command.

Core concepts

Variants. The family includes execl, execv, execle, execve, execlp, and execvp. They differ in three ways:

  • how arguments are passed (a list vs. an array)
  • how the environment is passed
  • whether $PATH is searched to find the program

No return on success. If exec returns at all, the call failed. The usual cause is that the executable could not be found.

Inherits file descriptors. The new program inherits any file descriptors the old program had open, except those marked close-on-exec.

Syntax notes

execl("/bin/ls", "ls", "-la", (char *)NULL);
/* If we reach this line, exec failed. */
perror("execl");
_exit(127);

Lesson

What exec does

The exec* family replaces the current process image with a new program.

  • On success, the functions do not return. The new program starts at its main.
  • On failure, they return -1 and set errno.

The common pattern

Fork, then call exec in the child. The parent then waits for the child to finish.

Two useful variants

  • execvp(file, argv) searches $PATH for file. The arguments are passed as an array.
  • execve(path, argv, envp) is the most explicit form. It takes an absolute path and an explicit environment.

Code examples

pid_t pid = fork();
if (pid == 0) {
    char *argv[] = { "ls", "-la", NULL };
    execvp("ls", argv);
    _exit(127);  // only reached if execvp fails
}
int st; waitpid(pid, &st, 0);

Common mistakes

  • Forgetting that exec only returns on failure. Always write the error-handling path right after the call.

Debugging tips

Trace every exec

strace -e trace=execve ./prog shows every exec attempt, along with its arguments.

Reading the error

  • No such file or directory means the first argument (the path) is wrong. Double-check it.
  • Permission denied means the file is not executable. Run chmod +x on it.

Memory safety

After exec succeeds, your current heap and stack vanish.

Any malloc'd buffers that were still in use are leaked by definition.

This is usually fine, because the new program does not need them. Note that AddressSanitizer (ASan) and Valgrind will not flag this path.

Real-world uses

  • Every shell command launch.
  • Every system() call.
  • Docker's entrypoint replacement.
  • Login: getty exec's login, which then exec's bash.

Practice tasks

  1. After fork, have the child exec /bin/ls -l / while the parent waits.
  2. Use execvp to make the program look up the command via $PATH.
  3. Pass environment variables explicitly using execve.

Summary

  • exec swaps the program running inside a process, keeping the same PID.
  • It pairs with fork: fork makes a copy, exec turns that copy into a different program.
  • On success exec never returns; if it returns, it failed.
  • Without exec, fork would only make duplicates. Together they implement the Unix process model.

Practice with these exercises