QUESTION
When ./xxx is entered in a terminal, which process executes what?
Because ./xxx contains a slash, the shell does not search PATH. It resolves the pathname relative to the current working directory, removes quotes, expands variables, prepares redirections, and creates a process to run the external command. Usually the shell remains the parent and waits for the foreground job while the child calls execve, but depending on the shell and execution context it may use posix_spawn, vfork, a clone-family call, or a last-command exec optimization.
When execve succeeds, the child PID remains the same but the old userspace image never returns. The kernel opens the executable and selects a binary-format handler. For ELF it creates the PT_LOAD mappings and initial stack; when PT_INTERP is present, it returns to the dynamic linker's entry point. After the dynamic linker completes relocations and constructors, it reaches main through _start and __libc_start_main. When main returns and exit_group terminates the process, the parent shell converts the wait result into $?, a signal indication, and job state, then prints the prompt again.
STRUCTURE
Structure diagram
PID A
parent shell
- record child PID/PGID
- hand over foreground terminal
- block in wait4(child)
PID B
child before exec
- fd redirection
- normalize signal state
- execve("./xxx", argv, envp)
- path walk relative to cwd
- execute permission / LSM
- ELF or #! handler
- PT_LOAD · stack · auxv
- set new IP/SP
- ld-linux entry
- dependency · relocation
- _start
- __libc_start_main
- main(argc, argv)
A vertical line is continued execution under the same PID; the left/right split means the parent shell and child coexist. After successful execve, the child PID remains but the old userspace image disappears.
CALL PATH
Call path
Thinking of the parent shell and the executing child as one process confuses wait with exec. exec does not create a child; it replaces the userspace image of an execution context that already exists.
This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.
SOURCE COORDINATES
Linux 6.18.37 LTS source locations
Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.
| File | Function / structure | What to inspect |
|---|---|---|
| fs/exec.c | do_execveat_common(), bprm_execve() | The common path that collects filename, argv, and envp in linux_binprm and invokes a binary handler |
| fs/namei.c | do_open_execat(), path_openat() | The path that finds ./xxx from the current working directory and checks mount and permission conditions |
| fs/binfmt_elf.c | load_elf_binary(), create_elf_tables() | The path that creates ELF segments and the initial stack containing PT_INTERP, argc/argv/envp, and auxv |
| fs/binfmt_script.c | load_script() | The path that reconstructs a script whose first two bytes are #! as an interpreter execution |
| kernel/exit.c | do_exit(), do_wait() | Terminating the executing process and collecting its exit status in the parent shell |
COMPLETE PROGRAM
Complete runnable example
The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.
cc -std=c17 -Wall -Wextra -O2 exec_path.c -o exec_path01#define _GNU_SOURCE
02#include <limits.h>
03#include <stdio.h>
04#include <stdlib.h>
05#include <unistd.h>
06
07extern char **environ;
08
09static void before_main(void) __attribute__((constructor));
10static void after_main(void) __attribute__((destructor));
11
12static void before_main(void)
13{
14 dprintf(STDERR_FILENO, "constructor: pid=%ld\n", (long)getpid());
15}
16
17static void after_main(void)
18{
19 dprintf(STDERR_FILENO, "destructor: normal exit path\n");
20}
21
22int main(int argc, char **argv)
23{
24 char executable[PATH_MAX];
25 ssize_t length = readlink("/proc/self/exe", executable,
26 sizeof(executable) - 1);
27 if (length < 0) {
28 perror("readlink");
29 return 1;
30 }
31 executable[length] = '\0';
32
33 printf("pid=%ld ppid=%ld exe=%s\n",
34 (long)getpid(), (long)getppid(), executable);
35 for (int i = 0; i < argc; ++i)
36 printf("argv[%d]=%s\n", i, argv[i]);
37 printf("first environment entry: %s\n",
38 environ[0] != NULL ? environ[0] : "(empty)");
39 return 7;
40}
CODE NOTES
Code notes
__attribute__((constructor))This function runs before main, but it is not the ELF entry point. The dynamic linker and libc startup call it while walking .init_array after relocations are complete.
dprintf(STDERR_FILENOIf stderr fd 2 is not closed by close-on-exec, it is inherited from the shell by the child. If redirection was used, the shell changes the object referred to by fd 2 before exec.
readlink("/proc/self/exe"argv[0] is a string chosen by the caller, so it is not proof of the actual executable path. The /proc/self/exe symlink refers to the executable file attached to the current mm.
for (int i = 0; i < argcThe result of the shell's quoting and expansion is passed in the argv array. Quote characters have been removed, and a glob has already expanded into multiple arguments in the shell.
return 7;The return value from main enters libc's exit path, which runs destructors and flushes stdio before it becomes the exit_group status. The parent shell extracts it from the wait status and records 7 in $?.
DETAILS
Detailed behavior
1. The shell turns the command line into an execution plan
The shell first performs tokenization, quote processing, parameter expansion, command substitution, and pathname expansion. Consequently, the argv received by the kernel contains no shell quote characters, and wildcards have already become a list of filenames.
When a command name contains no slash, builtins, functions, aliases, and PATH lookup are involved. Because ./xxx contains a slash, it uses a pathname relative to the current working directory without consulting the PATH cache or traversing PATH directories. This is why xxx in the current directory will not run without ./ when PATH does not contain . .
2. Redirections and process relationships are prepared first
For a foreground external command, the shell usually creates a child while the parent retains job-control information. For a pipeline it places multiple children in one process group and also changes the terminal's foreground PGID.
Before exec, the child performs setup such as dup2, close, setpgid, and resetting signal dispositions. As a result, the stdout and stderr of ./xxx >out 2>&1 already refer to new open file descriptions before the program begins. The implementation is not fixed to a single fork; posix_spawn and vfork-family optimizations can implement the same semantics.
3. Code that could return disappears at the execve boundary
execve passes a pathname, an argv pointer array, and an envp pointer array through the syscall ABI. The kernel copies the strings referenced by the user pointers into size-limited kernel memory and opens the executable file. On success, the old stack and heap pointers are no longer valid, and the C statement after execve is never executed.
A failure normally returns -1 and errno while the old image can still be retained. If a fatal error occurs after the loader passes the point of no return, however, it cannot restore the old image and the process may terminate with a signal.
4. Distinguish pathname, permission, and binary format
Search permission on the current directory, the file's execute bits, a noexec mount, and LSM policy can each prevent execution. Merely opening a regular file does not make it executable.
The kernel reads the beginning of linux_binprm and walks the registered binary handlers. ELF magic selects binfmt_elf, while #! selects binfmt_script. A script assembles a new argv from the shebang interpreter pathname, optional argument, and script pathname, then repeats exec processing for the interpreter.
5. The ELF loader creates mappings and initial registers
load_elf_binary validates the architecture, ELF class, and program-header bounds, then creates VMAs for PT_LOAD. The part where p_memsz exceeds p_filesz is zero-filled to form bss. A PIE receives a load bias and is placed at an ASLR-capable location.
The initial stack contains not only argc, argv, envp strings, and pointer arrays, but also auxiliary-vector entries such as AT_PHDR, AT_ENTRY, AT_RANDOM, AT_EXECFN, and AT_SYSINFO_EHDR. start_thread records the new instruction pointer and stack pointer in the architecture register state.
6. The first userspace PC of a dynamic ELF may be in ld.so
When PT_INTERP is present, the kernel maps both the main executable and the interpreter, then returns to userspace at the interpreter entry. The original entry of the main executable is passed in AT_ENTRY.
ld-linux maps DT_NEEDED dependencies and processes relocations, TLS, RELRO, and constructors in order. It then transfers control to the main executable's _start. A static executable has no PT_INTERP and skips this dynamic-linker stage.
7. _start completes the C execution environment
_start is not a compiler-generated prologue for main; it is a symbol supplied by a crt startup object. It extracts argc and argv from the initial stack, aligns the stack, and passes the address of main to a __libc_start_main-family entry point.
libc prepares thread-local storage, the stack canary, constructors, and the state required by stdio, then calls main as an ordinary C ABI function. The getpid value observed by the program is the same as the pre-exec child, but the address space's code, data, and stack belong to the new image.
8. Execution continues from main's return to the shell prompt
When main returns, libc's exit calls atexit handlers and .fini_array destructors and flushes stdio buffers. Finally, the exit_group syscall terminates the thread group. The _exit and fatal-signal paths omit some of this userspace cleanup.
The kernel leaves the task in EXIT_ZOMBIE and notifies the parent with SIGCHLD or a wait-queue wakeup. From the waitpid/wait4 result, the shell distinguishes normal exit, signal termination, stop, and continue; it then reclaims the terminal foreground, updates $? and the job table, and prints the next prompt.
OBJECTS
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
shell process | Persists for the terminal session and owns child creation, wait, and job control | PID/PGID, cwd, fd table, job table |
child task | Created at spawn, runs a new image under the same PID after exec, and may become a zombie before wait | PID, parent, credentials, exit status |
linux_binprm | Exists only while exec is being prepared and is used to select a binary handler and construct the stack | file, buf, argc/envc, interp |
new mm_struct | Established while loading ELF and owns the new address space until the next exec or process exit | PT_LOAD VMA, stack, brk, mmap base |
wait status | Recorded when the child exits and consumed when the parent shell waits | exit code, terminating signal, rusage |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| bash: ./xxx: No such file or directory | xxx itself is absent, or the PT_INTERP/shebang interpreter is absent | ls -l, file, readelf -l, head -1 |
| Permission denied | execute bit, directory search permission, noexec mount, LSM denial | namei -l, findmnt -no OPTIONS, audit log |
| Exec format error | Not a recognized ELF or shebang format | Inspect file, readelf -h, and the first bytes with hexdump |
| A segfault occurs before main | A dynamic relocation, loader, constructor, ABI, or initial-stack problem | gdb starti, LD_DEBUG, core dump |
| The shell status is 126/127 | By shell convention, 126 means found but not executable, while 127 means command lookup failed | Use strace to compare the shell diagnostic with the actual execve errno |
LAB
Verify it yourself
- Run ./exec_path 'two words' '*.c' and inspect which strings reach argv after quote removal.
- Create chmod -x exec_path, a noexec tmpfs, and a missing PT_INTERP in turn, and distinguish EACCES from ENOENT.
- Inspect INTERP and LOAD in readelf -l ./exec_path and match them to the execve/mmap sequence in strace.
- Stop with starti in gdb, then use info proc mappings and x/32gx $rsp to inspect the entry point and initial stack.
- Terminate the program through normal return, _exit(7), and SIGSEGV, then compare whether destructors run and what value the shell places in $?.
./exec_path alpha beta; printf 'status=%d\n' $?strace -f -e trace=clone,clone3,vfork,execve,openat,mmap,mprotect,wait4,exit_group bash -c './exec_path alpha beta; printf "status=%d\n" $?'PRIMARY REFERENCES