Unix/Linux编程:exec()族函数

执行新程序:execve()

系统调用execve()可以将新程序加载到某一进程的内存空间。在这一操作过程中,将丢弃就有程序,而进程的栈、数据以及堆段会被新程序的相应不见所替换。在执行了各种C语言函数库的运行时启动代码以及程序的初始化代码后,比如,C++静态构造函数,或者以gcc constructor属性生命的C语言函数,新程序会从main()函数处开始执行

由fork()生成的子进程对execve()的调用最为频繁,不以fork()调用为先导而单独调用execve()的做法在应用中实属罕见。

当进程fork出另外一个进程之后,子进程可以调用一种exec函数,exec会用磁盘上的一个新程序替换当前程序的正文段、数据段、堆栈和栈转,转而运行一个新程序,而不是用父进程的(调用exec并不创建新进程,所以新进程的进程ID仍然是子进程的进程ID)

基于系统调用 execve(),还提供了一系列冠以 exec 来命名的上层库函数,虽然接口方式各异,但功能相同。通常将调用这些函数加载一个新程序的过程称作 exec 操作,或是简单地以 exec()来表示。下面将先描述 execve(),然后再对相关库函数进行说明。

/*
* 功能: execve 执行文件
* 参数: argv[] 利用数组指针来传递给执行文件。
*        envp[] 环境变量数组
* 返回值:  如果执行成功则函数不会返回,执行失败则直接返回-1, 失败原因存于errno 中.
*/
int execve (const char *pathname, char *const argv[], char *const envp[])
  • 参数 pathname 包含准备载入当前进程空间的新程序的路径名,既可以是绝对路径,也可以是相对于调用进程当前工作目录(current working directory)的相对路径
  • 参数 argv 则指定了传递给新进程的命令行参数。该数组对应于 C 语言 main()函数的第 2个参数(argv),且格式也与之相同:是由字符串指针所组成的列表,以 NULL 结束。argv[0]的值则对应于命令名。通常情况下,该值与 pathname 中的 basename(路径名的最后部分)相同。
  • 最后一个参数 envp 指定了新程序的环境列表。参数 envp 对应于新程序的 environ 数组:也是由字符串指针组成的列表,以 NULL 结束,所指向的字符串格式为 name=value

Linux所特有的/proc/PID/exe文件是一个符号链接,包含PID对应进程中正在执行可执行文件的绝对路径名。

调用execve()之后,因为同一进程依然存在,所以进程ID扔保持不变。还有少量其他的进程属性也未发生变化。

  • 如果对 pathname 所指定的程序文件设置了 set-user-ID(set-group-ID)权限位,那么系统调用会在执行此文件时将进程的有效(effective)用户(组)ID 置为程序文件的属主(组)ID。利用这一机制,可令用户在运行特定程序时临时获取特权
  • 无论是否更改了有效 ID,也不管这一变化是否生效,execve()都会以进程的有效用户 ID 去覆盖已保存的(saved)set-user-ID,以进程的有效组 ID 去覆盖已保存的(saved)set-group-ID

