Syscall / ELF · Linux userspace / kernel ABI

From typing ./xxx to main() and process exit

Follows the complete sequence after ./xxx is entered in a shell: command interpretation, child creation, execve, ELF loading, the dynamic linker, _start, main, exit_group, and wait.

Series
02 / 38
Build
cc -std=c17 -Wall -Wextra -O2 exec_path.c -o exec_path
Run
./exec_path alpha beta; printf 'status=%d\n' $?
Kernel
Linux 6.18.37 LTS

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 diagram

Figure 1. Where the parent shell and executing child split and meet again
$ ./xxx alpha beta
interactive shellparse · expand · redirect · resolve ./ path
spawn

PID A

parent shell

  • record child PID/PGID
  • hand over foreground terminal
  • block in wait4(child)
blocked in wait

PID B

child before exec

  • fd redirection
  • normalize signal state
  • execve("./xxx", argv, envp)
kernel exec
  • path walk relative to cwd
  • execute permission / LSM
  • ELF or #! handler
  • PT_LOAD · stack · auxv
  • set new IP/SP
new userspace image
  • ld-linux entry
  • dependency · relocation
  • _start
  • __libc_start_main
  • main(argc, argv)
exit_group → wait4consume zombie status · update $? · reclaim terminal · prompt

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

Figure 2. From userspace code to observable results
shell parse finalize ./xxx, argv, and redirections
spawn child fork/clone/vfork/posix_spawn
execve replace the image under the same PID
runtime ld.so → _start → main
shell wait record the exit status in $?

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.

Figure 3. Major points along the kernel-internal path
do_execveat_common copy filename, argv, and envp
do_open_execat pathname and execute permission
search_binary_handler select ELF or #!
load_elf_binary PT_LOAD, stack, auxv
start_thread return to userspace with a new IP and SP

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.

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.

FileFunction / structureWhat 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 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.

Buildcc -std=c17 -Wall -Wextra -O2 exec_path.c -o exec_path
01#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

Source line 9__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.

Source line 14dprintf(STDERR_FILENO

If 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.

Source line 25readlink("/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.

Source line 35for (int i = 0; i < argc

The 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.

Source line 39return 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 $?.

Detailed behavior

01

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 . .

02

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.

03

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.

04

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.

05

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.

06

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.

07

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.

08

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 and lifetimes

ObjectCreation and releaseValues to inspect
shell processPersists for the terminal session and owns child creation, wait, and job controlPID/PGID, cwd, fd table, job table
child taskCreated at spawn, runs a new image under the same PID after exec, and may become a zombie before waitPID, parent, credentials, exit status
linux_binprmExists only while exec is being prepared and is used to select a binary handler and construct the stackfile, buf, argc/envc, interp
new mm_structEstablished while loading ELF and owns the new address space until the next exec or process exitPT_LOAD VMA, stack, brk, mmap base
wait statusRecorded when the child exits and consumed when the parent shell waitsexit code, terminating signal, rusage

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
bash: ./xxx: No such file or directoryxxx itself is absent, or the PT_INTERP/shebang interpreter is absentls -l, file, readelf -l, head -1
Permission deniedexecute bit, directory search permission, noexec mount, LSM denialnamei -l, findmnt -no OPTIONS, audit log
Exec format errorNot a recognized ELF or shebang formatInspect file, readelf -h, and the first bytes with hexdump
A segfault occurs before mainA dynamic relocation, loader, constructor, ABI, or initial-stack problemgdb starti, LD_DEBUG, core dump
The shell status is 126/127By shell convention, 126 means found but not executable, while 127 means command lookup failedUse strace to compare the shell diagnostic with the actual execve errno

Verify it yourself

  1. Run ./exec_path 'two words' '*.c' and inspect which strings reach argv after quote removal.
  2. Create chmod -x exec_path, a noexec tmpfs, and a missing PT_INTERP in turn, and distinguish EACCES from ENOENT.
  3. Inspect INTERP and LOAD in readelf -l ./exec_path and match them to the execve/mmap sequence in strace.
  4. Stop with starti in gdb, then use info proc mappings and x/32gx $rsp to inspect the entry point and initial stack.
  5. Terminate the program through normal return, _exit(7), and SIGSEGV, then compare whether destructors run and what value the shell places in $?.
Run./exec_path alpha beta; printf 'status=%d\n' $?
Tracestrace -f -e trace=clone,clone3,vfork,execve,openat,mmap,mprotect,wait4,exit_group bash -c './exec_path alpha beta; printf "status=%d\n" $?'

Primary sources