http://blog.endlesscode.com/2010/05/31/elf-file-structure-short-intro/
ELF文件结构简述
现在PC平台流行的可执行文件格式主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),它们都是COFF(Common File Format)格式的变种。目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和Linux下的.o),它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用一种格式存储。
Linux的.o/Windows的.obj、/bin/bash或Windows的exe、Linux的.so/Windows的.dll分别是什么文件?
- Linux的.o和Windows的.obj称为可重定位文件(Relocatable File),这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类。
- /bin/bash和Windows的.exe是可以直接执行的程序,它的代表就是ELF可执行文件,它们一般是没有扩展名的。
- Linux的.so和Windows的.dll称为共享目标文件,这种文件也包含了代码和数据,可以在以下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重位文件和共享目标文件链接、产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来执行。
ELF目标文件的格式是怎样?
ELF文件除了包括了上述图示中的机器指令代码、数据,还包括了链接时所需要的一些信息,比如符号表、调试信息、字符串等。这些信息按不同的属性,以“段“(Segment)的形式存储。程序源代码编译后的机器指令经常放在代码段(Code Section)里,代码段常见的名字有”.code”或”.text”;全局变量和局部静态变量经常放在数据段(Data Section),数据段一句名字叫为”.data”。而ELF文件的开头是一个“文件头”,它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是地动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息,文件头还包括一个段表的位置信息,段表在ELF文件中是一个描述文件中各个段的数组,包括各个段在文件的偏移位置,段的属性,段的名称等等。
程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段(.text)属于程序指令,而数据段(.data)和.bss段属于程序数据。 一般C语言的编译后执行语句都编译成机器代码,保存在.text段;已初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静 态变量一般放在.bss段,这些未初始化的全局变量和局部静态变量默认值都为0,放在.data段显示没有必要,因此放在了.bss段。所以.bss段只 是为未初始化的全局变量和局部静态变量预留位置,它并没有内容,所以它在文件中也不占据空间。
一般程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读可写的,而指令区域对于进程来是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。
常用的段名 | 说明 |
.rodata1 | Read Only Data,这种段里存放的是只读数据,比如字符串常量、全局const变量。跟”.rodata”一样 |
.comment | 存放的是编译器版本信息 |
.debug | 调试信息 |
.dynamic | 动态链接信息 |
.hash | 符号哈希表 |
.line | 调试时的行号表 |
.note | 额外的编译器信息。比如程序的公司名、发布版本号等 |
.strtab | String Table.字符串表 |
.symtab | Symbol Table.符号表 |
.shstrtab | Section String Table.段名表 |
.plt .got |
动态链接的跳转表和全局入口表 |
.init .fini |
程序初始化与终结代码段 |
其中,”.rodata”段存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。单独设立”.rodata”段有很多好 处,不光是在语义上支持了C++的const关键字,而且操作系统在加载的时候可以将”.rodata”段的属性映射成只读,这样对于这个段的任何修改操 作都会作为非法操作处理,保证程序的安全性。当然,有些编译器也会把字符串放到”.data”段,而不会单独放在”.rodata”段。
如C中的printf(“%d\nHello World!”)中的字符”%d\nHello World!”是存储在哪里的?
ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。如下图所示:
通过这种方法,在ELF文件中引用字符串只须给出一个数字下标即可,不用考虑字符串长度的问题。一般字符串表在ELF文件中也以段的形式保存,常见的段名为”.strtab“或”.shstrtab“。这两个字符串分别为字符串表(String Table)和段表字符串表(Section Header String Table)。顾名思义,字符串表用来保存普通的字符串,比如符号的名字;段表字符串表用来保存段表中用到的字符串,最常见的就是段名。
段表是结构是怎样?存储了哪些信息?
段表的结构其实就是一个数组,数组的元素是固定的结构,每个元素包括了各个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限、段的链接信息以及段的其他属性。其中描述段名的只是一个偏移,是段名字符串在”.shstrtab”段中的偏移。那么如何根据ELF文件头和段表来定位文件中的一个段呢?从ELF文件头中获取段表在文件中的偏移以及段表的大小,而从获取到了文件中的段表,再分析段表数组中的每个元素,得知段的偏移、段的长度和段的类型,就可以准确地定位一个段了。
字符串表和符号表有什么不同?
其实是完全不同的两个表,但是两个表又有关联。简单地说,字符串表就是记录ELF文件中的字符串常量,变量名等等。而符号表的存在意义是体现在多个目标文件进行链接的时候,在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用,而函数和变量可以统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。我们可以将符号看作是是链接中的粘合剂,整个链接过程就是基于符号才能够正确完成。在符号表”.symtab“中,其也是像段表的结构一样,是一个数组,每个数组元素是一个固定的结构来保存符号的相关信息,比如符号名(不是字符串,而是该符号名在字符串表的下标)、符号对应的值(可能是段中的偏移,也可能是符号的虚拟地址)、符号大小(数据类型的大小)等等。符号表中记录的一般是全局符号,比如全局变量、全局函数等等。
Visual Studio里面提示的Undefined Symbol,为什么是类似(?NewL@CHtmlControl@@SAPAV1@PAVCCoeControl@@@Z)这么诡异的symbol?
程序里面肯定是没有定义过”?NewL@CHtmlControl@@SAPAV1@PAVCCoeControl@@@Z”这么诡异的变量名的,但为什么变量名会变成这么诡异的?这是因为符号修饰的原因。符号修饰很大程度上是为了防止符号名冲突。比如复杂的C++拥有类、继承、虚拟机制、重载、名称空间等这些特性,如果两个函数func(int)和func(float)不进行符号修饰而直接存储变量名的话,然后只是单单根据函数名来判别的话,那么符号表就会出现2个一样的符号了。根据函数签名来修饰两个重载函数就不会出现这样的符号二义性的问题了。因为符号修饰的规则是各编译器厂商都不一样,因此再诡异也是没有什么出奇的。
如果两个目标文件定义了相同的全局变量?
在编辑过程中可能会遇到一种情况叫符号重复定义。这涉及到强符号(Strong Symbol)和弱符号(Weak Symbol)的定义。对于C/C++语言来说,编译默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。我们也可以通过GCC的”__attribute__((weak))”来定义任何一个强符号为弱符号,而用”extern int ext”这样的方式定义并非弱符号也非弱符号,因为”ext”只是一个外部变量的引用。针对强弱符号的概念,链接器一般会按如下规则选择全局符号:
- 不允许强符号被多次定义,否则链接器会报符号重复定义的错误。
- 如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
- 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。
预编译过程主要做了哪些处理?
- 将所有的”#define”删除,并且展开所有的宏定义。
- 处理所有条件预编译指令,比如”#if”, “#ifdef”, “#elif”, “#else”, “endif”。
- 处理”#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
- 删除所有的注释”//”和”/**/”。
- 添加行号和文件名标识,比如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
- 保留所有的#pragma编译器指令,因为编译器须要使用它们。