由于是将调用程序取而代之,对execve()的成功调用将永不返回,而且也无需检查execve()的返回值,因为该值总是是雷打不动地等于-1。实际上,一旦函数返回,就表明发生了错误。通常,可以通过errno来判断出错原因。可能将errno返回的错误如下:

  • EACCES
    • 参数pathname没有指向一个常规(regular)文件,未对该文件赋予可执行权限,或者因为pathname中某一级目录不可搜索索(not searchable)(即,关闭了该目录的可执行权限)。
    • 欲执行的文件所属的文件系统是以MS_NOEXEC 方式挂载(mount)上。
  • ENOEXEC:
    • 尽管对pathname所指代文件赋予了可执行权限,但系统却无法识别其文件格式。
    • 脚本文件,如果没有包含用于指定脚本解释器(interpreter)(以字符#!开头)的起始行,就可能导致这一错误
  • ETXTBSY :欲执行的文件已被其他进程打开而且正把数据写入该文件中
  • E2BIG:参数列表和环境列表所需空间总和超出了允许的最大值
  • EPERM
    • 进程处于被追踪模式,执行者并不具有root权限,欲执行的文件具有SUID 或SGID 位。
    • 欲执行的文件所属的文件系统是以nosuid方式挂上,欲执行的文件具有SUID 或SGID 位元,但执行者并不具有root权限。
  • EFAULT:参数filename所指的字符串地址超出可存取空间范围。
  • ENAMETOOLONG:参数filename所指的字符串太长。
  • ENOENT :pathname 所指代的文件并不存在。
  • ENOMEM:核心内存不足
  • ENOTDIR:参数filename字符串所包含的目录路径并非有效目录
  • ELOOP:过多的符号连接
  • EIO I/O:存取错误
  • ENFILE:已达到系统所允许的打开文件总数。
  • EMFILE:已达到系统所允许单一进程所能打开的文件总数。
  • EINVAL:欲执行文件的ELF执行格式不只一个PT_INTERP节区
  • EISDIR:ELF翻译器为一目录
  • ELIBBAD:ELF翻译器有问题。

下面我们来看个例子:

  • 这个程序实现为新程序创建参数列表和环境列表,接着调用execve()来执行行由命令行参数(argv[1])所指定的程序路径名
#include <cstring>
#include <stdio.h>
#include <cstdlib>
#include <zconf.h>

int main(int argc, char *argv[])
{
    char *argVec[10];           /* Larger than required */
    char *envVec[] = { "GREET=salut", "BYE=adieu", NULL };

    if (argc != 2 || strcmp(argv[1], "--help") == 0){
        printf("%s pathname\n", argv[0]);
        exit(EXIT_FAILURE);
    }


    /* Create an argument list for the new program */

    argVec[0] = strrchr(argv[1], '/');      /* Get basename from argv[1] */
    if (argVec[0] != NULL)
        argVec[0]++;
    else
        argVec[0] = argv[1];
    argVec[1] = "hello world";
    argVec[2] = "goodbye";
    argVec[3] = NULL;           /* List must be NULL-terminated */

    /* Execute the program specified in argv[1] */

    execve(argv[1], argVec, envVec);
    perror("execve");          /* If we get here, something went wrong */
    exit(EXIT_FAILURE);
}
  • 下面程序是设计专供上面程序来执行的。该程序只是简单显示一下自身的命令行参数以及环境列表
extern char **environ;
int main(int argc, char *argv[])
{
    int j;
    char **ep;

    /* Display argument list */

    for (j = 0; j < argc; j++)
        printf("argv[%d] = %s\n", j, argv[j]);

    /* Display environment list */

    for (ep = environ; *ep != NULL; ep++)
        printf("environ: %s\n", *ep);

    exit(EXIT_SUCCESS);
}
  • 运行上面两个程序:
    在这里插入图片描述

exec()库函数

下面的库函数为执行exec()提供了多种API选择,所有这些函数均构建于execve()调用之上,只是在为新程序指定程序名、参数列表以及环境变量的方式上有所不同:

#include <unistd.h>
/*
* __path: 路径名
*/
/*
* 
* 功能:  execl()执行文件, 
* 参数:  代表执行该文件时传递过去的argv(0),argv[1], ..., 最后一个参数必须用空指针(NULL)作结束.
* 返回值:  如果执行成功则函数不会返回,执行失败则直接返回-1, 失败原因存于errno 中.
*/
int execl (const char *__path, const char *__arg, ...)
/*
* 功能: execv 执行文件
* 参数: 利用数组指针来传递给执行文件。
* 返回值:  如果执行成功则函数不会返回,执行失败则直接返回-1, 失败原因存于errno 中.
*/
int execv (const char *__path, char *const __argv[])
/*
* 功能: execve 执行文件
* 参数: __argv[] 利用数组指针来传递给执行文件。
*        __envp[] 环境变量数组
* 返回值:  如果执行成功则函数不会返回,执行失败则直接返回-1, 失败原因存于errno 中.
*/
int execle (const char *__path, const char *__arg, ...)

/*
* * __file: 
* 	* 如果__file中包含/,则是路径名
*   * 如果__file不包含/,则按照PATH环境变量,从指定的个目录中查找可执行文件
* 			如果找到了一个可执行文件,但是该文件不是由连接编辑器尝试的机器可执行文件,则就认为该文件是一个shell脚本,就会调用/bin/sh,并将该file作为shell的输入
*/
int execvp (const char *__file, char *const __argv[])

/*
* 功能: execlp() 从PATH 环境变量中查找文件并执行
* 参数:  代表执行该文件时传递过去的argv(0),argv[1], ..., 最后一个参数必须用空指针(NULL)作结束.
* 返回值:  如果执行成功则函数不会返回,执行失败则直接返回-1, 失败原因存于errno 中.
* **/
int execlp (const char *__file, const char *__arg, ...)
int fexecve (int __fd, char *const __argv[], char *const __envp[])

在这里插入图片描述

  • 大部分exec()函数要求提供预加载新程序的路径名。而execlp()和execvp()则允许只提供程序的文件名。系统会在由环境变量PATH所指定的目录列表中寻找相应的执行文件。这与 shell 对键入命令的搜索方式一致。这些函数名都包含字母 p(表示PATH),以示在操作上有所不同。如果文件名中包含“/”,则将其视为相对或绝对路径名,不再使用变量 PATH 来搜索文件
  • 函数execle()、execlp()和execl()要求开发者在调用中以字符串列表形式来指定参数,而不是使用数组来描述argv列表。首个参数对应于main()函数的 argv[0],因而通常与参数 filename 或 pathname 的 basename 部分相同。必须以 NULL 指针来终止参数列表,以便于各调用定位列表的尾部。这些函数的名称都包含字母 l(表示 list),以示与那些将以 NULL 结尾的数组作为参数列表的函数有所区别。后者(execve()、execvp()和 execv())名称中则包含字母 v(表示 vector)。
  • 函数execve()和execle()则允许开发者通过envp为新程序显式指定环境变量,其中envp是一个以 NULL 结束的字符串指针数组。这些函数命名均以字母 e(environment)结尾。其他 exec()函数将使用调用者的当前环境(即 environ 中内容)作为新程序的环境

execl函数 : 执行文件函数

#include <unistd.h>

int main(int argc, char *argv[])
{
// 执行/bin/ls,参数是 ls -al /etc/passwd
    execl("/bin/ls","ls", "-al", "/etc/passwd", (char *)0);
    exit(0);
}

在这里插入图片描述

execv

#include <unistd.h>
int main(int argc, char *argv[])
{
// 先从PATH中找到/bin/ls,参数是 ls -al /etc/passwd
    char * myargv[ ]={"ls","-al","/etc/passwd",(char*)0};
    execv("/bin/ls",myargv);
    exit(0);
}

在这里插入图片描述

execve

#include <unistd.h>
int main(int argc, char *argv[])
{
    char * myargv[ ]={"ls","-al","/etc/passwd",(char*)0};
    char * myenvp[ ]={"PATH=/bin",0};
    execve("/bin/ls",myargv, myenvp);
    exit(0);
}

在这里插入图片描述

execlp()函数:从PATH环境变量中查找文件并执行

#include <unistd.h>

int main(int argc, char *argv[])
{
// 先从PATH中找到/bin/ls,参数是 ls -al /etc/passwd
    execlp("ls", "ls","-al", "/etc/passwd", (char *)0);
    exit(0);
}

在这里插入图片描述

execvp

#include <unistd.h>
int main(int argc, char *argv[])
{
    char * myargv[ ]={"ls","-al","/etc/passwd",0};
    execvp("ls",myargv);
    exit(0);
}

在这里插入图片描述

fork子进程之后如何运行另外一个程序

  1. another_process
#include "stdlib.h"
#include "stdio.h"

int main(int argc, char *argv[])
{
    int		i;

    for (i = 0; i < argc; i++)		/* echo all command-line args */
        printf("argv[%d]: %s\n", i, argv[i]);
    exit(0);
}
  1. fork_process
#include "apue.h"
#include <sys/wait.h>

char	*env_init[] = { "USER=unknown", "PATH=/tmp", NULL };

int main(void)
{
    pid_t	pid;

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        if (execle("/home/oceanstar/CLionProjects/apue/build/another_process", "another_process", "myarg1",
                   "my_arg2", (char *)0, env_init) < 0){
            err_sys("execle error");
        }
    }

    if (waitpid(pid, NULL, 0) < 0){
        err_sys("wait error");
    }


    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        if (execlp("ls","ls","-al","/etc/passwd",(char *)0)){
            err_sys("execlp error");
        }

    }

    exit(0);
}

