Linux 程序替换

2023-12-14 23:25:47

程序替换效果演示

#include<stdio.h>
#include<unistd.h>

int main()
{
    printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());

    execl("usr/bin/ls", "ls", "-a", "-l", NULL);

    printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());

    return 0;
}

我们可以看到,通过这一段代码,我们能够在 C 语言的程序中执行系统的命令!
在这里插入图片描述
其中 execl 函数就是程序替换的接口,我们发现程序替换成功之后,后续的代码没有被执行:printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());

程序替换的概念及原理

在Linux中,程序替换是指一个正在运行的进程用另一个程序来替代的过程。新程序取代了原始程序的内存空间和执行上下文。这个过程中,原始程序的代码、数据和堆栈都被新程序替代。
在这里插入图片描述
上图中是一个程序替换的示意图,对一开始的代码做出分析!我们将刚才演示的代码编译成可执行程序之后,会被放在磁盘中!./test 执行这个可执行程序,他就会被加载到内存中!当执行到 execl 函数时,原进程的代码和数据就会被 ls 这个可执行程序替换,从头开始执行 ls 这个可执行程序的代码!
其中绿色框框的那一部分是不会改变的,页表中虚拟地址到物理地址的映射可能会改变!因为当新程序的代码和数据比较大时就会多申请一些内存空间,当新程序的代码和数据比较小时就会释放一部分空间

  1. CPU 如何知道新程序从哪里开始执行代码?

    • 在编译形成可执行程序的时候,程序代码执行的起始地址就已经编译到了可执行程序中了!程序替换之后就能找到程序的入口并正确向后执行!具体会在以后讲程序加载的时候详解!
  2. 子进程执行程序替换,会影响父进程吗?

    • 答案显然是不会哈!子进程在调用程序替换的接口时,本质上就是在对父进程的代码做出修改,操作系统检测到子进程在修改父进程的代码,就会发生写时拷贝!写时拷贝的本质就是申请内存空间,写时拷贝之后就能进行程序替换啦!这么分析的确不会影响父进程!可以写个代码来验证:
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main()
    {
        pid_t id = fork();
        if (id == 0)
        {
            printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
            execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
            printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
        }
        else if (id > 0)
        {
            pid_t ret = waitpid(id, NULL, 0);
            if(ret > 0)
            {
                printf("wait %d success\n", ret);
            }
        }
        else
        {
            perror("fork");
        }
    
        return 0;
    }
    

    可以看到:子进程发生程序替换之后,父进程的代码并没有收到影响,必然父进程不可能成功等待子进程!
    在这里插入图片描述

  3. 代码也能发生写时拷贝?

    • 的确可以,代码是只读的只是针对上层用户而言的!操作系统完全有这个权限,有这个能力去修改进程的代码,因为操作系统是进程的管理者嘛!这也侧面印证了物理内存根本没有权限管理的概念!
  4. 程序替换是否有创建新的进程?

    • 这个是没有的!程序替换只进行程序代码和数据的替换,不会创建新的进程,不会释放 task_struct 等内核数据结构,只需要对页表等做一定层度上的修改即可!
      从第二个问题中父进程等待子进程成功的例子就能看出,如果创建了新的进程父进程是不可能等待子进程成功的!
  5. 为什么 execl 之后的代码没有被执行呢?

    • 这个应该很简单,execl 之后的代码属于原来进程的代码和数据,execl 之后代码和数据就被新程序替换了,不被执行是很正常的事情!如果说程序替换的时候发生了失败,那么原来的进程 execl 之后的代码才会被执行!这就意味着只有当程序替换的函数调用失败之后才会有返回值!
  6. 一个小知识:

    • Linux 中形成的可执行程序是有一定格式的:ELF 格式,可执行程序的入口地址就在这个表的表头中,表头中还有虚拟内存区域划分的起始地址等等!

学习各种程序替换接口

