Linux Execve函数详解

1 基本介绍

1
2
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
  • 描述

    execve()执行由pathname引用的程序。这会导致当前由调用进程运行的程序被一个新程序替换,该新程序具有新初始化的堆栈、堆和(已初始化和未初始化)数据段。

    pathname必须是二进制可执行文件或以形式为#!interpreter [optional-arg]开头的脚本。

    argv是传递给新程序作为其命令行参数的字符串指针数组。按照惯例,这些字符串中第一个(即argv[0])应包含与正在执行文件相关联的文件名。argv数组必须以NULL指针结尾。(因此,在新程序中,argv[argc]将为NULL)。

    envp是传递给新程序环境变量的字符串指针数组。该数组包含了环境变量。每个环境变量都是一个 char* 指针,格式为 “name=value”。envp数组同样必须以NULL指针结尾。

    1
    2
    3
    4
    5
    6
    
    char *envp[] = {
     "PATH=/bin",
     "HOME=/home/user",
     "USER=user",
     NULL // 终止环境变量数组
    };

    argvenvp可以从新程序的主函数访问。例如我们编写的C程序,实际上是由操作系统通过execve()系统调用执行(这里是操作系统先执行fork系统调用,创建一个新的子进程,然后在新的子进程中,操作系统执行execve()系统调用),它会传递这些参数给新程序的主函数,即 main 函数。这些参数定义了新程序执行时的环境和命令行参数,在程序启动时由操作系统设置,并在整个程序执行期间保持不变。这使得程序能够根据传递给它的参数和环境变量来执行不同的任务或调整其行为。

  • 返回值

    成功时,execve() 不返回任何值,当 execve 成功替换当前进程的映像并开始执行新的程序时,原来的进程(即调用 execve 的进程)已经不再存在,因此无法返回任何值。

    在出错时返回 -1,并设置适当的 errno

  • 重点

    1. execve实际上就是将当前运行的状态机重置成另一个程序的初始状态
    2. 允许对新状态机设置参数 argv (v) 和环境变量 envp (e)
    3. 在程序启动时,操作系统首先执行 fork 系统调用,创建一个新的子进程。然后,操作系统在子进程中执行 execve 系统调用,以替换子进程的程序映像并开始执行新的程序。原来的父进程继续执行 fork 之后的代码。
    4. 在调用 execve 之前,确保释放所有不再需要的资源,如打开的文件描述符、锁等。
    5. 在调用 execve 之前,确保子进程已经处理了所有待处理的信号,除非你希望信号处理程序在新程序中执行。
    6. 如果 execve 失败,子进程通常应该终止。
    7. 在父进程中,通常会在 fork 之后立即调用 wait 或 waitpid 来等待子进程结束,以确保父进程不会过早退出,从而导致子进程的僵尸进程。

2 execve实例

2.1 自定义argv和envp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <unistd.h>
#include <stdio.h>

int main() {
  char * const argv[] = {
    "/bin/bash", "-c", "env", NULL,
  };
  char * const envp[] = {
    "HELLO=WORLD", NULL,
  };
  execve(argv[0], argv, envp);
  printf("Hello, World!\n");
  return 0;
}

在这段代码中,我们显式的设置了argvenvp,其中参数 "/bin/bash", "-c", "env", NULL,这里的参数实际上是在告诉 bash 执行一个命令(由 -c 后面的字符串指定),在这个例子中是 env,它打印当前的环境变量。

我们运行代码,得到如下输出:

image-20240313091345219

如果我们不传 -c 参数和随后的命令,即只传入 "/bin/bash", NULL 作为参数,bash 会默认进入交互式模式。在这种模式下,它不会执行任何命令并立即退出,而是会等待用户输入,表现为进入了 shell 环境。

我们发现,打印的当前环境变量除了自定的envp,还有一些其他的输出。这是因为除了我们设定的环境变量外,还有一些系统或者 shell 默认的环境变量会被添加到新进程中,例如 PWD 表示当前工作目录,SHLVL 表示 shell 层级,_ 是上一个执行的命令。这就是为什么我们会看到额外的环境变量出现在输出中。

2.2 fork后再通过子进程执行execve

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // 子进程
        char * const argv[] = {"/bin/echo", "Hello, World!", NULL};
        char * const envp[] = {NULL};
        execve("/bin/echo", argv, envp);
    } else if (pid > 0) {
        // 父进程
        wait(NULL); // 等待子进程结束
        printf("Child process finished.\n");
    } else {
        // fork失败
        perror("fork");
        return 1;
    }
    return 0;
}

这段代码演示了如何使用 fork() 系统调用创建一个新的子进程,然后在子进程中执行 execve() 系统调用。这是在 Unix-like 系统中常见的操作模式,因为 execve() 系统调用有一些关键的限制:

  • 一次机会:execve() 系统调用只能用于替换当前进程的映像一次。如果一个进程已经调用了 execve(),它就不能再调用 fork() 或再次执行 execve()
  • 无返回值:execve() 成功执行时,原来的进程映像被新程序映像替换,原来的进程不再存在,因此无法返回任何值。如果在 execve() 执行之前有任何返回值,那么这个返回值是在 fork() 调用之后,由父进程获得的。

因此,在实际应用中,我们通常会先使用 fork() 创建一个子进程,然后在子进程中调用 execve() 执行新的程序。父进程在 fork() 之后会继续执行,并通过调用 wait(NULL) 来等待子进程结束。这样,父进程可以知道子进程已经成功执行了 execve(),并且可以继续执行其他任务或退出。

在多线程程序中,如果一个线程执行了 fork() 并尝试在子进程中执行 execve(),那么其他线程将继续执行,不受 fork()execve() 的影响。只有调用 fork() 的线程会进入子进程,而其他线程则继续在父进程中运行。

得到的运行结果:

image-20240313102827423


相关内容

Buy me a coffee~
HeZephyr 支付宝支付宝
HeZephyr 微信微信
0%