MOOC《Linux课程分析》—— exec*函数对应的系统调用处理过程

许松原创,转载请注明出处。
《Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000

本周课程将通过分析一个比较重要的系统调用execve,从而了解Linux内核如何装载和启动一个可执行程序。

execve函数原型:

#include <unistd.h>
int execve (const char* path,char* const argv[],char* const envp[]);

参数说明:path 表示启动程序所在的路径名;argv 表示启动程序所带的参数;envp 表示启动程序所需要的环境变量参数

所要启动的可执行程序m.o的代码,该段代码输出字符串“Output something”:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    printf("Output something\n");
    return 0;
}

执行execve的代码:

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

int main()
{
    char* argv = {0,0};
    char* envp = {0,0};
    pid_t pid = fork();
    if(pid == 0)
    {
        printf("Child Process,Do m.o\n");
        execve("./m.o",argv,envp);
        printf("Can not find ./m.o");
    }
    else if(pid < 0)
        printf("Fork Failed\n");
    else
        printf("Father Failed\n");
    return 0;
}

对上述代码编译运行,可以得到如下结果:
这里写图片描述

m.o顺利执行!

回过头去观察execve附近的代码,会发现紧跟在它后面的printf语句并没有执行。在execve函数的描述中,有下面的几句话:

On success, execve() does not return, on error -1 is returned, and errno is set appropriately.

这个系统调用在成功的时候是不返回的,这又与之前的情况不一样了!又有:

execve() does not return on success, and the text, data, bss, and stack of the calling process are overwritten by that of the program loaded.

也就是说:调用进程的代码段,数据段和堆栈等都已经被新的内容取代,只有进程ID等一些表面上的信息仍保持原样。那么事实到底如何?接下来我们将会对execve函数进行跟踪,来一窥究竟。

execve函数执行过程追踪

通过查表可知execve对应的内核调用为sys_execve。通过断点,可以定位到此处:

这里写图片描述

进入do_execve之前,先看看filename是什么:
这里写图片描述

这里是/hello的原因是,在实验中采用了以下调用:

这里写图片描述

在进入do_execve之后,发现它调用了do_execve_common,继续深入。

函数首先获取到了当前进程的unshare文件:

这里写图片描述

暂时先略过变量struct linux_binprm *bprm,因为接下来关于它的两句代码完成的是初始化的工作。在第1474行,看到了与打开文件有关的代码:file = do_open_exec(filename); 该函数打开了二进制文件所在的文件目录。

这里写图片描述

接下来执行就开始对bprm进行一些填充,对于struct linux_binprm的描述在这里:

* This structure is used to hold the arguments that are used when loading binaries.

接下来包括两个 copy_string语句,将环境变量参数以及启动程序所带的参数复制到bprm中。
然后执行exec_binprm,这个函数真正调用了二进制可执行文件——首先很据bprm获取到对应的文件句柄,然后使用proc_exec_connector(current);来执行二进制可执行文件。

这里写图片描述

首先进入search_binary_handler,在该函数中发现了load_binary:

这里写图片描述

然而这个函数是一个函数指针,这里是一种在C语言中模仿OO的一个技巧(比较简单不再赘述,有兴趣可以搜索相关资料)至于而fmt则是struct linux_binfmt 类型的,具体可以参见这里。描述为:

This structure defines the functions that are used to load the binary formats that linux accepts.

这里事实上调用了 load_elf_library函数,这个函数读取并加载ELF类型的可执行文件,并将ELF文件中的各个段映射到对应的内存当中。
下面是load_elf_binary中的一些重要代码:

static int load_elf_binary(struct linux_binprm *bprm)
{
    ...
    struct pt_regs *regs = current_pt_regs();//首先获取当前寄存器状态
    ...
    loc->elf_ex = *((struct elfhdr *)bprm->buf);//获取exec—header
    ...
    //用从ELF文件中获取到的信息设置当前进程的数据段、堆栈等
    current->mm->end_code = end_code;
    current->mm->start_code = start_code;
    current->mm->start_data = start_data;
    current->mm->end_data = end_data;
    current->mm->start_stack = bprm->p;
    ...
    //开始执行ELF
    start_thread(regs, elf_entry, bprm->p);
    ...
}

而start_thread就是设置一系列的寄存器,当然其中最要的是设置了eip以及esp,从而使得CPU可以转到新的程序执行:

这里写图片描述

而new_ip正是elf_entry的值():

这里写图片描述

new_ip = 134516010,换算成十六进制正是:0x8048d2a

这里写图片描述

由于整个进程的代码段等都被替换掉了,因此原有的代码不再有效,这就是前面例子中,execve语句后面的printf语句不会被执行的原因。

总结

在使用exec*函数的时候,一般首先需要调用一个fork来生成一个新的子进程(否则原有的进程将会被覆盖掉),然后新的进程调用execve()系统调用来执行指定的ELF文件。内核调用sys_execve函数来实现execve。
sys_execve通过调用 do_execve_common,首先访问需要加载文件所在的目录文件,然后通过search_binary_handle在目录中检索需要执行的文件,并根据文件类型来采用对应的加载函数对其进行加载。在加载的过程中,将原来进程的代码段、以及堆栈等利用所加载的文件中的对应值进行替换,最后重新设定EIP和ESP来使可执行文件运行起来。
至于运行参数以及环境变量,则是首先传递到系统调用,然后传递到一个struct linux_binprm类型的结构体中,最后,该结构体变量作为参数参与load工作,将程序的运行参数以及环境变量参数应用到可执行程序上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值