正如我们前文所提到的,符号是链接过程的粘合剂,它为众多目标文件提供接口,使得众多目标文件能够正确的链接在一起。因此,本文我们将详细介绍符号表的内容已经符号在链接过程中的作用。
1. ELF符号表结构
目标文件中的符号表其实也是一个段,段名往往叫作“.symtab”,类型为SYMTAB,符号表也是一个结构体数组,每个符号的结构体如下所示,它包含了该符号的符号名,所在段的索引,该符号的值,该符号的长度等等信息。
符号绑定属性分为STB_LOCAL,STB_GLOBAL,STB_WEAK。其中STB_LOCAL表示该符号是局部符号,对目标文件的外部不可见;STB_GLOBAL表示该符号为全局符号,外部可见;STB_WEAK表示该符号为若符号,即当两个以上的强符号的符号名相同时,在链接中会报符号重复定义的错误,但是若符号和强符号的名称相同时,链接器会选择强符号的定义,而不会报错。
符号的类型包括STT_NOTYPE(未知类型的符号),STT_OBJECT(数据对象符号,比如全局变量),STT_FUNC(可执行符号,比如函数名),STT_SECTION(段符号,表示该符号是一个段),STT_FILE(文件符号,表示文件名)。
在linux使用readelf执行如下命令 readelf -s SimpleSection.o,显示如下:
可以看到,第0个符号是未定义的,类型未知的空符号;SimpleSection.c是该源文件的文件名,所以符号类型是FILE;有七个符号名为空的段符号;还有七个符号是我们代码中定义的变量,或者函数。
其中有个很有意思的现象,那就是我们每个人都知道printf是一个函数啊,为什么这个符号的类型却是“NOTYPE”呢?其实答案也很简单,因为printf这个符号并不是在这个源文件中定义的,因此在对这个源文件进行编译的时候,编译器只知道这是一个外部定义的符号,这个源文件对外部定义的符号进行引用。至于这个符号具体的类型和大小,需要在链接时找到定义这个符号的目标文件时,才能确认。我们也可以看到printf这个符号的段索引时未定义(UND)。
2. 符号的定义和引用
除了特殊的诸如文件符号和段符号之外,一个目标文件中的符号要么是源文件定义的符号,要么是源文件中引用其他源文件定义的符号。那么源文件中怎样算是定义了一个符号,怎样是引用了其他的符号呢?
源文件自己定义的符号就是指代码中自己定义的全局或者静态局部变量,自己定义的函数。而源文件引用的符号是指该源文件中使用extern修饰的函数或者变量,或者包含在头文件中的函数。
C++和C语言在函数的符号定义上,存在一定的区别。C语言中,符号名就是使用的函数名称,因此如果两个函数的函数名相同,则在链接中会产生符号重复定义的错误。而C++中,符号名使用的是函数签名,函数签名包含了一个函数的函数名,调用参数个数,类型等等,因此C++代码中,两个同名的函数,调用参数的个数或者类型不同,编译链接也是合法的,这便是C++支持函数重载的原因。
C++为了与C兼容,支持一个 extern "C" 的语法,它的意思是定义或引用在 extern "C" 内部的符号将使用C规则修饰。例如下述代码中的函数func和变量var,他们的符号名的定义使用C语法,所以函数重载功能失效。
3. 特殊符号
当我们使用ld作为链接器来链接生成可执行文件时,它会定义很多特殊的符号,这些符号不在我们的程序中定义,但是你可以直接声明并且使用它们。比如下述特殊符号,即可直接如下述代码一样使用:
(1). _executable_start,该符号为程序的起始地址
(2). _etext, 该符号为代码段的结束地址
(3). _edata, 该符号为数据段的结束地址
(4). _end, 该符号为程序结束地址
#include <stdio.h>
extern char _end[];
int main()
{
printf("Executable End %x \n", _end);
return 0;
}
4.强符号与弱符号
上文中我们提到过,当多个强符号重名时,连接过程会发生符号重定义错误,而强符号和弱符号重名时,链接器会选择强符号的定义。通常情况下,编译器默认函数和初始化的全局变量为强符号,而未初始化的全局变量为弱符号,除此以外,我们也可以显示的通过" __attribute__((weak))"来指定弱符号的定义。
int weak;
int strong = 1;
__attribute__((weak)) weak2 = 2;
int main()
{
return 0;
}
上述代码中,weak和weak2是弱符号,strong和main函数是强符号。
弱符号的主要使用场景是共享库,当库中定义的符号是弱符号时,一旦该符号的符号名与用户定义的符号名重名时,程序可以使用用户定义的符号来执行程序,而不至于发生链接失败的错误。
介绍过我们目标文件中各个段和目标文件中的符号后,那么链接器是如何利用符号把各个目标文件正确的链接在一起呢?下问我们将会对静态链接过程进行讲解和阐述。