Replacing the program, keeping the process
◈ 11 cardsUse the exec family correctly, state exactly what survives an exec and what does not, and compile and debug the result with gcc, make and gdb.
exec does not create anything
fork() makes a process. exec makes it run a different program. It allocates nothing, starts nothing, and returns nothing on success — it overwrites the text, data, heap and stack of the calling process with a new executable image and jumps to its entry point.
The consequence catches everyone once: on success, control never comes back. The line after an exec is reached only if the exec failed. So the correct shape is always exec-then-handle-failure:
execlp("date", "date", (char *)0);
perror("execlp"); /* reached ONLY on failure */
_exit(127);
Worked example — fork, exec, wait
The combination is the whole of how a shell runs a command. Fork a child, have the child become the program, have the parent wait:
pid_t pid = fork();
if (pid == -1) { perror("fork"); return 1; }
if (pid == 0) {
execl("/bin/date", "date", (char *)0);
perror("execl");
_exit(127);
}
printf("child is %d\n", (int)pid);
int status;
wait(&status);
printf("child %d finished\n", (int)pid);
Run it and the date appears between the two printfs. The detail worth stopping on: the PID printed before and after is the same. The child that ran your code and the date that printed the line are the same process. exec replaced the program; the process identity survived intact. ps would have shown one PID whose command name changed.
Note the argument list too. execl("/bin/date", "date", (char *)0) passes the path and separately the name the program will see as argv[0]. They are two different things: the kernel uses the first to find the file, and the program sees the second. The list is terminated by a null pointer, cast — a bare 0 or NULL is not portably a pointer in a variadic call.
Six spellings, three questions
The family looks bigger than it is. Every member answers the same three questions, and the letters in the name tell you the answers:
l— the arguments are a list in the call, terminated by(char *)0.v— the arguments are a vector: a null-terminatedchar *argv[]you built.e— you supply the environment explicitly as a second array. Without it the new image inheritsenviron.p— the file is searched for alongPATH, so you can pass"sort"instead of"/usr/bin/sort".
That gives execl, execv, execle, execve, execlp, execvp. Only execve is the actual system call; the rest are library wrappers that marshal their arguments and call it. Use v when the argument count is decided at run time, l when you are writing the arguments out literally, and the p forms whenever you want PATH behaviour — which is most of the time, and which the textbook omits entirely.
What survives, and the one asymmetry that is always examined
Across a successful exec, these survive, because they belong to the process rather than to the program: the PID, PPID and process-group ID; open file descriptors (unless FD_CLOEXEC was set on them); the current working directory and root directory; the controlling terminal; the umask; resource limits and accumulated usage; pending alarms; and the signal mask.
These are destroyed, because they belong to the program: the text, the initialised and uninitialised data, the heap, and the stack. Every variable you had is gone.
That descriptor row is not a curiosity — it is the mechanism the shell uses. The shell forks, the child rearranges descriptors with dup2 to set up > and |, and then execs. The redirection survives because descriptors survive.
And then the asymmetry:
- A signal set to
SIG_IGNstays ignored after the exec. - A signal set to a handler reverts to its default action.
The reason is one sentence: the handler was code, and that code no longer exists. The new image has different text at that address, so the kernel cannot keep pointing at it and resets the disposition to the default. Ignoring, by contrast, needs no code at all — it is a flag — so it survives. Signals set to SIG_DFL were already at the default and stay there.
Building and debugging what you wrote
gcc -Wall -Wextra -g -o prog prog.c
-Wall -Wextra turns on the warnings, and in this course most of them are real bugs — an unchecked return, a %d given a long, a missing *. -g emits the symbol table, and without it a debugger can show you addresses but not your source.
Libraries are named by stripping lib and the extension: libm.a is linked with -lm, libpthread with -lpthread. Miss it and the compiler is happy — the prototype was in <math.h> — and the linker fails with undefined reference to pow. Library arguments go at the end of the command line, after the objects that need them.
When it crashes:
$ gdb -q ./prog
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x... in copy (dst=0x0, src=0x4006f4) at prog.c:14
(gdb) backtrace
#0 copy (dst=0x0, ...) at prog.c:14
#1 main (argc=1, argv=...) at prog.c:27
(gdb) print dst
$1 = 0x0
backtrace (or where) prints the call stack: frame #0 is where the fault happened and #1 is its caller, so you read downwards to find out how you got there. From there, break prog.c:14 and run again to stop before the fault, next to step over a line, step to go into a call, print expr to inspect anything in scope, and info locals for all of it at once. A null pointer in a frame argument, as above, is usually the answer before you have typed a second command.
For more than a couple of source files, put the rules in a makefile. One rule is a target, its dependencies, and the commands to rebuild it — and the command line must begin with a literal TAB character. Spaces there produce missing separator, which is the least helpful error message in the toolchain and the single most common make mistake.