08 October 2013

第3章 目标文件里有什么

这一章可以说是基础部分的重点,无论是要分析链接还是装载过程,首先要对目标文件里有什么做到了如指掌。 这样才不会在后面的分析过程中遇到障碍。

ELF文件的类型与结构描述

我以前一直好奇,shell是如何区分一个不带后缀的文件是执行文件,还是库,还是一个文本文档?学习这一章以后, 对ELF文件的鉴别应该很清楚了。首先,ELF文件带有魔数,如果魔术校验不正确的话,shell应该是不会将文件视为可执行文件的。 其次,文件头部中出去magic num以外,还有各种其他的信息。例如,e_iden成员中的Class Data Version ABI等等其他关键的字段。 试想,如果一个TXT文件试图伪装成一个ELF文件,那么他至少在文件头部要满足这些结构体的描述。

书中有一段原文是这样描述的:
“从上面输出的结果可以看到,ELF的文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、EFL重定位类型、硬件平台、硬件平台版本、入口地址、程序头出口和长度、段表的位置和长度以及段的数量等。”

了解了这些自然能够想到,为什么32位机器上无法运行64位的程序?为什么在诸如跨平台编译器上编译出来的程序无法直接运行?当然也能够解释为什么shell可以将 “helloworld(可执行文件)”这样的命令后面附带的文件装载到内存中,原来ELF文件中是有这么多丰富的信息哦!特别需要注意的是,位数信息,大小端种类这个信息是直接放在文件头16个字节中的。因为先解析他们对后面解析的解析过程至关重要。

ELF文件除了文件头以外,就是各种段的组合。而段有个类似索引的结构,叫做段表。


ELF的段表结构

那什么是段表呢?段表里有什么东西呢?为什么要有段表呢?

我觉得段表可以理解为段的索引结构。用来存放ELF文件各个段的信息。书中一直重复强调了section,这个section(段)和经常碰到的“segment error”中的segment(段)还真不是一回事,有啥区别呢?书中在装载那一部分有详细的介绍。section更注重ELF的静态结构。是ELF文件的结构。而segment是装载过后的动态结构。两个段有一定的区别和联系哟~

首先试想一下,如果你是操作系统?在解析完ELF文件头以后,如何解析其他的段的结构呢?

下面,我们的用户执行了一条readelf的命令,希望我们显示全部的段信息。怎么返回呢?从文件头里找找吧。这里可以发现,在文件头中有个比较重要的字段 e_shoff 。他标识 “Start pf sectopm headers: *** 段表在文件中的偏移。” 有个这个偏移以后,我们就可以找到段表的位置。在通过e_shentsize 和 e_shnum可以知道段表描述符的到校和个数。这样就可以计算出每个段表描述符的位置信息和整个段表的大小。 有个这个,在去解读每个段的信息就比较容易了。

我这里描述的比较简单,当然还有另外一些详细的过程没有描述,例如:是怎么样根据段表字符串表来观察每个段的名字的?应该是通过段表中的段名字符串描述符得到段名字符串段在文件中的偏移以后,在 段表描述符中的sh_name字段来查找该段的名字在段表字符串段中的偏移。一些其他的过程有的时候,自己多想想,还是很有乐趣的。

最后引用书上的一句原话“由此我们可以得出结论,只有分析ELF文件头,就可以得到段表和段表字符串表的位置,从而解析整个ELF文件”。


符号表和重定位表

符号对于链接的作用举足轻重。没有符号,链接无法完成。符号就像目标文件的胶水一样。也想乐高玩具的卡槽。

对于链接时需要提供出去给链接器用的东东,无论是变量还是函数,在链接的过程都用符号做唯一的标识。为此,不同语言甚至同种语言的各个编译器,甚至于编译器的各个版本对于符号的解释规则都不完全相同,造成了ABI的不兼容。我就曾经碰见过由于编译器升级造成的ABI不兼容的问题。当然还不只是符号的问题。而C++被广为诟病的ABI不兼容问题还远不止于次。

对于符号的问题,曾经出过缺少“extern C”而没能找到C++描述规则的符号的问题,还碰到过编写的DLL由于没有导出符号导致链接不过的问题。对于符号的深刻教训还远不只这些。你肯定在编译链接过程中碰到过无法解析的符号 或者 找不到符号等等问题。WTF~

对于符号我们可以明确的是,符号是有大小的。不同于其他的符号,common块中的符号的真正大小要等到链接以后才可以决定。这样的变量(全局为初始化或者弱符号)就不适合放在bss段里。因为bss段需要在段表里面先划分空间,如果链接过程中发现编译单元内部的变量实际链接过后并不是编译期预想的那样的大小,那么bss段的大小就需要修改,这无疑是很麻烦的。而且重复的修改段的分布,得不偿失。

另外,重定位表对于链接过程也很重要。需要重定位的符号在重定位表中都有描述。包括重定位的类型,位置等等信息。对于地址修正方式,带入书上的公式以后可以发现,其实地址修正的方式在书中介绍的很简单。对于那个例子,shared和swap分别是绝对地址引用和相对地址引用。一个直接将绝对地址填入修正的位置,另外一个要将符号地址减去当前指令的地址,计算出一个偏移量再填入到修正位置中去。

链接过程

回头看看整个静态链接过程,流程也比较简单。先根据目标文件的每个段的信息来划分链接产物中每个段的大小。这里面书上介绍了两种方法,一种是直接组合,另外一种是相同的段合并以后再组合。在划分好段地址以后,在根据每个符号在段中的位置得出符号的绝对地址。再根据该绝对地址和重定位表,在目标文件中查找全局符号或者需要重定位的符号的地址和其他信息,最后在将计算出来的修正值填入链接产物中。最终得到地址修正过的链接产物,静态链接过程就完成了。