execveat系统调用及示例

这次我们介绍 execveat


1. 函数介绍

execveat 是一个 Linux 系统调用(内核版本 >= 3.19),它是 execve 函数族的一员。它的核心功能是在当前进程中执行一个新的程序,从而替换当前进程的镜像(代码、数据、堆栈等)。

简单来说,execveat 就像用一个新灵魂替换旧灵魂

  • 你的身体(进程)还在,但里面的思想、记忆、行为(程序代码和数据)被完全替换成另一个人(新程序)的。
  • 旧程序的所有状态(局部变量、堆栈)都消失了。
  • 新程序从它的 main 函数开始执行。

execveat 相比于 execve 的独特之处在于它引入了目录文件描述符dirfd)和路径解析标志flags),使得程序执行可以相对于一个已打开的目录进行,或者直接执行一个已打开的文件描述符所指向的文件。这提供了更灵活和安全的程序执行方式,尤其是在处理复杂路径或受限环境(如容器)时。


2. 函数原型

#include <unistd.h> // 必需

int execveat(int dirfd, const char *pathname,
             char *const argv[], char *const envp[], int flags);

3. 功能

  • 执行新程序: 终止调用进程的当前程序,并使用由 pathname(结合 dirfd 和 flags)指定的可执行文件来替换当前进程的内存镜像。
  • 传递参数和环境: 将新的命令行参数 (argv) 和环境变量 (envp) 传递给新程序。
  • 灵活的路径解析: 通过 dirfd 和 flags 参数,提供了比 execve 更灵活的路径解析方式。

4. 参数

  • int dirfd: 一个目录文件描述符,用作解析 pathname 的起始点
    • 如果 pathname 是相对路径(例如 "subdir/myprogram"),则相对于 dirfd 指向的目录进行查找。
    • 如果 pathname 是绝对路径(例如 "/usr/bin/ls"),则 dirfd 被忽略。
    • 可以传入特殊的值 AT_FDCWD,表示使用当前工作目录作为起始点(此时行为类似于 execve)。
  • const char *pathname: 指向要执行的可执行文件的路径名。这个路径名会根据 dirfd 和 flags 进行解析。
  • char *const argv[]: 一个字符串数组(向量),用于传递给新程序的命令行参数
    • 数组的每个元素都是一个指向以空字符 (\0) 结尾的字符串的指针。
    • 数组必须以 NULL 指针结束
    • argv[0] 通常是程序的名字(惯例,但不是强制)。
    • 例如:char *argv[] = {"myprogram", "--verbose", "input.txt", NULL};
  • char *const envp[]: 一个字符串数组(向量),用于设置新程序的环境变量
    • 数组的每个元素都是一个形如 "NAME=VALUE" 的字符串。
    • 数组必须以 NULL 指针结束
    • 例如:char *envp[] = {"PATH=/usr/bin:/bin", "HOME=/home/user", NULL};
    • 可以使用全局变量 environ 来传递当前进程的环境。
  • int flags: 控制路径解析行为的标志位。可以是以下值的按位或组合:
    • 0: 默认行为。pathname 被当作普通路径名处理,相对于 dirfd 解析。
    • AT_EMPTY_PATH: 如果 pathname 是一个空字符串 (""),则 execveat 会尝试执行 dirfd 本身所引用的文件。dirfd 必须是一个有效的、指向可执行文件的文件描述符
    • AT_SYMLINK_NOFOLLOW: 如果 pathname 解析过程中遇到符号链接(symbolic link),则不跟随该链接,而是尝试执行符号链接文件本身。注意:并非所有文件系统都支持执行符号链接文件本身,通常会失败。

5. 返回值

  • 成功时永不返回。因为 execveat 成功执行后,当前进程的代码和数据已经被新程序完全替换,从新程序的 main 函数开始执行。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EACCES 权限不足,ENOENT 文件未找到,EINVAL 参数无效,ENOMEM 内存不足,ELIBBAD 动态链接库损坏等)。

重要只有在 execveat 调用失败时,它才会返回 -1。如果成功,控制流就不会回到调用 execveat 的代码处。


6. 相似函数,或关联函数

  • execve: 最基础的 exec 函数,功能与 execveat 相同,但不支持 dirfd 和 flags 参数。
  • execlexeclpexecleexecvexecvpexecve 的各种包装函数,提供了不同的参数传递方式(如使用可变参数列表 ... 或自动搜索 PATH 环境变量)。
  • fork: 通常与 execveat/execve 配合使用。fork 创建子进程,然后在子进程中调用 execveat/execve 来执行新程序。
  • system: 一个更高级的库函数,内部通过 fork + execve (/bin/sh -c command) 来执行 shell 命令。
  • posix_spawn: POSIX 标准定义的、更现代和可移植的创建和执行子进程的方式,功能强大且避免了 fork 的开销(在某些系统上)。

7. 示例代码

示例 1:基本的 execveat 使用

这个例子演示了如何使用 execveat 来执行一个简单的程序(/bin/echo)。

