Chinaunix首页 | 论坛 | 博客
  • 博客访问: 271354
  • 博文数量: 67
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 620
  • 用 户 组: 普通用户
  • 注册时间: 2015-07-12 19:56
文章分类

全部博文(67)

文章存档

2019年(1)

2018年(1)

2017年(4)

2016年(34)

2015年(27)

我的朋友

分类: LINUX

2016-08-05 14:31:23


分析linux系统如何装载和启动一个可执行程序,也就是分析一个重要的系统调用execve

execve函数原型: #include  int execve (const char* path,char* const argv[],char* const envp[]);

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

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

#include  #include  int main(void)
{ printf("Output something\n"); return 0;
}
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

执行execve的代码:

#include  #include  #include  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;
}
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

对上述代码编译运行,可以得到如下结果:
这里写图片描述
m.o执行结果
回过头去观察execve附近的代码,会发现紧跟在它后面的printf语句并没有执行。在execve函数的描述中,有下面的几句话:

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

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

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.
		
  • 1

也就是说:调用进程的代码段,数据段和堆栈等都已经被新的内容取代,只有进程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.
		
  • 1

接下来包括两个 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.
		
  • 1

这里事实上调用了 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); ... }
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

而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工作,将程序的运行参数以及环境变量参数应用到可执行程序上。

阅读(1613) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~