《漫谈兼容内核之九:elf映像的装入(二)》由会员分享,可在线阅读,更多相关《漫谈兼容内核之九:elf映像的装入(二)(26页珍藏版)》请在金锄头文库上搜索。
1、漫谈兼容内核之九:漫谈兼容内核之九:ELFELF 映像的装入映像的装入( (二二) )漫谈兼容内核之九:ELF 映像的装入(二)align=centersize=4b漫谈兼容内核之九:ELF 映像的装入(二)/b/size/alignalign=center毛德操/align上一篇漫谈介绍了在通过 execve()系统调用启动一个 ELF 格式的目标映像时发生于 Linux 内核中的活动。简而言之,内核根据映像头部所提供的信息把目标映像映射到(装入)当前进程用户空间的某个位置上;并且,如果目标映像需要使用共享库的话,还要(根据映像头部所提供的信息)将所需的“解释器”的映像也映射到用户空间的某个
2、位置上,然后在从系统调用返回用户空间的时候就“返回”到解释器的入口,下面就是解释器的事了。如果目标映像不使用共享库,那么问题就比较简单,返回用户空间的时候就直接“返回”到目标映像的入口。现代的应用软件一般都要使用共享库,所以我们把这当作常态,而把不使用共享库的应用软件作为一种简化了的特例。映像装入用户空间的位置有些是固定的、在编译连接时就确定好了的;有些则是“浮动”的、可以在装入时动态决定;具体要看编译时是否使用了-fPIC 选项。一般应用软件主体的映像都是固定地址的,而共享库映像的装入地址都是浮动的。特别地,解释器映像的装入地址也是浮动的。2ELF 映像的结构每个操作系统对于在其内核上运行的
3、可执行程序二进制映像都有特定的要求和规定,包括例如映像的格式,映像在用户空间的布局(程序段、数据段、堆栈段的划分等等),映像装入用户空间的地址是否可以浮动、以及如何浮动,是否支持动态连接、以及如何连接,如何进行系统调用,等等。这些要求和规定合在一起就构成了具体操作系统的“应用(软件)二进制界面(Application Binary Interface)” ,缩写成 ABI。显然,ABI 是二进制映像的“生产者”即编译/连接工具和使用者即映像装入/启动手段之间的一组约定。而我们一般所说的二进制映像格式,实际上并不仅仅是指字面意义上的、类似于数据结构定义那样的“格式” ,还包括了跟映像装入过程有关
4、的其它约定。所以,二进制映像格式是 ABI 的主体。目前的 Linux ABI 是在 Unix 系统 5 的时期(大约在 1980 年代)发展起来的,其主体就是 ELF,这是“可执行映像和连接格式(Executable and Lnking Format)”的缩写。读者已经看到,ELF 映像文件的开始是个 ELF 头,这是一个数据结构,结构中有个指针(位移量),指向文件中的一个“程序头”数组(表)。各个程序头表项当然也是数据结构,这是对映像文件中各个“节(Segment)”的(结构性)描述。从映像装入的角度看,一个映像是由若干个 Segment 构成的。有些 Segment 需要被装入、即被映
5、射到用户空间,有些则不需要被装入。在前一篇漫谈中读者已经看到,只有类型为 PT_LOAD 的Segment 才需要被装入。所以,映像装入的过程只“管”到 Segment为止。而从映像的动态连接、重定位(即浮动)、和启动运行的角度看,则映像是由若干个“段(Section)”构成的。我们通常所说映像中的“代码段” 、 “数据段”等等都是 Section。所以,动态连接和启动运行的过程所涉及的则是 Section。一般而言,一个 Segment可以包含多个 Section。其实,Segment 和 Section 都是从操作/处理的角度对映像的划分;对于不同的操作/处理,划分的方式也就可以不同。所以
6、,读者在后面将会看到,一个 Segment 里面也可以包含几个别的 Segment,这就是因为它们是按不同的操作/处理划分的、不同意义上的 Segment。Section 也是一样。在 Linux 系统中,(应用软件主体)目标映像本身的装入是由内核负责的,这个过程读者已经看到;而动态连接的过程则由运行于用户空间的“解释器”负责。这里要注意:第一, “解释器”是与具体的映像相连系的,其本身也有个映像,也需要被装入。与目标映像相连系的“解释器”也是由内核装入的,这一点读者也已看到。第二,动态连接的过程包括了共享库映像的装入,那却是由“解释器”在用户空间实现的。本来,看了内核中与装入目标映像有关的代
7、码以后,应该接着看“解释器”的代码了。但是后者比前者复杂得多,也繁琐得多,原因是牵涉到许多 ELF 和 ABI 的原理和细节,所以有必要先对 ELF动态连接的原理作一介绍。明白了有关的原理和大致的方法以后,具体的代码实现倒在其次了。前面讲过,Linux 提供了两个很有用的工具,即 readelf 和objdump。下面就用这两个工具对映像/usr/local/bin/wine 进行一番考察,以期在此过程中逐步对 ELF 和 ABI 有所了解和理解,这也是进一步阅读、理解“解释器”的代码所需要的。我们用命令行“readelf a /usr/local/bin/wine”和“objdump d /
8、usr/local/bin/wine”产生两个文件(把结果重定向到文件中),然后察看这两个文件的部分内容。首先是目标映像的 ELF 头:ELF Header:Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00Class: ELF32Data: 2s complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: EXEC (Executable file)Machine: Intel 80386Version: 0x1Entr
9、y point address: 0x8048750Start of program headers: 52 (bytes into file)Start of section headers: 114904 (bytes into file)Flags: 0x0Size of this header: 52 (bytes)Size of program headers: 32 (bytes)Number of program headers: 6Size of section headers: 40 (bytes)Number of section headers: 36Section he
10、ader string table index: 33这就是映像文件开头处的 ELF 头,其最初 4 个字节为0x7f 、和E 、 L 、 F 。从其余字段中我们可以看出: OS 是 Unix、其实是 Linux、而 ABI 是系统 5 的 ABI。ABI的版本号为 0。 CPU 为 x86。 映像的类型为 EXEC,即带有主函数 main()的应用软件映像(若是共享库则类型为 DYN、即动态连接库)。 映像的程序入口地址为 0x8048750。如前所述,EXEC 映像的装入地址是固定的、不能浮动。 程序头数组起点在文件中的位移为 52(字节),而 ELF 头的大小正好也是 52,所以紧接 E
11、LF 头的后面就是程序头数组。数组的大小为 6,即映像中有 6 个 Segment。 Section 头的数组则一直在后面位移位 114904 的地方,映像中有 36 个 Section。于是,我们接下去看程序头数组:Program Headers:Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg AlignPHDR 0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4INTERP 0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R 0x1Req
12、uesting program interpreter: /lib/ld-linux.so.2LOAD 0x000000 0x08048000 0x08048000 0x011cc 0x011cc R E 0x1000LOAD 0x0011cc 0x0804a1cc 0x0804a1cc 0x00158 0x00160 RW 0x1000DYNAMIC 0x0011d8 0x0804a1d8 0x0804a1d8 0x000d8 0x000d8 RW 0x4NOTE 0x000108 0x08048108 0x08048108 0x00020 0x00020 R 0x4一个程序头就是关于一个
13、Segment 的说明,所以这就是 6 个Segment。第一个 Segment 的类型是 PHDR,在文件中的位移为0x34、即 52,这就是程序头数组本身。其大小为 0xc0、即 192。前面说每个程序头的大小为 32 字节,而 6 X 32 = 192。第二个Segment 的类型是 INTERP,即“解释器”的文件/路径名,是个字符串,这里说是“/lib/ld-linux.so.2” 。下面是两个类型为 LOAD 的 Segment。如前所述,只有这种类型的 Segment 才需要装入。但是,看一下前者的说明,其起点在文件中的位移是 0,大小是 0x011cc,显然是把 ELF 头和前
14、两个 Segment也包含在里面了。再看后者,其起点的位移是 0x011cc,所以是和前者连在一起的;其大小为 0x158,这样两个 Segment 合在一起是从 0 到 0x1324。计算一下就可知道,实际上是把所有的 Segment 都包括进去了。所以,对于这个特定的映像,说是只装入类型为 LOAD的 Segment,实际上装入的却是整个映像。那么,映像中的什么内容可以不必装入呢?例如 bss 段,那是无初始内容的数据段,就不用装入;还有(与动态连接无关的)符号表,那也不需要装入。注意两个 LOAD 类 Segment 的边界(Alignment)都是 0x1000,即 4KB,那正好是存
15、储页面的大小。还有个问题,既然两个 LOAD 类的 Segment是连续的,那为什么不合并成一个呢?看一下它们的特性标志位就可以知道,第一个 Segment 的映像是可读可执行、但是不可写;第二个则是可读可写、但是不可执行,这当然不能合并。再往下看,下一个 Segment 的类型是 DYNAMIC,那就是跟动态连接有关的信息。如上所述,这个 Segment 其实是包含在前一个Segment 中的,所以也会被装入。最后一个 Segment 的类型是NOTE,那只是注释、说明一类的信息了。当然,跟动态连接有关的信息是我们最为关心的,所以我们看一下这个 Segment 的具体内容:Dynamic segment at offset 0x11d8 contains 22 entries:Tag