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

1.elf文件格式

目标文件主要分为三种类型:

  • 可重定位文件(Relocatable File) 包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
  • 可执行文件(Executable File) 包含适合于执行的一个程序。
  • 共享目标文件(Shared Object File) 包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。

关于elf文件格式可以看《ELF文件格式分析》,对照010editor的elf模板,分析起来也会比较容易:

eelf

2.重要数据结构

在讲解可执行程序的加载前先来看看两个十分关键的数据结构:

  • linux_binprm结构。内核会为每个要执行的可执行程序分配一个linux_binprm结构,用来存储这个可执行程序的关键数据(比如这个可执行程序的文件名,参数个数等),结构大致如下(/linux-3.18.6/include/linux/binfmts.h#14):

  • linux_binfmt结构。对于不同格式的文件,需要对应不同的解析器,而linux_binfmt结构就保存了各个解析器的关键信息(比如解析器代码的起始位置)具体如下(linux-3.18.6/include/linux/binfmts.h#70):

3.可执行程序的加载和运行

Linux系统下有很多不同格式的可执行文件(java,elf,shell等),那么内核到底是如何加载不同格式的文件呢?首先肯定的一点是解析不同文件的代码肯定不同,在加载不同程序时内核肯定得先找到处理这个文件格式的解析代码,这个过程应该大致如下图:

loadelf

那内核到底是不是这样做的呢?要想解开这个疑惑还是得从源码中找答案,接下来就来具体分析下内核到底是怎么加载可执行程序的。

Linux下的execve函数大家应该都不陌生, 它的作用是调用一个可执行程序完全代替当前的进程,接下来就以execve加载可执行程序为例来看看内核是如何加载可执行程序的。

execve在linux下有不同形式的封装,但是最终都会调用到内核的do_execve函数(linux-3.18.6/fs/exec.c#1604):

上面的代码中将SYSCALL_DEFINE3宏展开之后就相当于调用了execve函数。调用的do_execve如下(linux-3.18.6/fs/exec.c#do_execve):

do_execve_common开始才是关键部分(linux-3.18.6/fs/exec.c#do_execve_common):

再来到exec_binprm函数(linux-3.18.6/fs/exec.c#exec_binprm):

继续来到search_binary_handle(/linux-3.18.6/fs/exec.c#1352):

search_binary_handler 函数就是逐个扫描formats 队列,直到找到一个匹配的可执行文件格式,运行的事就交给它。如果在这个队列中没有找到相应的可执行文件格式,就要根据文件头部的信息来查找是否有为此种格式设计的可动态安装的模块,如果有,就把这个模块安装进内核,并挂入 formats 队列,然后再重新扫描。

formats的作用是用来遍历不同的文件格式处理模块,他的定义如下:

内核在初始化时通过register_binfmt函数注册不同的处理模块( /linux-3.18.6/fs/exec.#74):

看到这就基本验证了上面那幅图的猜想, 内核要先注册各个不同类型可执行程序的处理模块,在解析一个新程序时查找该用哪个模块处理,最终完成可执行程序的加载。

若是处理elf格式的可执行程序,则在调用 retval = fmt->load_binary(bprm);时本质上调用了load_elf_binary函数(相当于多态机制)初始化如下(/linux-3.18.6/fs/binfmt_elf.c#84):

而load_elf_binary就是加载elf文件的“解析器”(/linux-3.18.6/fs/binfmt_elf.c#571):

解析elf格式,并且将elf中的各个段映射到进程的虚拟地址空间。如果这个elf可执行程序需要依赖其他动态链接库时就要先加载链接器ld(代码不完整时要先将代码补全),而如果这个elf是静态链接成的话,说明elf已经不缺代码可以直接跑,这时就将这个可执行程序的入口赋值给elf_entry。

4.gdb动态跟踪加载流程

用gdb跟踪下执行execve时的返回地址:

执行的程序是静态链接而成。在start_thread处下断,查看new_ip为0x8048d2a:

newip

用readelf -h查看这个可执行程序如下:

newip2

可以看到new_ip和这个静态链接的可执行入口地址一致。

由于老师给的代码没有动态链接器,所以在跟踪动态链接的文件时在打开动态链接器的过程中出错,无法跟踪完整的动态链接过程:

newip3

不过这也能验证上面关于加载动态链接文件的说法,即在加载动态链接的文件时execve函数并不会直接返回到elf文件入口,而是先跳转至链接器ld,链接器用广度优先(也可以是深度)算法加载所有需要加载的动态链接库,加载完成后才会返回elf程序的入口点继续执行。

5.总结

通过对源码的追踪分析,理解了可执行程序加载的大致流程,尤其看到了静态链接和动态链接的另一个差别:加载之后的入口点不同,前者是直接跳转至程序入口点,后者是先跳转值链接器,完成必要的加载操作之后才会将控制权交还给源程序。

 

观看更多有关 的文章?

*

+
跳转到评论