ELF文件中的“符号表(symbol table)”包含的是程序中的符号信息 – 这些符号代表的或许是定义(例如定义全局变量时使用的变量名,或者定义函数时使用的函数名),或许代表的是引用(例如使用关键字extern声明的变量或函数时使用的符号名称)。当代表的是定义时,在链接阶段链接器需要为它们重定位;当代表的是引用时,在链接阶段链接器需要在其他编译模块定位到该符号的定义。
符号表其实是所有符号信息的集合统称,即符号表是所有符号信息一起组成的一个数组,所以一个符号表索引(symbol table index) 对应该数组中的一个(符号表)表项。
其中,索引值0有双重含义:一般情况下代表的意思是指代符号表的第一个表项,然而一个未定义符号却也使用STN_UNDEF(数值为0)来指定其节头表索引值。
/* Symbol table entry. */
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
如下为readelf解出来的文本信息(展示理解),与表项存储数据类型不一定一致,请注意!!!
st_name成员其实不是一个字符串,而是一个数值,代表的是目标文件中 字符串表 中的一个索引值, 那里才真正存储着该符号的名称对应的字符串。如果st_name成员数值不为0,则代表该符号有符号名称。否则,说明该符号没有名称。
**注意:**在C程序中的 具有外部链接属性的符号 的名称 和 最后生成的目标文件中的符号表中的符号名称 是相同的,这点与C++不同。
st_value成员给出了相应的符号值。这个符号值具体是什么意思,是要依据上下文的(主要依据不同的符号属性和不同的目标文件),也许是个绝对值,也许是个地址值,等等。
在不同的目标文件中,对成员st_value的意义解释也是不同的。
(1)在可重定位文件中,若符号的st_shndx等于SHN_COMMON,则st_value的值代表的是该符号的对齐字节数。
(2)在可重定位文件中,若一个符号是已定义的,那么st_value的值代表的是该符号其所在的节中的偏移量 – 当然了,这个节是由st_shndx指定的。
(3)在可执行文件和共行库中,一个已定义符号的st_value的值不再是一个节偏移量,而是一个虚拟地址,因为动态链接器需要知道符号的内存地址。 – 因为虚拟地址是与节无关的,所以这种情况下,我们不需要关心st_shndx的值。
综合以上三点可知,在不同的目标文件中st_value的值代表的含义不同。这样设计是有原因的:在静态链接阶段,链接器需要的是符号在文件中的位置信息,而在程序运行时,动态链接器需要的是却符号在内存中的位置信息。
很多类型的符号都是有大小属性的。例如,一个数据对象的大小指的是它实际在目标文件中占的字节数。st_size成员如果是0的话,说明这个符号在目标文件中不占用任何字节数(例如common symbols)或者当前是未知大小的(例如undefined symbols)。
成员st_info指定了符号的类型(低四位)和绑定属性(高四位)。不同的符号类型/符号绑定属性以及意义将在下文列出。下面的宏分别展现了如何操作成员st_info。通过st_info得到类型和绑定属性,以及如何通过类型和绑定属性而得到st_info。
一个符号的绑定属性决定了该符号在链接阶段的可见性以及链接时的处理方式。
例如全局符号和本地符号的链接可见性是不同的,而当出现同名的全局符号和弱符号时,链接器会做出相应的处理(这点可参考下文对全局符号和弱符号不同点的描述)。
注:
对于 static 类型的成员,其 bind 信息必然是 STB_LOCAL,函数的 type 是 STT_FUNC,变量的 type 是 STT_OBJECT
对于本文件引用的外部符号,该符号通常是 GLOBAL 与 STT_NOTYPE,并且该符号所处于的节区的下标为特殊的 SHN_UNDEF,表明该符号是一个外部符号。
当一个符号的绑定属性是STB_LOCAL时,则表明该符号的链接属性是internal的,对其他目标文件来说是不可见的,即不可访问。所以不同的目标文件中的本地符号可以同名,它们彼此不会干扰对方。
当一个符号的绑定属性是STB_GLOBAL时,则表明该符号的链接属性是external的,对其他目标文件来说是可见的,即可以访问。
当一个符号的绑定属性是STB_WEAK时,则表明该符号是个弱符号,它和全局符号有类似的地方,即链接属性也是external的。但是链接器处理弱符号的优先级相对全局符号要低,即当全局符号和弱符号同名时,链接器最后使用全局符号而忽略弱符号。
全局符号和弱符号的区别主要在两个方面。
当链接器链接若干可重定位文件时,它是不允许具有STB_GLOBAL属性的符号以相同名字进行重复定义的。而如果一个已定义的全局符号存在,则即便另一个具有相同名字的弱符号存在也不会引起错误。链接器将认可全局符号的定义而忽略弱符号的定义。与此类似的,如果一个符号被放在COMMON块(就是说这个符号的st_shndx成员的值为SHN_COMMON),则一个同名的弱符号也不会引起错误。链接器同样认可放在COMMON块符号的定义而忽略其他的弱符号。
STB_GLOBAL > SHN_COMMON > STB_WEAK
在链接静态库的情况下:
(1)当链接器遇到一个未定义的全局符号(global symbol)时,链接器会去提取静态库,试图找到这个符号定义。在静态库中,这个符号可以是全局符号,也可以是弱符号。
(2)当链接器遇到一个未定义的弱符号(weak symbols)时,链接器是不会去提取静态库的,而是直接将该弱符号的值赋为0。(可以参考文章《Fun with weak symbols》。试验结果发现:如果引用了静态库中的非弱符号,那么即使链接器遇到了一个未定义的弱符号,依然会去静态库中解析符号)
注意: 弱符号在上述规则之外地方的行为是实现相关的。弱符号主要用于系统软件中,不推荐在应用程序中使用弱符号,因为在运行时,弱符号很容易被覆盖掉。
在符号表中,不同绑定属性的符号所在位置是不同的 – 所有的本地符号都被安放在符号表的前头,紧接着的才是全局符号和弱符号。前文提到过,一个符号表节对应的节头表项的节头表成员sh_info中的数值代表的是第一个绑定属性为非STB_LOCAL的符号的符号表索引值(即最后一个绑定属性为STB_LOCAL符号的符号表索引值加1)。
此包含范围内的值是为操作系统特定的语义保留的。
此包含范围内的值是为处理器特定的语义保留的。如果指定了含义,则处理器补充说明。
当符号类型是STT_NOTYPE时,表明该符号未指定类型或者当前还不知道该符号的类型。
当符号类型是STT_OBJECT时,表明该符号关联的实体是个数据对象,例如一个变量,数组等。
当符号类型是STT_FUNC时,表明该符号关联的实体是个函数或者其他的可执行代码。
当符号类型是STT_SECTION时,表明该符号关联的实体是个节。一般符号表中的一个符号是这个类型时,主要是用于重定位的目的,并且其绑定属性一般情况下是STB_LOCAL。
通常情况下,当一个符号的类型是STT_FILE时,这个符号的名称就是该目标文件相关联的源文件的名称。这种类型的符号的绑定属性是STB_LOCAL的,与它相关的节的节头表索引值为SHN_ABS,并且如果在符号表中存在此种符号的话,那么其位置排在本地符号(STB_LOCAL)的前头。
当符号类型是STT_COMMON时,表明该符号是个公用块数据对象,并且这个公用块在目标文件中实际是未被分配空间的。
当符号的类型是STT_TLS时,表明该符号对应变量存储在线程局部存储内。
st_other成员目前指定符号的可见性属性。不同的可见性属性值以及意义将在文章后续给出。下面的宏分别展示了在32位和64位机器上如何操作st_other。除了低两位,其余的位都为0并且没有意义。
尽管我们可以在编译阶段(通过在source code中或者编译器选项指定符号的可见性)和静态链接阶段(通过export list文件)指定符号的可见性属性,但其实可见性属性控制的是一个符号在运行时的解析行为。
当符号的可见性是STV_DEFAULT时,那么该符号的可见性由符号的绑定属性决定。这类情况下,(可执行文件和共享库中的)全局符号和弱符号默认是外部可访问的,本地符号默认外部是无法被访问的。但是,可见性是STV_DEFAULT的全局符号和弱符号是可被覆盖的。什么意思?举个最典型的例子,共享库中的可见性值为STV_DEFAULTD的全局符号和弱符号是可被可执行文件中的同名符号覆盖的。
注意:一个具体的实现可能会限制对外可访问的全局符号和弱符号的数量。
当符号的可见性是STV_PROTECTED时,它是外部可见的,这点跟可见性是STV_DEFAULT的一样,但不同的是它是不可覆盖的。这样的符号在共享库中比较常见。不可覆盖意味着如果是在该符号所在的共享库中访问这个符号,那么就一定是访问的这个符号,尽管可执行文件中也会存在同样名字的符号也不会被覆盖掉。
规定绑定属性为STB_LOCAL的符号的可见性不可以是STV_PROTECTED。
当符号的可见性是STV_HIDDEN时,证明该符号是外部无法访问的。这个属性主要用来控制共享库对外接口的数量。需要注意的是,一个可见性为STV_HIDDEN的数据对象,如果能获取到该符号的地址,那么依然是可以访问或者修改该数据对象的。
在可重定位文件中,如果一个符号的可见性是STV_HIDDEN的话,那么在链接生成可执行文件或者共享库的过程中,该符号要么被删除,要么绑定属性变成STB_LOCAL。
每一个符号表项所代表的特定符号信息都是和一个特定的 “节” 相关联的,st_shndx成员代表的就是这个特定“节”的节头表索引(比如一个定义的全局变量global,那么符号global的属性st_shndx值应该就是.data节所对应的节头表索引;定义的函数foo,那么符号foo的属性st_shndx值应该就是.text节所对应的节头表索引)。部分符号的节头表索引会有特殊的含义。
如果这个成员的值是SHN_XINDEX时,证明该符号关联的节的节头表索引值过大,超出了st_shndx所能代表的最大数值。那么真正的节头表索引值存储在一个类型为SHT_SYMTAB_SHNDX的扩展节中。