我们看到程序替换的接口还是比较多的哈!不过他们之间还是有一定的规律的!可以通过他们的名字记忆怎么使用!
在这里插入图片描述
这些函数的本质:第一个参数:找到可执行程序;第二个参数:可执行程序的执行方法。

execl

可以看到这些所有的接口都是 exec 开头的!

  • execl 的第一个参数是你要执行的可执行程序的路径!
  • execl 的第二个参数是一个可变参数哈!我们在学习 C 语言的时候,scanf printf 不就是两个有可变参数的函数嘛!第二个参数表示你想怎么执行这个可执行程序。比如你在命令行执行 ls 命令的时候:ls -a -l 那么这第二个参数就是 "ls", "-a", "-l", NULL
  • 为什么要加一个 NULL 呢?你还记得我们在讲命令行参数的时候提到的命令行参数表吧!那个表的结尾就有一个 NULL。我们用的程序替换的接口中,传入执行方式的参数都必须加 NULL,其实传入的执行方式就是传给你要执行程序的 char* argv[],即给新的程序传入命令行参数!所以要传入一个 NULL

怎么记忆呢?
execl 中除了 exec 还有一个 l 我们可以把 l 理解为 listlistC++ 就是链表嘛,链表不是一个一个的节点,表明我们要将新程序的执行方式拆分称为一个一个的 选项嘛!

这个函数的效果在一开始就演示了,这里就不再重复演示啦!

execlp

这个函数中除了 l 还多了一个 p 这个 p 可以理解为 PATH 表示这个函数会在 PATH 环境变量中寻找你要执行的可执行程序!这个 l 就跟刚才那个一毛一样!

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
    execlp("ls", "ls", "-a", "-l", NULL);
    printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
    return 0;
}

可以看到也是成功执行了程序替换!execlp 的第一个参数也可以传路径哈,只不过我们讲的是这个函数的标准使用方法!
请结合这些函数的本质理解!
在这里插入图片描述

execv

这个函数没有 l 了,变成了一个 v 这个 v 可以理解为 vectorvectorC++ 中表示一个数组嘛!表示新程序的执行方式要通过一个字符串数组传递!这个数组的末尾也要带上 NULL 哦!请结合传递执行方式的本质来理解
并且这个函数中没有 p 说明第一个参数需要传递可执行程序的路径!

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
    
    char* argv[] = {
        "ls",
        "-a",
        "-l",
        NULL
    };
    
    execv("/usr/bin/ls", argv);
    printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
    return 0;
}

execvp

这个函数中有 v 表示执行新程序的方式需要用数组传递,这个函数中还有 p 表示新的程序会在 PATH 环境变量中查找,我们只需要指定新程序的名称就可以啦!

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
    
    char* argv[] = {
        "ls",
        "-a",
        "-l",
        NULL
    };
    
    execvp("ls", argv);
    printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
    return 0;
}

在这里插入图片描述

execle

这个函数中有个 l 说明传递新程序执行方法时需要一个一个选项地传递;这个函数中没有 p 说明传递第一个参数时需要指定可执行程序的路径!
我们还发现这个函数中多了一个 e 这个 e 就是环境变量的意思,也就是说通过这个函数我们可以自定义地为新程序传入环境变量!我们这里就以 char** environ 进行演示啦!environ 变量是库提供的一个全局变量,指向父进程的环境变量表,environ 在讲解环境变量的时候讲过,环境变量表在 Linux 的命令行参数讲过!

#include <stdio.h>
#include <unistd.h>

extern char** environ;

int main()
{
    printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
    
    execle("/usr/bin/ls", "ls", "-a", "-l", NULL, environ);
    printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
    return 0;
}

在这里插入图片描述

execvpe

这个函数带了字母 v, p, e 想必你已经知道该怎么使用了吧!这里就不在做讲解了哦!

exec 系列函数执行其他可执行文件

我们看到说明文档的描述是:执行一个文件!(execute a file) 这就说明不是非要执行可以由 C 语言形成的可执行文件哇!
在这里插入图片描述

execl 执行脚本文件

