链接的接口——符号
符号
链接过程的本质就是要把多个不同的目标文件之间像拼图一样拼起来,这些目标文件必须有像拼图那样的凹凸部分才能够粘合。
情景:目标文件B要用到了目标文件A中的函数“foo”。
那么就称目标文件A定义(Define)了函数“foo”,目标文件B引用(Reference)了目标文件A中的函数“foo”。
在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)
每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里记录了目标文件中所用到的所有符号。
每个定义的符号有一个对应的值叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。
符号包括以下几类:
- 定义在本目标文件的全局符号,可以被其他目标文件引用。
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(External Symbol),也就是符号引用。
- 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。
- 局部符号,这类符号值在编译单元内部可见。
- 行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。
ELF符号表结构
ELF文件中的符号表往往是文件中的一个段,段名一般叫“.symtab”。它是一个Elf32_Sym结构的数组,每个Elf32_Sym结构对应一个符号。
typedef struct{
Elf32_Word st_name; // 符号名。这个成员包含了该符号名在字符串表中的下标
Elf32_Addr st_value; // 符号相对应的值
Elf32_Word st_size; // 符号大小
unsigned char st_info; // 符号类型(低4位)和绑定信息(高28位)
unsigned char st_other; // 该成员目前为0,没用
Elf32_Half st_shndx; // 符号所在的段
}Elf32_Sym;
符号绑定信息
宏定义名 | 值 | 说明 |
---|---|---|
STB_LOCAL | 0 | 局部符号,对于目标文件的外部不可见 |
STB_GLOBAL | 1 | 全局符号,外部可见 |
STB_WEAK | 2 | 弱引用 |
符号类型
宏定义名 | 值 | 说明 |
---|---|---|
STT_NOTYPE | 0 | 未知类型符号 |
STT_OBJECT | 1 | 该符号是个数据对象,比如变量、数组等 |
STT_FUNC | 2 | 该符号是个函数或其他可执行代码 |
STT_SECTION | 3 | 该符号表示一个段,这种符号必须是STB_LOCAL的 |
STT_FILE | 4 | 该符号表示文件名,一般都是该目标文件所对应的源文件名,它一定是STB_LOCAL类型的,并且它的st_shndx一定是SHN_ABS |
符号所在段
如果符号定义在本目标文件中,那么这个成员表示符号所在的段在段表中的下标;
但是如果符号不是定义在本目标文件中,或者对于有些特殊符号,sh_shndx的值有些特殊。
宏定义名 | 值 | 说明 |
---|---|---|
SHN_ABS | 0xfff1 | 表示该符号包含了一个绝对的值,比如表示文件名的符号就属于这种类型的 |
SHN_COMMON | 0xfff2 | 表示该符号是一个COMMON块类型的符号,一般来说,未初始化的全局符号定义就是这种类型的 |
SHN_UNDEF | 0 | 表示该符号未定义。这个符号表示该符号在本目标文件被引用到,但是定义在其他目标文件中 |
符号值
- 在目标文件中,如果是符号的定义并且该符号不是COMMON块类型的,则st_value表示该符号在段中的偏移。
- 在目标文件中,如果符号是COMMON块类型的,则st_value表示该符号的对齐属性。
- 在可执行文件中,st_value表示符号的虚拟地址。
特殊符号
当我们使用ld作为链接器来链接生产可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但是你可以直接声明并且引用它。
- __executable_start,该符号为程序起始地址,注意,不是入口地址,是程序的最开始的地址。
- __etext或_etext或etext,该符号位代码段结束地址,即代码段最末尾的地址。
- _edata或edata,该符号位数据段结束地址,即数据段最末尾的地址。
- _end或end,该符号为程序结束地址
- 以上地址都为程序被装载时的虚拟地址
符号修饰与函数签名
C++增加了名称空间(NameSpace)的方法来解决多模块的符号冲突问题。
众所周知,强大而又复杂的C++拥有类、继承、虚机制、重载、名称空间等这些特性,它们使得符号管理更为复杂。
为了支持C++这些复杂的特性,发明了符号修饰(Name Decoration)或符号改编(Name Mangling)的机制。
函数签名(Function Signature)包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间即其他信息。
在编译器及链接器处理符号时,它们采用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称(Decorated Name)
extern “C”
C++为了与C兼容,在符号的管理上,C++有一个用来声明或定义一个C的符号的extern “C”关键字用法。
C++编译器会将在extern “C”的大括号内部的代码当做C语言代码处理。所以C++的名称修饰机制将不会起作用。
弱符号与强符号
对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号(Strong Symbol),未初始化的全局变量为弱符号(Weak Symbol)。
我们也可以通过GCC的__attrtibute__((weak))来定义任何一个强符号为弱符号。
针对强弱符号的概念,链接器就会按如下规则处理与选择被多次定义的全局符号:
- 规则1:不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,则链接器报符号重复定义错误。
- 规则2:如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
- 规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。
弱引用和强引用
目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用(Strong Reference);
在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。
这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些拓展功能模块的引用定义为弱引用,当我们将拓展模块与程序链接在一起时,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。