Linux 程序替换

程序替换效果演示

c 复制代码
#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. 子进程执行程序替换,会影响父进程吗?

    • 答案显然是不会哈!子进程在调用程序替换的接口时,本质上就是在对父进程的代码做出修改,操作系统检测到子进程在修改父进程的代码,就会发生写时拷贝!写时拷贝的本质就是申请内存空间,写时拷贝之后就能进行程序替换啦!这么分析的确不会影响父进程!可以写个代码来验证:
    c 复制代码
    #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 就跟刚才那个一毛一样!

c 复制代码
#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 说明第一个参数需要传递可执行程序的路径!

c 复制代码
#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 环境变量中查找,我们只需要指定新程序的名称就可以啦!

c 复制代码
#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 的命令行参数讲过!

c 复制代码
#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 中的代码:

bash 复制代码
echo "这是一个脚本文件"
ls -a -l

这是 C 语言代码:

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 中的代码:

python 复制代码
def function():
    for i in range(5):
        print("hello python!")

function()

test.c 中的代码:

c 复制代码
#include <stdio.h>
#include<unistd.h>

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

我们可以看到也是成功执行起来了 .py 文件好吧!

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

这是一个 C++ 的代码:

cpp 复制代码
#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 这个可执行程序了,第二个参数就没必要加啦!

c 复制代码
#include <stdio.h>
#include<unistd.h>

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

原因

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

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

传递命令行参数

前面在讲程序替换的接口时提到,传递执行方式的本质就是给新的程序传递命令行参数,如何验证呢?

很简单,我们可以利用命令行参数表将命令行参数打印出来嘛!

我们用刚刚的 C++ 文件来吧,C++C 一样都有命令行参数表和环境变量表哈!

cpp 复制代码
#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 拿到哈:

c 复制代码
#include <stdio.h>
#include<unistd.h>

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

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

bash 复制代码
All:test testcpp
test:test.c
	gcc -o $@ $^
testcpp:test.cpp
	g++ -o $@ $^
.PHONY:clean
clean:
	rm -f clean

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

传递环境变量

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

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

c 复制代码
#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 中打印环境变量:

cpp 复制代码
#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 为父进程导入一个环境变量,看看能否被新程序拿到哈!

c 复制代码
#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 哦!

c 复制代码
#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. 如何为新程序传递环境变量
相关推荐
Scott9999HH9 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
Yana.nice10 小时前
Linux 只保留 30 天内日志(find命令删除日志文件)
linux·运维·chrome
码智社10 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海10 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖11 小时前
JDK 26 新特性详解
java·开发语言
马优晨12 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
2601_9605679613 小时前
电商套图自动化效率的工程量化分析——从逐张生成到批量套图的架构演进
运维·架构·自动化
吳所畏惧14 小时前
宝塔面板Redis密码修改指南:SSH命令修改 vs 面板UI界面修改,哪个更靠谱?
运维·服务器·数据库·redis·缓存·ssh
DFT计算杂谈14 小时前
无 Root 权限在 Tesla K80 零门槛部署 DeepSeek 大模型
linux·服务器·网络·数据库·机器学习
人邮异步社区14 小时前
怎么把C语言学到精通?
c语言·开发语言