第 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;<unistd.h>;errno: EACCES/ENOENT/ENOEXEC/ETXTBSY/E2BIG;脚本执行靠 #! 行 + ELF PT_INTERP 解释器。

      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 默认 .:/usr/bin:/bin

      脚本解释器 (#!)

      文件首行 ! interpreter [arg] 指定解释器;execve 看到 ! 后调用解释器,参数为 interpreter [arg] script-path arg…​;Linux 限制 #! 行 ≤ 127 字符;无 #! 时 execlp/execvp 退化用 /bin/sh。

      §27.3;optional-arg 不含空格;awk 用 #!/usr/bin/awk -f 让解释器读脚本文件;图 27-1 展示 argv 来源。

      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

      system(cmd) = fork + execl("/bin/sh", "-c", cmd) + waitpid;父进程 block SIGCHLD + ignore SIGINT/SIGQUIT;子进程 reset 信号 disposition → exec;返回 shell 的 wait status。

      §27.6-§27.7;set-UID 程序禁止使用 system()(IFS/BASH_ENV 等环境变量攻击);用 fork+exec 替代。

      二、详细笔记

      27.1 execve() 系统调用

      Whatexecve(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 绝对或相对(相对当前工作目录)。

      • argv NULL 结尾的字符串数组;argv[0] 通常为 basename。

      • envp NULL 结尾的 name=value 字符串数组。

      • 进程 ID 保留——同一进程继续存在(部分属性变化见第 28 章)。

      • set-UID 位设置时 effective UID → 文件所有者;saved set-UID ← effective UID。

      • 成功 → 不返回(execve 之后代码无意义);失败 → 返回 -1,errno 说明原因。

      错误码(§27.1):

      • EACCES:非普通文件、无执行权限、目录不可搜索、MS_NOEXEC 挂载。

      • ENOENT:文件不存在。

      • ENOEXEC:标记为可执行但格式无法识别(脚本无 #!)。

      • ETXTBSY:文件被另一进程以写方式打开。

      • E2BIGargv + 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() 库函数族

      Whatexecve() 的 6 个库函数封装——按 (pathname vs filename+PATH) × (list vs vector) × (envp vs caller environ) 三个维度差异组合。

      Why:根据「知道路径还是只知名字」「参数固定还是动态」「环境是否安全」选择最合适的封装;减少样板代码。

      How:命名规则(§27.2 + 表 27-1):

      函数 程序指定 参数 环境

      execve()

      pathname

      array

      envp 参数

      execle()

      pathname

      list

      envp 参数

      execlp()

      filename + PATH

      list

      caller environ

      execvp()

      filename + PATH

      array

      caller environ

      execv()

      pathname

      array

      caller environ

      execl()

      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*eexecve

      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):

      1. fork 子 shell。

      2. 子 shell 打开目标文件 → 用 dup2 复制到 STDOUT_FILENO/STDIN_FILENO → 关闭原 fd。

      3. 子 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):

      1. Handled 信号 → SIG_DFL:handler 函数地址指向旧代码,必须复位。

      2. SIG_IGN + SIG_DFL 信号不变:这些是 disposition 而非 handler;保存它们无副作用。

      3. 信号屏蔽字保留:父 block 的信号在子中也 block。

      4. 挂起信号保留:尚未递送的信号在 exec 后仍可递送给新程序。

      5. sigaltstack 丢失:备用信号栈随旧栈消失;SA_ONSTACK 自动清除。

      6. SIGCHLD 特殊:SUSv3 未规定 exec 是否保留 SIG_IGN;Linux 保留,Solaris 复位;为可移植,exec 任意程序前 signal(SIGCHLD, SIG_DFL)

      7. 建议: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()

      Whatsystem(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=ashar 被解释为 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() 函数族对照表
      函数 程序指定 参数 环境 备注

      execve()

      pathname

      array

      envp

      原始系统调用

      execle()

      pathname

      list

      envp

      列参数 + 自定义环境

      execlp()

      filename + PATH

      list

      caller environ

      PATH 搜索

      execvp()

      filename + PATH

      array

      caller environ

      PATH 搜索

      execv()

      pathname

      array

      caller environ

      同 execve 但继承环境

      execl()

      pathname

      list

      caller environ

      列参数 + 继承环境

      命名规则:末字母 p=path/PATH 搜索,v=argv vector,l=list 参数,e=envp 参数。

      exec 前后进程属性保留/复位对照表
      属性 exec 行为

      PID、PPID、PGID、SID

      保留

      进程优先级 nice 值

      保留

      工作目录、根目录

      保留

      umask

      保留

      文件描述符

      默认保留;FD_CLOEXEC 关闭

      打开文件偏移量、状态标志

      保留

      信号屏蔽字

      保留

      挂起信号

      保留

      Handled 信号 disposition

      → SIG_DFL

      SIG_IGN / SIG_DFL 信号

      保留

      sigaltstack + SA_ONSTACK

      丢失

      环境变量

      execl/execv/exec*p 继承;exec*e/execve 用 envp

      进程时间(user/sys CPU)

      清零

      文件锁

      释放(fcntl locks)

      RLIMIT_CPU 计时

      不重置

      四、思维导图

      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 历史漏洞

      五、重点与易错点

      1. exec 成功永不返回——任何 exec 之后的代码都不会执行;常用 errExit("execve") 形式表明「到达这里 = 失败」。

      2. 6 个 exec() 函数命名规则:末字母 p/PATH 搜索、v/argv vector、l/list 参数、e/envp 参数;execve 是底层系统调用。

      3. set-UID 程序禁用 execlp/execvp——PATH 可被攻击者劫持;同样禁用 system()(IFS/BASH_ENV 历史漏洞);安全做法是 fork + execve(pathname, …​, envp)

      4. 默认 fd 跨 exec 保留——shell 重定向的核心机制;库函数必须 O_CLOEXECfcntl(F_SETFD, FD_CLOEXEC) 防泄露。

      5. dup/dup2/fcntl 复制的 fd 清零 FD_CLOEXEC——SUSv3 要求;这是「close-on-exec 不跟随 dup 复制」的硬规则。

      6. handled 信号 → SIG_DFL——handler 函数地址随旧代码消失,必须复位;SIG_IGN/SIG_DFL 不变。

      7. 信号屏蔽字与挂起信号跨 exec 保留——父 block 的信号在子中也 block;这影响 set-UID 程序的「干净状态」设计。

      8. SIGCHLD 在 exec 上的行为:Linux 保留 SIG_IGN,Solaris 复位——为可移植,exec 任意程序前显式 signal(SIGCHLD, SIG_DFL)

      9. sigaltstack + SA_ONSTACK 跨 exec 丢失——备用信号栈随旧栈消失;不要依赖 alternate stack 跨 exec。

      10. 脚本 #! 行 ≤ 127 字符(Linux)——超出部分被忽略;用绝对路径指定解释器;optional-arg 不含空格。

      11. 无 #! 时 execlp/execvp 自动用 /bin/sh——这是「找到可执行文件但非二进制且无 #!」的退化路径;其他 exec 函数直接失败 ENOEXEC。

      12. awk 用 #!/usr/bin/awk -f——告诉解释器下一参数是脚本文件名,避免把脚本内容当 awk 命令解析。

      13. fexecve(fd, argv, envp):用 fd 而非 pathname 执行——避免「check → exec」的 TOCTOU 攻击;glibc 2.3.2+。

      14. system() 实现要点:fork 前 block SIGCHLD + ignore SIGINT/SIGQUIT;子进程 reset SIGINT/SIGQUIT 为 default + unblock SIGCHLD;waitpid 循环处理 EINTR;子失败用 _exit(127) 而非 exit()

      15. system() 返回 shell 的 wait status——用 WIFEXITED/WEXITSTATUS 等宏解析(见第 26 章);无法区分「shell exec 失败」与「shell exit(127)」。

      16. exec 不会重置 RLIMIT_CPU 计时——若达到 CPU 时间上限仍会被 SIGXCPU 杀死;也不重置进程时间统计相关字段。

      17. exec 释放所有 fcntl 文件锁——POSIX 要求;锁不跨 exec 保留。

        • 跨章衔接:第 25 章 fork → 第 26 章 wait → 第 27 章 exec + system;第 28 章更详细的 fork+exec 属性变化;第 38 章 set-UID 程序的 exec 安全性。