脚本文件就是一个文本文件,只不过需要特定的解释器来解释文本文件的字符串!脚本文件的后缀可以是 .sh,我们就可以写一点 Linux 下的命令,然后用 bash 命令行解释器来执行这个脚本文件!

这是 test.sh 中的代码:

echo "这是一个脚本文件"
ls -a -l

这是 C 语言代码:

#include <stdio.h>
#include <unistd.h>

int main()
{
    execl("/usr/bin/bash", "bash", "test.sh");
    return 0;
}

我们找到这个可以执行的 bash 命令行解释器,然后用 bash 命令行解释器来执行这个脚本文件:
在这里插入图片描述

execl 执行 .py 文件

python 是一种解释型语言,需要用 python 解释器来一行一行地解释执行 python 代码!
test.py 中的代码:

def function():
    for i in range(5):
        print("hello python!")

function()

test.c 中的代码:

#include <stdio.h>
#include<unistd.h>

int main()
{
    execl("/usr/bin/python", "python", "test.py", NULL);
    return 0;
}

我们可以看到也是成功执行起来了 .py 文件好吧!
在这里插入图片描述

运行 C++ 形成的可执行文件

这是一个 C++ 的代码:

#include<iostream>
using namespace std;

int main()
{
    cout << "hello linux said by cplusplus" << endl;
    return 0;
}

我们先用 g++ -o testcpp test.cpp 编译形成一个可执行程序!
然后我们来使用 execl 调用这个可执行程序!
这里为什么执行新程序的方式不是 ./testcpp 呢?这就得弄清楚我们之前执行我们自己写的可执行程序为什么要加 ./ 了!这个的原因已经在讲环境变量的时候讲过啦!就是为了找到这个可执行程序了嘛!execl 的第一个参数已经能够找到 testcpp 这个可执行程序了,第二个参数就没必要加啦!

#include <stdio.h>
#include<unistd.h>

int main()
{
    execl("./testcpp", "testcpp", NULL);
    return 0;
}

在这里插入图片描述

原因

无论是可执行程序,还是脚本,为什么能够跨语言调用呢?

  • 所有的语言不管他借助什么东西运行起来,在现代操作系统中他一定是进程!是进程就一定能够别其他进程调用!只要一个语言提供了调用进程的接口,那么理论上他就可以调用任何一个进程,表现出来就是可以调用其他语言形成的可执行程序 (执行其他语言的代码)!

传递命令行参数

前面在讲程序替换的接口时提到,传递执行方式的本质就是给新的程序传递命令行参数,如何验证呢?
很简单,我们可以利用命令行参数表将命令行参数打印出来嘛!
我们用刚刚的 C++ 文件来吧,C++C 一样都有命令行参数表和环境变量表哈!

#include <iostream>
using namespace std;

int main(int argc, char *argv[])
{
    cout << "test.cpp 文件的命令行参数:" << endl;
    for (int i = 0; argv[i]; i++)
    {
        cout << i << " : " << argv[i] << endl;
    }
    return 0;
}

我们在 test.c 文件中就多传一点命令行参数,看他能不能被 testcpp 拿到哈:

#include <stdio.h>
#include<unistd.h>

int main()
{
    execl("./testcpp", "testcpp", "-a", "l", "-w", NULL);
    return 0;
}

这里我们就用到了 makefile 编译多个源文件生成多个可执行程序的编写方法,忘记了的 uu 可以复习一下哦!??一文搞懂 makefile

All:test testcpp
test:test.c
	gcc -o $@ $^
testcpp:test.cpp
	g++ -o $@ $^
.PHONY:clean
clean:
	rm -f clean

在这里插入图片描述
这个有什么用呢?你想想这个是我们自己写的可执行程序嘛!如果我们要执行的新的程序是系统的命令,我们就能自己模拟实现一个命令行解释器啦!

传递环境变量

前面提到可以给新程序传递环境变量!那我们不传环境变量,新程序还会有环境变量嘛?

