现在PC平台流行的可执行文件格式主要是Windows下的PE(Portable Executable)和Linux下的ELF(Executable Linkable Format),它们都是COFF(Common file format)格式的变种。目标文件就是源代码编译后但未链接的那些中间文件,它跟可执行文件的内容与结构很相似,一般跟可执行文件格式采用同一种格式存储
动态链接库、静态链接库文件都按照可执行文件格式存储。
- 程序被装载后,数据和指令分别被映射到两个虚存区域。数据区域对于进程来说是可读写的;指令区域对于进程来说只只读的,这两个虚存区域的权限可以被分别设置成可读写和只读。
- 当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只须保存一份该程序的指令部分。每个副本进程的数据区域是不一样的,它们是进程私有的。
除此之外,程序员可以指定变量所处的段:
$ readelf -h ***.o
用于查看文件头信息
ELF的文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表位置和长度及段的数量等
ELF文件有3种类型,ET_REL(1)可重定位文件、ET_EXEC(2)可执行文件、ET_DYN(3)共享目标文件
$ readelf -S ***.o
用于查看段表信息
段表是ELF头文件以外最重要的结构,它描述了ELF各个段的信息,比如每个段的段名、长度、在文件中的偏移、读写权限及段的其他属性;ELF文件的段结构就是由段表决定的,编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性的。段表在ELF文件中的位置有ELF头文件的e_shoff成员决定。段表是一个段描述符的数组。
段表[2](.rel.text)是一个重定位表,链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,即代码段和数据段中那些绝对地址引用的位置。这些重定位信息都记录在ELF文件的重定位表里,对于每个需要重定位的代码段或数据段,都会有一个相应的重定位表。
ELF文件中用到了很多字符串,比如段名、变量名,常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。通过这种方法,在ELF文件中引用字符串只需给出一个数字下标即可,不用考虑字符串的长度。一般字符串表在ELF文件中也以段的形式保存,常见的段名为".strtab"或".shstrtab"。这两个字符串表分别为字符串表和段表字符串表;字符串表用来保存普通的字符串,段表字符串用来保存段表中用到的字符串,常见的是段名。文件头中最后一项就是指示段表字符串表在段表中的位置
在链接中,函数和变量统称为符号,变量名和函数名就是符号名。每一个目标文件都会有一个相应的符号表,这个表里面记录了目标文件中所用到的所有符号,对于变量和函数来说,符号值就是它们的地址。符号表中的符号有以下几种类型:
$ nm SimpleSection.o
用于查看符号信息
ELF文件中的符号表往往是文件中的一个段,段名一般叫".symtab",符号表的结构简单,是一个Elf32_Sym结构的数组,每个这种结构对应一个符号,该结构定义如下:
目标文件中定义一个全局变量,并且将它初始化,我们把这种符号的定义称为
强符号;未初始化的全局变量为
弱符号,它们都是针对定义来说的。我们也可以通过
__attribute__((weak))
来定义任何一个强符号为弱符号。针对强弱符号的概念,链接器会按照如下规则处理与选择被多次定义的全局符号:
目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们需要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用。与之对应的还有一种弱引用,在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议,若未被定义,则链接器对于该引用不会报错。一般对于未定义的弱引用,链接器默认其为0。在GCC中我们可以通过使用
__attribute__((weakref))
这个扩展关键字来声明一个符号引用为弱引用。比如下面这段代码:
这种弱符号和弱引用对于库来说十分有用,库中定义的弱符号可以被用户所定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接到一起的时候,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序就可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。
__attribute__((section("FOO"))) int global = 42;
__attribute__((section("BAR"))) void foo(){}
我们在全局或者函数之前加上
__attribute__((section("name"))
属性就可以把相应的变量或函数放到以"name"作为段名的段中。
- 定义在本目标文件的全局符号,可以被其他目标文件引用
- 在本目标文件中引用的全局符号,却没有定义在本目标文件中,这一般叫做外部符号
- 段名,这种符号往往有编译器产生,它的值就是该段的起始地址
- 局部符号,这类符号只在编译单元内部可见
- 行号信息
typedef struct{
Elf32_Word st_name; //name
Elf32_Addr st_value; //value
Elf32_Word st_size; //size
unsigned char st_info; // type and Symbol binding
unsigned char st_other; //
Elf32_Half st_shndx; //the index of section which the symbol in
} Elf32_Sym;
- 不允许强符号被多次定义,否则,链接器报符号重复定义错误
- 如果一个符号在某个目标文件中是强符号,其他文件中都是弱符号,那么链接器选择强符号
- 如果一个符号在所有目标文件中都是弱符号,那么链接器选择其中占用空间最大的一个
__attribute__((weakref)) void foo();
int main()
{foo();}
我们可以将它编译成一个可执行文件,GCC并不会报连接错误。但是当我们运行这个可执行文件时,会发生运行错误。因为当main函数试图调用foo函数时,foo函数地址为0,于是发生非法地址访问的错误。一个改进的例子是:
__attribute__((weakref)) void foo();
int main()
{if(foo) foo();}