// execveat_basic.c
#define _GNU_SOURCE // For AT_FDCWD
#include <unistd.h>   // execveat
#include <fcntl.h>    // open, O_RDONLY, AT_FDCWD
#include <stdio.h>    // perror, printf
#include <stdlib.h>   // exit
#include <errno.h>    // errno

int main() {
    // 1. 准备 execveat 的参数
    const char *pathname = "/bin/echo";
    char *const argv[] = {"echo", "Hello,", "from", "execveat!", NULL};
    // 使用当前进程的环境变量
    extern char **environ;
    char *const envp[] = {NULL}; // 或者直接传递 environ

    printf("About to execute '/bin/echo' using execveat...\n");

    // 2. 调用 execveat
    // dirfd = AT_FDCWD: 使用当前工作目录解析绝对路径
    // pathname = "/bin/echo": 要执行的程序
    // argv: 传递给新程序的参数
    // environ: 传递给新程序的环境变量
    // flags = 0: 默认行为
    if (execveat(AT_FDCWD, pathname, argv, environ, 0) == -1) {
        // 3. 如果 execveat 返回,说明执行失败
        perror("execveat failed");
        exit(EXIT_FAILURE);
    }

    // 4. 这行代码永远不会被执行,因为 execveat 成功后不会返回
    printf("This line will never be printed if execveat succeeds.\n");

    return 0; // 这行也不会被执行
}

代码解释:

  1. 定义要执行的程序路径 pathname ("/bin/echo")。
  2. 准备传递给新程序的命令行参数 argv 数组。argv[0] 通常是程序名 "echo",后面跟着要传递的参数 "Hello,""from""execveat!"非常重要:数组必须以 NULL 结尾。
  3. 准备环境变量 envp。这里为了简化,传递一个只包含 NULL 的数组,表示不传递任何环境变量。实际应用中通常传递 extern char **environ 来继承当前进程的环境。
  4. 调用 execveat(AT_FDCWD, pathname, argv, environ, 0)
    • AT_FDCWD: 使用当前工作目录作为路径解析起点。
    • pathname: 要执行的程序的绝对路径。
    • argv: 参数向量。
    • environ: 环境变量向量。
    • 0: 默认标志。
  5. 关键: 检查 execveat 的返回值。如果它返回 -1,说明执行失败,打印错误信息并退出。
  6. 如果 execveat 成功,当前进程的代码被 /bin/echo 程序替换,从 echo 的 main 函数开始执行,打印 "Hello, from execveat!",然后 echo 程序退出,整个进程也随之结束。
  7. main 函数中 execveat 之后的代码(包括 return 0;永远不会被执行

示例 2:使用 execveat 和 AT_EMPTY_PATH 执行已打开的文件

这个例子演示了如何使用 AT_EMPTY_PATH 标志来执行一个已经通过文件描述符打开的可执行文件。

// execveat_empty_path.c
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
    const char *binary_path = "/bin/ls"; // 我们将执行 ls 命令
    int binary_fd;
    char *const argv[] = {"ls", "-l", "/tmp", NULL}; // ls -l /tmp
    extern char **environ;

    printf("Opening executable file '%s'...\n", binary_path);

    // 1. 打开可执行文件,获取文件描述符
    binary_fd = open(binary_path, O_RDONLY);
    if (binary_fd == -1) {
        perror("open executable file");
        exit(EXIT_FAILURE);
    }
    printf("Executable file opened successfully. File descriptor: %d\n", binary_fd);

    // 2. 准备 execveat 的参数
    // 注意:pathname 是空字符串 ""
    const char *pathname = "";

    printf("About to execute using execveat with AT_EMPTY_PATH...\n");
    printf("Command will be: ls -l /tmp\n");

    // 3. 调用 execveat
    // dirfd = binary_fd: 指向已打开的可执行文件的文件描述符
    // pathname = "": 空字符串
    // argv: 传递给 ls 的参数
    // environ: 环境变量
    // flags = AT_EMPTY_PATH: 告诉 execveat 执行 dirfd 指向的文件
    if (execveat(binary_fd, pathname, argv, environ, AT_EMPTY_PATH) == -1) {
        // 4. 如果 execveat 返回,说明执行失败
        perror("execveat with AT_EMPTY_PATH failed");
        // 可能的原因:内核版本 < 3.19, binary_fd 无效, 文件不可执行等
        close(binary_fd); // 关闭文件描述符
        exit(EXIT_FAILURE);
    }

    // 5. 这行代码永远不会被执行
    printf("This line will never be printed if execveat succeeds.\n");

    // close(binary_fd); // 这行也不会被执行,因为 execveat 成功后进程已替换
    return 0;
}