环境变量PATH

函数execvp()和execlp()允许调用者只提供欲执行程序的文件名。二者均使用环境变量PATH来搜索文件。PATH的值是一个以:分割,由多个目录名,也将其称为路径前缀组成的字符串:

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

对一个登录shell而言,由PATH值将由系统级和特定用户的shell启动脚本来设置。由于子进程继承其父进程的环境变量,shell执行每个命令时所创建的进程也就继承了shell的PATH

  • PATH 中指定的路径名既可以是绝对路径名(以/开始),也可以是相对路径名。对相对路径名的诠释是基于调用进程的当前工作目录(current working directory)。自SUSv3起, 当前工作目录应该用.(点)来显式指定
  • 如果没有定义变量 PATH,那么 execvp()和 execlp()会采用默认的路径列表:.:/usr/bin:/bin
  • 出于安全方面的考虑,通常会将当前工作目录排除在超级用户(root)的 PATH 之外

函数 execvp()和 execlp()会在 PATH 包含的每个目录中搜索文件,从列表开头的目录开始,直至成功执行了既定文件。

应该避免在设置了 set-user-ID 或 set-group-ID 的程序中调用 execvp()和 execlp(),至少应当慎用。需要特别谨慎地控制 PATH 环境变量,以防运行恶意程序。

