第 27 章 程序执行 (Program Execution)
核心结论
-
execve() 是替换进程映像的系统调用:用一个新程序替换当前进程的代码/数据/堆/栈;PID 不变;成功永返回——失败返回 -1,errno 指示原因(EACCES/ENOENT/ENOEXEC/ETXTBSY/E2BIG)。
-
exec() 库函数族都是 execve() 的封装:按 (pathname vs filename+PATH) × (list vs vector) × (envp vs caller environ) 三维差异展开——execl/execlp/execle/execv/execvp/execve 共 6 个;命名末字母 p/v/l/e 提示用法。
-
脚本通过 ! 行指定解释器:内核在 execve() 中识别
!后调用解释器,传入interpreter-path [optional-arg] script-path arg…;Linux 限制 #! 行 ≤ 127 字符;无 #! 时 execlp/execvp 自动用 /bin/sh 解释。 -
fexecve() 用 fd 而非路径执行:glibc 2.3.2+;避免「检查文件 → 执行」间文件被替换的 TOCTOU 攻击。
-
close-on-exec (FD_CLOEXEC) 控制 fd 是否跨 exec:默认 fd 跨 exec 保留——shell 重定向靠此机制;fcntl(F_SETFD, FD_CLOEXEC) 关闭;dup/dup2/fcntl 复制时清零该标志。
-
exec 复位信号 disposition:被捕获的信号重置为 SIG_DFL;SIG_IGN/SIG_DFL 不变;信号屏蔽字与挂起信号保留;sigaltstack 与 SA_ONSTACK 标志丢失。
|
本章主旨
本章衔接 fork() 之后的故事——如何让子进程「换成」另一个程序运行。核心是 execve() 系统调用及其 6 个库函数封装(命名规则 p/v/l/e 暗示用法),加上脚本执行机制(#!)、文件描述符在 exec 前后的保留规则(shell 重定向 + FD_CLOEXEC)、信号 disposition 的复位规则,以及 system() 的实现原理与信号处理细节。理解 exec 语义是写 shell、daemon、fork-and-exec 服务器、set-UID 程序的关键——也是后续章节「更详细的进程创建」「daemon」的基础。 |
一、核心概念
本章围绕 6 个核心概念展开:从 execve() 系统调用入手,到 exec() 库函数族(命名规则)、脚本解释器机制、fd 与信号在 exec 前后的保留/复位规则,最后到 system() 的实现与安全性。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
execve() 系统调用 |
替换进程映像的 UNIX 系统调用;接受 pathname + argv + envp;成功不返回;PID 保留;set-UID/set-GID 文件改变 effective ID;执行二进制(ELF)或脚本(#!)。 |
§27.1; |
exec() 库函数族 |
execve/execve/execle/execlp/execvp/execv/execl 共 6 函数(加 glibc 非标的 execvpe);命名末字母 p=filename+PATH、v=argv 数组、l=参数列表、e=envp 参数;其余用 caller environ。 |
§27.2;表 27-1;set-UID 程序避免 execlp/execvp(PATH 被劫持);空 PATH 默认 |
脚本解释器 (#!) |
文件首行 |
§27.3;optional-arg 不含空格;awk 用 |
fd 与 exec() 的保留规则 |
默认所有 fd 跨 exec 保留——shell 用此实现重定向;FD_CLOEXEC 标志控制单个 fd 在 exec 时关闭;dup/dup2/fcntl 复制的 fd 清零 FD_CLOEXEC。 |
§27.4;fcntl(F_GETFD/F_SETFD) 操作;图 27-2 展示 shell 重定向 3 步:fork → open+dup2 → exec。 |
信号与 exec() 的复位规则 |
handled 信号 → SIG_DFL;SIG_IGN/SIG_DFL 不变;信号屏蔽字 + 挂起信号保留;sigaltstack + SA_ONSTACK 丢失;SUSv3 对 SIGCHLD 行为未规定(Linux 保留 SIG_IGN,Solaris 复位)。 |
§27.5;SA_ONSTACK 失效;建议 exec 任意程序前显式 unblock+reset。 |
system() 与 fork+exec+wait |
|
§27.6-§27.7;set-UID 程序禁止使用 system()(IFS/BASH_ENV 等环境变量攻击);用 fork+exec 替代。 |
二、详细笔记
27.1 execve() 系统调用
What:execve(pathname, argv, envp) 用新程序替换当前进程映像——丢弃旧代码/数据/堆/栈;成功永不返回,失败返回 -1;PID 保留。
Why:让进程能在保持 PID/打开文件/会话上下文的同时加载完全不同的程序。这是 shell 执行命令、daemon 重读配置、脚本解释器启动的语言运行时共同依赖的机制。
How:
// 摘自《The Linux Programming Interface》第 27 章
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
语义要点:
-
pathname绝对或相对(相对当前工作目录)。 -
argvNULL 结尾的字符串数组;argv[0]通常为 basename。 -
envpNULL 结尾的name=value字符串数组。 -
进程 ID 保留——同一进程继续存在(部分属性变化见第 28 章)。
-
set-UID 位设置时 effective UID → 文件所有者;saved set-UID ← effective UID。
-
成功 → 不返回(
execve之后代码无意义);失败 → 返回 -1,errno 说明原因。
错误码(§27.1):
-
EACCES:非普通文件、无执行权限、目录不可搜索、MS_NOEXEC挂载。 -
ENOENT:文件不存在。 -
ENOEXEC:标记为可执行但格式无法识别(脚本无 #!)。 -
ETXTBSY:文件被另一进程以写方式打开。 -
E2BIG:argv + envp总大小超限。
When:写 fork-and-exec 子进程、shell、daemon 重读配置、set-UID 程序加载受信任工具时调用。注意:成功永不返回——调用后任何代码都不会执行(包括 errExit)。
Example:
// 摘自《The Linux Programming Interface》第 27 章 — Listing 27-1
// 摘自 procexec/t_execve.c
char *argVec[10];
char *envVec[] = { "GREET=salut", "BYE=adieu", NULL };
argVec[0] = strrchr(argv[1], '/'); /* Get basename */
if (argVec[0] != NULL) argVec[0]++;
else argVec[0] = argv[1];
argVec[1] = "hello world";
argVec[2] = "goodbye";
argVec[3] = NULL;
execve(argv[1], argVec, envVec);
errExit("execve"); /* If we get here, exec failed */
27.2 exec() 库函数族
What:execve() 的 6 个库函数封装——按 (pathname vs filename+PATH) × (list vs vector) × (envp vs caller environ) 三个维度差异组合。
Why:根据「知道路径还是只知名字」「参数固定还是动态」「环境是否安全」选择最合适的封装;减少样板代码。
How:命名规则(§27.2 + 表 27-1):
| 函数 | 程序指定 | 参数 | 环境 |
|---|---|---|---|
|
pathname |
array |
envp 参数 |
|
pathname |
list |
envp 参数 |
|
filename + PATH |
list |
caller environ |
|
filename + PATH |
array |
caller environ |
|
pathname |
array |
caller environ |
|
pathname |
list |
caller environ |
PATH 搜索规则(§27.2.1):
-
execlp/execvp用 PATH 找 filename;execl/execv/execle/execve不用。 -
路径含
/→ 视为 pathname,PATH 不用。 -
PATH 未定义 → 默认
.:/usr/bin:/bin。 -
路径前缀可为绝对或相对;相对参照当前工作目录;
.表示当前目录。 -
SUSv3 将空前缀(连续
:、首:、尾:)标记为 obsolete——用.替代。 -
set-UID/set-GID 程序应避免
execlp/execvp——攻击者可通过 PATH 劫持;安全做法是覆盖 PATH 为已知安全目录或直接用execve。
glibc 2.11+ 增加非标 execvpe(file, argv, envp)——execvp + envp 参数。
When:列表参数固定且少 → 用 execl*;参数动态 → 用 execv*;只知程序名 → 用 exec*p;要传干净环境 → 用 exec*e 或 execve。
Example:
// 摘自《The Linux Programming Interface》第 27 章 — Listing 27-4
// 摘自 procexec/t_execle.c
char *envVec[] = { "GREET=salut", "BYE=adieu", NULL };
char *filename = strrchr(argv[1], '/');
if (filename != NULL) filename++;
else filename = argv[1];
execle(argv[1], filename, "hello world", (char *) NULL, envVec);
errExit("execle");
27.3 解释器脚本 (#!)
What:以 #! interpreter-path [optional-arg] 开头的文本文件——内核在 execve() 中识别后调用解释器,将脚本作为输入。
Why:让 sh/awk/perl/python 脚本像二进制一样可执行;不必显式 sh script.sh。
How:execve 看到 #! 后的展开(§27.3):
-
调用:
interpreter-path [optional-arg] script-path arg… -
script-path是给 execve() 的路径;arg…是 argv 排除 argv[0] 后所有元素。 -
图 27-1 展示 argv 来源。
-
#!行限制:Linux ≤ 127 字符;OpenBSD 64;Tru64 1024;SunOS 32。 -
optional-arg不含空格——Linux 把整行余下部分当单词(其他 UNIX 实现不一致)。 -
无
#!→execve/execv/execle/execl失败;execlp/execvp退化用/bin/sh解释。 -
awk用#!/usr/bin/awk -f告知「下一参数是脚本文件」——避免把脚本内容当 awk 命令解析。
When:写可执行脚本、Python 虚拟环境脚本(#!/usr/bin/env python3)、awk/perl/ruby 单文件应用。
Example:
// 摘自《The Linux Programming Interface》第 27 章 — 演示 argv 来源
// 摘自 procexec/t_execve.c + necho.script
$ cat > necho.script
#!/home/mtk/bin/necho some argument
Some junk
$ chmod +x necho.script
$ ./t_execve necho.script
argv[0] = /home/mtk/bin/necho /* 内核生成 */
argv[1] = some argument /* 来自 #! optional-arg */
argv[2] = necho.script /* execve 的 pathname */
argv[3] = hello world /* argv 排除 [0] */
argv[4] = goodbye
27.4 文件描述符与 exec()
What:默认所有 fd 跨 exec 保留;FD_CLOEXEC 标志控制单个 fd 在成功 exec 时自动关闭。
Why:shell 用「保留」实现 >/</<() 重定向;库函数/特权程序用 FD_CLOEXEC 防止文件描述符泄露给未知子进程。
How:
shell 重定向流程(§27.4,图 27-2):
-
fork 子 shell。
-
子 shell 打开目标文件 → 用 dup2 复制到 STDOUT_FILENO/STDIN_FILENO → 关闭原 fd。
-
子 shell exec 目标程序——fd 已就位,程序
printf即写入文件。
FD_CLOEXEC 操作(§27.4):
// 摘自《The Linux Programming Interface》第 27 章 — Listing 27-6
// 摘自 procexec/closeonexec.c
int flags = fcntl(fd, F_GETFD);
if (flags == -1) errExit("fcntl - F_GETFD");
flags |= FD_CLOEXEC;
if (fcntl(fd, F_SETFD, flags) == -1) errExit("fcntl - F_SETFD");
要点:
-
FD_CLOEXEC 是 F_GETFD/F_SETFD 唯一使用的位(值 1)。
-
dup/dup2/fcntl 复制 fd 时清零 FD_CLOEXEC——SUSv3 要求。
-
Linux 还支持非标 ioctl(FIOCLEX/FIONCLEX),但应避免。
-
现代代码推荐用
open(… O_CLOEXEC)——一步设置,避免「open → exec 间 fd 被子进程继承」的窗口。
When:库函数打开文件后必须 O_CLOEXEC 或显式 fcntl;特权程序 exec 未知程序前 close 所有不必要 fd 或用 FD_CLOEXEC 标记。
Example:
// 摘自《The Linux Programming Interface》第 27 章 — shell 重定向代码
fd = open("dir.txt", O_WRONLY | O_CREAT, 0666);
if (fd != STDOUT_FILENO) {
dup2(fd, STDOUT_FILENO);
close(fd);
}
execlp("ls", "ls", (char *) NULL);
27.5 信号与 exec()
What:exec 复位信号 disposition——handled 信号 → SIG_DFL;SIG_IGN/SIG_DFL 不变;信号屏蔽字 + 挂起信号保留;sigaltstack + SA_ONSTACK 丢失。
Why:让 exec 后进程处于「干净的信号基线」——handler 函数指针随旧代码消失,必须重置;保留屏蔽字可让父/子同步信号状态。
How:规则清单(§27.5):
-
Handled 信号 → SIG_DFL:handler 函数地址指向旧代码,必须复位。
-
SIG_IGN + SIG_DFL 信号不变:这些是 disposition 而非 handler;保存它们无副作用。
-
信号屏蔽字保留:父 block 的信号在子中也 block。
-
挂起信号保留:尚未递送的信号在 exec 后仍可递送给新程序。
-
sigaltstack 丢失:备用信号栈随旧栈消失;SA_ONSTACK 自动清除。
-
SIGCHLD 特殊:SUSv3 未规定 exec 是否保留 SIG_IGN;Linux 保留,Solaris 复位;为可移植,exec 任意程序前
signal(SIGCHLD, SIG_DFL)。 -
建议:exec 任意(非自写)程序前显式 unblock 所有信号并 reset disposition——避免继承「奇怪的」信号状态。
When:写 set-UID 程序、通用 fork+exec 框架、daemon 重读配置时遵守这些规则。
Example:
// exec 前显式重置 SIGCHLD 处置(避免 Linux/Solaris 差异)
signal(SIGCHLD, SIG_DFL);
execve(pathname, argv, envp);
27.6 system() 与 27.7 实现 system()
What:system(cmd) = fork + execl("/bin/sh", "sh", "-c", cmd) + waitpid;完整实现需处理 SIGCHLD/SIGINT/SIGQUIT 与 SIG_IGN 边界。
Why:让应用以一行代码执行 shell 命令——包含管道/重定向/通配;节省 fork+exec+wait 样板。
How:返回值语义(§27.6):
-
command == NULL→ 非零表示 shell 可用;零表示不可用。 -
fork 失败 → 返回 -1。
-
shell exec 失败 → 返回 shell
exit(127)的 wait status。 -
成功 → 返回 shell 的 wait status(waitpid 形式;用 W* 宏解析)。
返回值无法区分「shell exec 失败」与「shell exit(127)」。
信号处理(§27.7):
-
父进程:block SIGCHLD(避免主程序 handler 抢走子状态)+ ignore SIGINT/SIGQUIT(用户中断只杀子进程,不杀调用者)。
-
子进程:恢复 SIGINT/SIGQUIT 为 default(让执行的命令能响应 Ctrl-C);unblock SIGCHLD;exec shell。
-
必须 fork 之前 block——否则 fork 后到 block 前的窗口期可能丢信号。
-
waitpid 用 while 循环 + 检查 EINTR——handler 可能中断 wait;SUSv3 要求自动重启。
-
子进程用
_exit(127)而非exit()——避免刷新 stdio 缓冲区(那些是父的副本)。
完整实现见 Listing 27-9。
安全性(§27.6 末):
-
set-UID 程序禁用 system()——IFS/BASH_ENV/PATH 等环境变量是攻击入口。
-
1980s Bourne shell IFS 漏洞:
IFS=a让shar被解释为sh ar;现代 shell 只在 shell 展开时应用 IFS,启动时重置为「space tab newline」。 -
bash 在 set-UID 下调用时 revert 到 real UID/GID。
-
安全替代:
fork + execve(pathname, …, envp)(无 PATH 搜索、无 shell 解析)。
When:写「执行用户命令」的工具、shell-out 调试器、CI runner、容器内脚本入口。
Example:
// 摘自《The Linux Programming Interface》第 27 章 — Listing 27-8 (简化版)
// 摘自 procexec/simple_system.c
int system(char *command) {
int status;
pid_t childPid;
switch (childPid = fork()) {
case -1: return -1;
case 0:
execl("/bin/sh", "sh", "-c", command, (char *) NULL);
_exit(127);
default:
if (waitpid(childPid, &status, 0) == -1) return -1;
return status;
}
}
三、关键图表
|
exec() 函数族对照表
命名规则:末字母 p=path/PATH 搜索,v=argv vector,l=list 参数,e=envp 参数。 |
|
exec 前后进程属性保留/复位对照表
|
四、思维导图
mindmap
root((第 27 章 程序执行))
execve 系统调用
替换进程映像
PID 保留
set UID 改变 euid
ELF 与 解释器
exec 库函数族
6 个封装函数
p PATH 搜索
v argv vector
l 参数列表
e envp 参数
setUID 避免 p
解释器脚本
#! 首行
127 字符限制
optional arg
execlp 无 #! 退化 sh
awk -f 案例
fd 与 exec
默认跨 exec 保留
shell 重定向 3 步
FD_CLOEXEC 标志
fcntl F GETFD SETFD
dup 复制清 CLOEXEC
信号与 exec
handled 复位 DFL
SIGIGN DFL 不变
屏蔽字保留
挂起信号保留
sigaltstack 丢失
SIGCHLD 平台差异
system 函数
fork exec waitpid
block SIGCHLD
ignore SIGINT QUIT
子 reset SIGINT QUIT
exit 127 而非 exit
setUID 禁用 system
IFS 历史漏洞
五、重点与易错点
-
exec 成功永不返回——任何 exec 之后的代码都不会执行;常用
errExit("execve")形式表明「到达这里 = 失败」。 -
6 个 exec() 函数命名规则:末字母 p/PATH 搜索、v/argv vector、l/list 参数、e/envp 参数;
execve是底层系统调用。 -
set-UID 程序禁用 execlp/execvp——PATH 可被攻击者劫持;同样禁用 system()(IFS/BASH_ENV 历史漏洞);安全做法是
fork + execve(pathname, …, envp)。 -
默认 fd 跨 exec 保留——shell 重定向的核心机制;库函数必须
O_CLOEXEC或fcntl(F_SETFD, FD_CLOEXEC)防泄露。 -
dup/dup2/fcntl 复制的 fd 清零 FD_CLOEXEC——SUSv3 要求;这是「close-on-exec 不跟随 dup 复制」的硬规则。
-
handled 信号 → SIG_DFL——handler 函数地址随旧代码消失,必须复位;SIG_IGN/SIG_DFL 不变。
-
信号屏蔽字与挂起信号跨 exec 保留——父 block 的信号在子中也 block;这影响 set-UID 程序的「干净状态」设计。
-
SIGCHLD 在 exec 上的行为:Linux 保留 SIG_IGN,Solaris 复位——为可移植,exec 任意程序前显式
signal(SIGCHLD, SIG_DFL)。 -
sigaltstack + SA_ONSTACK 跨 exec 丢失——备用信号栈随旧栈消失;不要依赖 alternate stack 跨 exec。
-
脚本 #! 行 ≤ 127 字符(Linux)——超出部分被忽略;用绝对路径指定解释器;optional-arg 不含空格。
-
无 #! 时 execlp/execvp 自动用 /bin/sh——这是「找到可执行文件但非二进制且无 #!」的退化路径;其他 exec 函数直接失败 ENOEXEC。
-
awk 用
#!/usr/bin/awk -f——告诉解释器下一参数是脚本文件名,避免把脚本内容当 awk 命令解析。 -
fexecve(fd, argv, envp):用 fd 而非 pathname 执行——避免「check → exec」的 TOCTOU 攻击;glibc 2.3.2+。
-
system() 实现要点:fork 前 block SIGCHLD + ignore SIGINT/SIGQUIT;子进程 reset SIGINT/SIGQUIT 为 default + unblock SIGCHLD;waitpid 循环处理 EINTR;子失败用
_exit(127)而非exit()。 -
system() 返回 shell 的 wait status——用 WIFEXITED/WEXITSTATUS 等宏解析(见第 26 章);无法区分「shell exec 失败」与「shell exit(127)」。
-
exec 不会重置 RLIMIT_CPU 计时——若达到 CPU 时间上限仍会被 SIGXCPU 杀死;也不重置进程时间统计相关字段。
-
exec 释放所有 fcntl 文件锁——POSIX 要求;锁不跨 exec 保留。
-
跨章衔接:第 25 章 fork → 第 26 章 wait → 第 27 章 exec + system;第 28 章更详细的 fork+exec 属性变化;第 38 章 set-UID 程序的 exec 安全性。
-