代码解释:

  1. 定义要执行的二进制文件路径 binary_path ("/bin/ls")。
  2. 使用 open(binary_path, O_RDONLY) 以只读方式打开该可执行文件,并获得文件描述符 binary_fd
  3. 准备 execveat 的参数:
    • pathname 被设置为空字符串 ""
    • argv 设置为执行 ls -l /tmp 所需的参数。
    • envp 传递 environ
  4. 关键步骤: 调用 execveat(binary_fd, "", argv, environ, AT_EMPTY_PATH)
    • binary_fd: 之前打开的可执行文件的文件描述符。
    • "": 空字符串 pathname
    • AT_EMPTY_PATH: 这个标志告诉 execveat:忽略 pathname,直接执行 dirfd(即 binary_fd)所指向的文件。
  5. 检查返回值。如果成功,当前进程被 /bin/ls 程序替换,并执行 ls -l /tmp 命令。
  6. 如果失败,打印错误信息,关闭 binary_fd 并退出。

示例 3:execveat 与 fork 结合使用

这个例子演示了 execveat 如何与 fork 结合,创建一个子进程并在其中执行新程序。

// execveat_with_fork.c
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/wait.h> // waitpid
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
    pid_t pid;
    int status;
    const char *pathname = "/bin/date"; // 执行 date 命令
    char *const argv[] = {"date", "+%Y-%m-%d %H:%M:%S", NULL};
    extern char **environ;

    printf("Parent process (PID: %d) is about to fork.\n", getpid());

    // 1. 创建子进程
    pid = fork();
    if (pid == -1) {
        // fork 失败
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // --- 子进程 ---
        printf("Child process (PID: %d) created.\n", getpid());

        // 2. 在子进程中调用 execveat 执行新程序
        printf("Child process executing '/bin/date'...\n");

        if (execveat(AT_FDCWD, pathname, argv, environ, 0) == -1) {
            // 3. execveat 失败
            perror("execveat in child failed");
            _exit(EXIT_FAILURE); // 子进程使用 _exit 退出
        }

        // 4. 这行代码在子进程中永远不会被执行
        printf("This line in child will never be printed.\n");

    } else {
        // --- 父进程 ---
        printf("Parent process (PID: %d) forked child (PID: %d).\n", getpid(), pid);

        // 5. 父进程等待子进程结束
        printf("Parent process waiting for child to finish...\n");
        if (waitpid(pid, &status, 0) == -1) {
            perror("waitpid");
            exit(EXIT_FAILURE);
        }

        // 6. 检查子进程退出状态
        if (WIFEXITED(status)) {
            printf("Child process (PID: %d) exited normally with status %d.\n",
                   pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child process (PID: %d) was killed by signal %d.\n",
                   pid, WTERMSIG(status));
        } else {
            printf("Child process (PID: %d) exited abnormally.\n", pid);
        }

        printf("Parent process (PID: %d) finished.\n", getpid());
    }

    return 0;
}

代码解释:

  1. 定义要执行的程序 "/bin/date" 及其参数。
  2. 调用 fork() 创建子进程。
  3. fork 返回后
    • 子进程 (pid == 0):
      • 调用 execveat(AT_FDCWD, pathname, argv, environ, 0) 执行 /bin/date 命令。
      • 如果 execveat 成功,子进程的代码被替换,date 命令开始执行并打印当前日期时间。
      • date 命令执行完毕后,子进程退出。
      • 如果 execveat 失败,打印错误信息并使用 _exit(EXIT_FAILURE) 退出(在子进程中推荐使用 _exit 而非 exit)。
    • 父进程 (pid > 0):
      • 打印子进程 PID。
      • 调用 waitpid(pid, &status, 0) 等待子进程结束。
      • waitpid 返回后,检查子进程的退出状态 status
      • 使用 WIFEXITEDWEXITSTATUSWIFSIGNALEDWTERMSIG 等宏来判断子进程是正常退出还是被信号终止,并获取退出码或信号号。
      • 父进程打印相关信息并退出。

重要提示与注意事项:

  1. 永不返回execveat 成功时永远不会返回到调用者。这是 exec 系列函数的根本特性。
  2. 失败处理必须检查 execveat 的返回值。如果返回 -1,表示执行失败,需要进行错误处理(如打印错误信息、退出等)。
  3. argv 和 envp 必须以 NULL 结尾: 这是 execve 系列函数的严格要求。
  4. AT_EMPTY_PATH: 这是 execveat 相比 execve 的关键新增功能,允许通过文件描述符直接执行文件,这在容器技术和安全沙箱中非常有用。
  5. AT_SYMLINK_NOFOLLOW: 尝试执行符号链接本身,但支持有限。
  6. dirfd 的使用: 提供了更灵活的路径解析能力,尤其是在受限或复杂目录结构中。
  7. 与 fork 结合: 这是创建新进程并执行新程序的经典模式(fork-and-exec 模式)。
  8. 内核版本: 需要 Linux 内核 3.19 或更高版本。
  9. 权限: 执行文件需要有可执行权限 (x)。
  10. environ: 使用 extern char **environ; 可以方便地传递当前进程的环境变量给新程序。

总结:

execveat 是 execve 的强大扩展,通过引入 dirfd 和 flags 参数,提供了更灵活、更安全的程序执行方式。它允许程序基于已打开的目录或文件描述符来定位和执行可执行文件,这在现代 Linux 系统编程,特别是容器和沙箱技术中具有重要意义。理解其参数和与 fork 的结合使用是掌握 Linux 进程控制的基础。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注