test.c 的代码:可以看到我们没有为 testcpp 传递环境变量哦!

#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        execl("./testcpp", "testcpp", "-a", "l", "-w", NULL);
    }
    else if(id > 0)
    {
        waitpid(id, NULL, 0);
    }
    return 0;
}

我们在 testcpp 中打印环境变量:

#include <iostream>
using namespace std;

int main(int argc, char *argv[], char* env[])
{
    cout << "test.cpp 文件的命令行参数:" << endl;
    for (int i = 0; argv[i]; i++)
    {
        cout << i << " : " << argv[i] << endl;
    }
    cout << "test.cpp 文件的环境变量: " << endl; 
    for(int i = 0; env[i]; i++)
    {
        cout << env[i] << endl;
    }
    return 0;
}

我们并没有传递环境变量,testcpp 还真就将环境变量打印出来了!
在这里插入图片描述
这是为什么呢?在学习环境变量的时候,我们知道一个进程的环境变量来自于两个地方:

  • 要么是通过 main 函数传递进来的!
  • 要么就是从父进程那里继承过来的!

这个例子中我们没有传递环境变量表,那么只能说明新程序的环境变量是从父进程那里继承过来的!
这能说明什么问题呢?

  • fork创建的子进程会继承 test 进程的环境变量,当我们在子进程中使用程序替换接口,能在新程序中打印出来环境变量,只能说明这个环境变量是来自原来的子进程!因为我们并没有给新程序传递环境变量!这就说明程序替换并不会替换原进程的环境变量

为新程序添加环境变量

bash 添加环境变量

我们自己写的可执行程序一旦运行起来就是 bash 的子进程!我们在自己写的程序中创建子进程,然后在子进程中调用程序替换接口!这就意味着只要为 bash 添加环境变量之后,环境变量会通过 bash->自己写的程序->子进程 的顺序一路继承给子进程,最后给到新的程序!
在这里插入图片描述

为父进程添加环境变量

根据上面个的原理,我们可以拦腰截断,为父进程添加环境变量,然后继承给子进程,最终给到新程序!


在学习环境变量的时候,我们学到了为当前进程添加环境变量的函数 putenv 这里就可以用起来啦!
在这里插入图片描述
我们使用 putenv 为父进程导入一个环境变量,看看能否被新程序拿到哈!

#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>

int main()
{
    putenv("MY_VALUE_1=123456");
    pid_t id = fork();
    if(id == 0)
    {
        execl("./testcpp", "testcpp", "-a", "l", "-w", NULL);
    }
    else if(id > 0)
    {
        waitpid(id, NULL, 0);
    }
    return 0;
}

环境变量比较多哈,我们使用 grep 命令过滤一下,发现新程序中的确有新导入的环境变量呢!
在这里插入图片描述

使用程序替换接口

在程序替换接口的使用部分,我们使用 execle 给新程序传递了 environ 指向的环境变量表,可是我们就是想传递自己的环境变量应该怎么做呢!
很简单哈,我们看到 execle 的最后一个参数是字符串数组哈,我们只需要传递一个字符串数组过去就行,环境变量表传递嘛,数组最后必须🉐有 NULL 哦!

#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        char* env[] = {
            "MY_VALUE_2=123455",
            "MY_VALUE_3=123213123",
            "MY_VALUE_4=2343243",
            NULL
        };
        execle("./testcpp", "testcpp", "-a", "l", "-w", NULL, env);
    }
    else if(id > 0)
    {
        waitpid(id, NULL, 0);
    }
    return 0;
}

在这里插入图片描述
可以看到这么传递之后,原来的一大坨的环境变量就没啦!可以看出通过程序替换接口传递环境变量是通过替换的方式传递哒!

  1. Linux 程序替换的本质
  2. Linux 程序替换的接口以及参数理解
  3. 如何为新程序传递命令行参数
  4. 如何为新程序传递环境变量

文章来源:https://blog.csdn.net/m0_73096566/article/details/134918160
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。