将程序参数指定为列表

如果已知每个exec()的参数个数,调用execle()、execlp()或者execl()时就可以将参数作为列表传入。

下面程序与第一个例子相同,只是调用了 execle()而非 execve():

int
main(int argc, char *argv[])
{
    char *envVec[] = { "GREET=salut", "BYE=adieu", NULL };
    char *filename;

    if (argc != 2 || strcmp(argv[1], "--help") == 0){
        printf("%s pathname\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* Execute the program specified in argv[1] */

    filename = strrchr(argv[1], '/');       /* Get basename from argv[1] */
    if (filename != NULL)
        filename++;
    else
        filename = argv[1];

    execle(argv[1], filename, "hello world", "goodbye", (char *) NULL, envVec);
    errExit("execle");          /* If we get here, something went wrong */
}

将调用者的环境传递给新程序

函数 execlp()、execvp()、execl()和 execv()不允许开发者显式指定环境列表,新程序的环境继承自调用进程。这一举措的后果可以说是喜忧参半。出于安全方面的考虑,有时希望确保程序能够在一个个已知(安全)的环境列表下运行

下面演示了如何运用函数 execl()使新程序继承调用者的环境。对于通过 fork()从 shell 处所继承的环境,程序首先用函数 putenv()进行了修改,接着执行 printenv 程序来显示环境变量 USER 和 SHELL 的值。运行程序的输出如下:
在这里插入图片描述

#include <cstring>
#include <stdio.h>
#include <cstdlib>
#include <zconf.h>

int
main(int argc, char *argv[])
{
    printf("Initial value of USER: %s\n", getenv("USER"));
    if (putenv("USER=britta") != 0){
        perror("putenv");
        exit(EXIT_FAILURE);
    }


    /* exec printenv to display the USER and SHELL environment vars */

    execl("/usr/bin/printenv", "printenv", "USER", "SHELL", (char *) NULL);
    perror("execl");           /* If we get here, something went wrong */
    exit(EXIT_FAILURE);
}

执行由文件描述符指代的程序:fexecve()

glibc 自版本 2.3.2 开始提供函数 fexecve(),其行为与 execve()类似,只是指定将要执行的程序是以打开文件描述符 fd 的方式,而非通过路径名。有些应用程序需要打开某个程序文件,通过执行校验和(checksum)来验证文件内容,然后再运行该程序,这一场景就较为适宜使用函数 fexecve()

当然,即便没有 fexecve()函数,也可以调用 open()来打开文件,读取并验证其内容,并最终运行。然而,在打开与执行文件之间,存在将该文件替换的可能性(持有打开文件描述并不能阻止创建同名新文件),最终造成验证者并非执行者的情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值