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()来打开文件,读取并验证其内容,并最终运行。然而,在打开与执行文件之间,存在将该文件替换的可能性(持有打开文件描述并不能阻止创建同名新文件),最终造成验证者并非执行者的情况。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
非常的全,绝对不会让你后悔。 目 录 译者序 前言 第一部分 shell 第1章 文件安全与权限 1 1.1 文件 1 1.2 文件类型 2 1.3 权限 2 1.4 改变权限位 4 1.4.1 符号模式 4 1.4.2 chmod命令举例 5 1.4.3 绝对模式 5 1.4.4 chmod命令的其他例子 6 1.4.5 可以选择使用符号模式或绝对模式 7 1.5 目录 7 1.6 suid/guid 7 1.6.1 为什么要使用suid/guid 8 1.6.2 设置suid/guid的例子 8 1.7 chown和chgrp 9 1.7.1 chown举例 9 1.7.2 chgrp举例 9 1.7.3 找出你所属于的用户组 9 1.7.4 找出其他用户所属于的组 10 1.8 umask 10 1.8.1 如何计算umask值 10 1.8.2 常用的umask值 11 1.9 符号链接 12 1.9.1 使用软链接来保存文件的多个映像 12 1.9.2 符号链接举例 12 1.10 小结 13 第2章 使用find和xargs 14 2.1 find命令选项 14 2.1.1 使用name选项 15 2.1.2 使用perm选项 16 2.1.3 忽略某个目录 16 2.1.4 使用user和nouser选项 16 2.1.5 使用group和nogroup选项 16 2.1.6 按照更改时间查找文件 17 2.1.7 查找比某个文件新或旧的文件 17 2.1.8 使用type选项 17 2.1.9 使用size选项 18 2.1.10 使用depth选项 18 2.1.11 使用mount选项 18 2.1.12 使用cpio选项 18 2.1.13 使用exec或ok来执行shell命令 19 2.1.14 find命令的例子 20 2.2 xargs 20 2.3 小结 21 第3章 后台执行命令 22 3.1 cron和crontab 22 3.1.1 crontab的域 22 3.1.2 crontab条目举例 23 3.1.3 crontab命令选项 23 3.1.4 创建一个新的crontab文件 24 3.1.5 列出crontab文件 24 3.1.6 编辑crontab文件 24 3.1.7 删除crontab文件 25 3.1.8 恢复丢失的crontab文件 25 3.2 at命令 25 3.2.1 使用at命令提交命令或脚本 26 3.2.2 列出所提交的作业 27 3.2.3 清除一个作业 27 3.3 &命令 27 3.3.1 向后台提交命令 28 3.3.2 用ps命令查看进程 28 3.3.3 杀死后台进程 28 3.4 nohup命令 29 3.4.1 使用nohup命令提交作业 29 3.4.2 一次提交几个作业 29 3.5 小结 30 第4章 文件名置换 31 4.1 使用* 31 4.2 使用? 32 4.3 使用[...]和[!...] 32 4.4 小结 33 第5章 shell输入与输出 34 5.1 echo 34 5.2 read 35 5.3 cat 37 5.4 管道 38 5.5 tee 39 5.6 标准输入、输出和错误 40 5.6.1 标准输入 40 5.6.2 标准输出 40 5.6.3 标准错误 40 5.7 文件重定向 40 5.7.1 重定向标准输出 41 5.7.2 重定向标准输入 42 5.7.3 重定向标准错误 42 5.8 结合使用标准输出和标准错误 43 5.9 合并标准输出和标准错误 43 5.10 exec 44 5.11 使用文件描述符 44 5.12 小结 45 第6章 命令执行顺序 46 6.1 使用&& 46 6.2 使用|| 46 6.3 用()和{ }将命令结合在一起 47 6.4 小结 48 第二部分 文本过滤 第7章 正则表达式介绍 49 7.1 使用句点匹配单字符 50 7.2 在行首以^匹配字符串或字符序列 50 7.3 在行尾以$匹配字符串或字符 51 7.4 使用*匹配字符串中的单字符或其重复 序列 51 7.5 使用\屏蔽一个特殊字符的含义 52 7.6 使用[]匹配一个范围或集合 52

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值