这次我们介绍 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
参数。execl
,execlp
,execle
,execv
,execvp
:execve
的各种包装函数,提供了不同的参数传递方式(如使用可变参数列表...
或自动搜索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; // 这行也不会被执行
}
代码解释:
- 定义要执行的程序路径
pathname
("/bin/echo"
)。 - 准备传递给新程序的命令行参数
argv
数组。argv[0]
通常是程序名"echo"
,后面跟着要传递的参数"Hello,"
,"from"
,"execveat!"
。非常重要:数组必须以NULL
结尾。 - 准备环境变量
envp
。这里为了简化,传递一个只包含NULL
的数组,表示不传递任何环境变量。实际应用中通常传递extern char **environ
来继承当前进程的环境。 - 调用
execveat(AT_FDCWD, pathname, argv, environ, 0)
。AT_FDCWD
: 使用当前工作目录作为路径解析起点。pathname
: 要执行的程序的绝对路径。argv
: 参数向量。environ
: 环境变量向量。0
: 默认标志。
- 关键: 检查
execveat
的返回值。如果它返回 -1,说明执行失败,打印错误信息并退出。 - 如果
execveat
成功,当前进程的代码被/bin/echo
程序替换,从echo
的main
函数开始执行,打印"Hello, from execveat!"
,然后echo
程序退出,整个进程也随之结束。 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;
}
代码解释:
- 定义要执行的二进制文件路径
binary_path
("/bin/ls"
)。 - 使用
open(binary_path, O_RDONLY)
以只读方式打开该可执行文件,并获得文件描述符binary_fd
。 - 准备
execveat
的参数:pathname
被设置为空字符串""
。argv
设置为执行ls -l /tmp
所需的参数。envp
传递environ
。
- 关键步骤: 调用
execveat(binary_fd, "", argv, environ, AT_EMPTY_PATH)
。binary_fd
: 之前打开的可执行文件的文件描述符。""
: 空字符串pathname
。AT_EMPTY_PATH
: 这个标志告诉execveat
:忽略pathname
,直接执行dirfd
(即binary_fd
)所指向的文件。
- 检查返回值。如果成功,当前进程被
/bin/ls
程序替换,并执行ls -l /tmp
命令。 - 如果失败,打印错误信息,关闭
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;
}
代码解释:
- 定义要执行的程序
"/bin/date"
及其参数。 - 调用
fork()
创建子进程。 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
。- 使用
WIFEXITED
,WEXITSTATUS
,WIFSIGNALED
,WTERMSIG
等宏来判断子进程是正常退出还是被信号终止,并获取退出码或信号号。 - 父进程打印相关信息并退出。
- 在子进程 (
重要提示与注意事项:
- 永不返回:
execveat
成功时永远不会返回到调用者。这是exec
系列函数的根本特性。 - 失败处理: 必须检查
execveat
的返回值。如果返回 -1,表示执行失败,需要进行错误处理(如打印错误信息、退出等)。 argv
和envp
必须以NULL
结尾: 这是execve
系列函数的严格要求。AT_EMPTY_PATH
: 这是execveat
相比execve
的关键新增功能,允许通过文件描述符直接执行文件,这在容器技术和安全沙箱中非常有用。AT_SYMLINK_NOFOLLOW
: 尝试执行符号链接本身,但支持有限。dirfd
的使用: 提供了更灵活的路径解析能力,尤其是在受限或复杂目录结构中。- 与
fork
结合: 这是创建新进程并执行新程序的经典模式(fork-and-exec 模式)。 - 内核版本: 需要 Linux 内核 3.19 或更高版本。
- 权限: 执行文件需要有可执行权限 (
x
)。 environ
: 使用extern char **environ;
可以方便地传递当前进程的环境变量给新程序。
总结:
execveat
是 execve
的强大扩展,通过引入 dirfd
和 flags
参数,提供了更灵活、更安全的程序执行方式。它允许程序基于已打开的目录或文件描述符来定位和执行可执行文件,这在现代 Linux 系统编程,特别是容器和沙箱技术中具有重要意义。理解其参数和与 fork
的结合使用是掌握 Linux 进程控制的基础。