GCC编译器原理(二)------编译原理一:ELF文件(3)
4.5 String Table:字符串表
字符串表节区包含以 NULL( ASCII 码 0) 结尾的字符序列, 通常称为字符串。 ELF 目标文件通常使用字符串来表示符号和节区名称。 对字符串的引用通常以字符串在字符串表中的下标给出。
一般, 第一个字节(索引为 0)定义为一个空字符串。类似的,字符串表的最后一个字节也定义为 NULL,以确保所有的字符串都以 NULL 结尾。索引为 0 的字符串在不同的上下文中可以表示无名或者名字为 NULL 的字符串。
允许存在空的字符串表节区,其节区头部的 sh_size 成员应该为 0。对空的字符串表而言,非 0 的索引值是非法的。
例如:对于各个节区而言,节区头部的 sh_name 成员包含其对应的节区头部字符串表节区的索引,此节区由 ELF 头的 e_shstrndx 成员给出。下图给出了包含 25 个字节的一个字符串表,以及与不同索引相关的字符串。
上表中包含的字符串如下:
-
在使用、分析字符串表时,要注意以下几点:
- 字符串表索引可以引用节区中任意字节。
- 字符串可以出现多次
- 可以存在对子字符串的引用
- 同一个字符串可以被引用多次。
- 字符串表中也可以存在未引用的字符串。
4.6 Symbol Table:符号表
目标文件的符号表中包含用来定位、 重定位程序中符号定义和引用的信息。 符号表索引是对此数组的索引。索引 0 表示表中的第一表项,同时也作为未定义符号的索引(即 STN_UNDEF)。
目标文件中的符号表通常是文件中的一个段,段名一般叫做".symtab"。符号表的结构是 Elf32_sym,每个 Elf32_sym 结构对应一个符号,最后组成一个结构体数组。
1 typedef struct { 2 Elf32_Word st_name; /* 符号名 */ 3 Elf32_Addr st_value; /* 符号相对应的值 */ 4 Elf32_Word st_size; /* 符号大小 */ 5 unsigned char st_info; /* 符号类型和绑定信息 */ 6 unsigned char st_other; /* 未使用 */ 7 Elf32_Half st_shndx; /* 符号所在的段 */ 8 } Elf32_sym;
各个字段的含义如下:
字段 |
说明 |
st_name |
包含目标文件符号字符串表的索引, 其中包含符号名的字符串表示。 如果该值非 0, 则它表示了给出符号名的字符串表索引, 否则符号表项没有名称。 注:外部 C 符号在 C 语言和目标文件的符号表中具有相同的名称。 |
st_value |
此成员给出相关联的符号的取值。依赖于具体的上下文,它可能是一个绝对值、一个地址等等。 |
st_size |
很多符号具有相关的尺寸大小。 例如一个数据对象的大小是对象中包含的字节数。如果符号没有大小或者大小未知,则此成员为 0。 |
st_info |
此成员给出符号的类型和绑定属性。 下面给出若干取值和含义的绑定关系。 |
st_other |
该成员当前包含 0,其含义没有定义。 |
st_shndx |
每个符号表项都以和其他节区间的关系的方式给出定义。此成员给出相关的节区头部表索引。某些索引具有特殊含义。 |
4.6.1 st_info符号类型和绑定信息
该成员的低 4 位表示符号的类型(Symbol Type),高 28 位表示符号绑定信息(Symbol Binding),符号表项 st_info 字段合成如下:
/usr/include/elf.h
低 4 位表示符号绑定,用于确定链接可见性和行为,具体绑定类型如下:
名称 |
取值 |
说明 |
STB_LOCAL |
0 |
局部符号在包含该符号定义的目标文件以外不可见。 相同名称的局部符号可以存在于多个文件中,互不影响。 |
STB_GLOBAL |
1 |
全局符号对所有将组合的目标文件都是可见的。一个文件中对某个全局符号的定义将满足另一个文件对相同全局符号的未定义引用。 |
STB_WEAK |
2 |
弱符号与全局符号类似,不过他们的定义优先级比较低。 |
STB_LOPROC |
13 |
处于这个范围的取值是保留给处理器专用语义的。 |
STB_HIPROC |
15 |
在每个符号表中,所有具有 STB_LOCAL 绑定的符号都优先于弱符号和全局符号。符号表节区中的 sh_info 头部成员包含第一个非局部符号的符号表索引。
符号类型( ELF32_ST_TYPE)定义如下:
名称 |
取值 |
说明 |
STT_NOTYPE |
0 |
符号的类型没有指定 |
STT_OBJECT |
1 |
符号与某个数据对象相关,比如一个变量、数组等等 |
STT_FUNC |
2 |
符号与某个函数或者其他可执行代码相关 |
STT_SECTION |
3 |
符号与某个节区相关。 这种类型的符号表项主要用于重定位,通常具有 STB_LOCAL 绑定。 |
STT_FILE |
4 |
传统上, 符号的名称给出了与目标文件相关的源文件的名称。文件符号具有 STB_LOCAL 绑定,其节区索引是 SHN_ABS, 并且它优先于文件的其他 STB_LOCAL 符号(如果有的话) |
STT_LOPROC |
13 |
此范围的符号类型值保留给处理器专用语义用途。 |
STT_HIPROC |
15 |
在共享目标文件中的函数符号(类型为 STT_FUNC)具有特别的重要性。当其他目标文件引用了来自某个共享目标中的函数时, 链接编辑器自动为所引用的符号创建过程链接表项。类型不是 STT_FUNC 的共享目标符号不会自动通过过程链接表进行引用。
如果一个符号的取值引用了某个节区中的特定位置,那么它的节区索引成员(st_shndx)包含了其在节区头部表中的索引。当节区在重定位过程中被移动时,符号的取值也会随之变化,对符号的引用始终会 "指向" 程序中的相同位置。
【1】弱符号与强符号
在编程中经常碰到一种情况叫符号重复定义。多个目标文件中含有相同名字全局符号的定义,在这些目标文件链接的时候将会出现符号重复定义的错误。
强符号(Strong Symbol):对于C/C++ 语言来说,编译器默认函数和初始化了的全局变量为强符号
弱符号(Weak Symbol):未初始化的全局变量为弱符号
可以通过 GCC 的 "__attribute__((weak))" 来定义任何一个强符号为弱符号。需要注意的是,强符号和弱符号都是针对定义来说的,不是针对符号引用。
-
链接器处理强弱符号的规则如下:
- 规则1:不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,则链接器报符号重复定义错误
- 规则2:如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号
- 规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个
【2】弱引用和强引用
强引用(Strong Reference):我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们必须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种就称为强引用。
弱引用(Weak Reference):在处理弱引用的时候,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未定义,则链接器对于该引用不报错。
弱引用和强引用主要用于库的链接过程。
在 GCC 中,可以通过符号 "__attribute__((weakref))" 这个扩展关键字来声明对一个外部函数的引用为弱引用。例如:
1 __attribute__ ((weakref)) void foo(); 2 3 int main() 4 { 5 if(foo) foo(); 6 }
4.6.2 st_shndx:节区索引
-
某些特殊的节区索引具有不同的语义:
- SHN_ABS:符号具有绝对取值,不会因为重定位而发生变化。
- SHN_COMMON:符号标注了一个尚未分配的公共块。符号的取值给出了对齐约束,与节区的 sh_addralign 成员类似。就是说,链接编辑器将为符号分配存储空间,地址位于 st_value 的倍数处。 符号的大小给出了所需要的字节数。
- SHN_UNDEF: 此节区表索引值意味着符号没有定义。当链接编辑器将此目标文件与其他定义了该符号的目标文件进行组合时, 此文件中对该符号的引用将被链接到实际定义的位置。
4.6.3 STN_UNDEF 符号
符号表中下标为 0(STN_UNDEF)的表项被保留。其中包含如下数值:
名称 |
取值 |
说明 |
st_name |
0 |
无名称 |
st_value |
0 |
0 值 |
st_size |
0 |
无大小 |
st_info |
0 |
无类型,局部绑定 |
st_other |
0 |
无附加信息 |
st_shndx |
0 |
无节区 |
4.6.4 st_value:符号取值
-
不同的目标文件类型中符号表项对 st_value 成员具有不同的解释:
- 在可重定位文件中, st_value 中遵从了节区索引为 SHN_COMMON 的符号的对齐约束。
- 在可重定位的文件中, st_value 中包含已定义符号的节区偏移。 就是说,st_value 是从 st_shndx 所标识的节区头部开始计算,到符号位置的偏移。
- 在可执行和共享目标文件中, st_value 包含一个虚地址。为了使得这些文件的符号对动态链接器更有用,节区偏移( 针对文 件的解释)让位于虚拟地址(针对内存的解释),因为这时与节区号无关。
尽管符号表取值在不同的目标文件中具有相似的含义, 适当的程序可以采取高效的数据访问方式。
4.7 重定位信息
重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。
4.7.1 重定位表
成员 |
说明 |
r_offset |
此成员给出了重定位动作所适用的位置。对于一个可重定位文件而言,此值是从节区头部开始到将被重定位影响的存储单位之间的字节偏移。对于可执行文件或者共享目标文件而言, 其取值是被重定位影响到的存储单元的虚拟地址。 |
r_info |
此成员给出要进行重定位的符号表索引, 以及将实施的重定位类型。 例如一个调用指令的重定位项将包含被调用函数的符号表索引。 如果索引是 STN_UNDEF, 那么重定位使用 0 作为"符号值"。重定位类型是和处理器相关的。当程序代码引用一个重定位项的重定位类型或者符号表索引, 则表示对表项的 r_info 成员应用 ELF32_R_TYPE 或者 ELF32_R_SYM 的结果。 #define ELF32_R_SYM(i) ((i)>>8) #define ELF32_R_TYPE(i)((unsigned char)(i)) #define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t) |
r_addend |
此成员给出一个常量补齐, 用来计算将被填充到可重定位字段的数值。 |
如上所述,只有 Elf32_Rela 项目可以明确包含补齐信息。类型为 Elf32_Rel 的表项在将被修改的位置保存隐式的补齐信息。依赖于处理器体系结构,各种形式都可能存在, 甚至是必需的。 因此, 对特定机器的实现可以仅使用一种形式, 也可以根据上下文使用不同的形式。
重定位节区会引用两个其它节区:符号表、要修改的节区。节区头部的 sh_info 和 sh_link 成员给出这些关系。不同目标文件的重定位表项对 r_offset 成员具有略微不同的解释。
- 在可重定位文件中, r_offset 中包含节区偏移。就是说重定位节区自身描述了如何修改文件中的其他节区;重定位偏移 指定了被修改节区中的一个存储单元。
- 在可执行文件和共享的目标文件中, r_offset 中包含一个虚拟地址。为了使得这些文件的重定位表项对动态链接器更为有用,节区偏移(针对文件的解释)让位于虚地址(针对内存的解释)。
尽管对 r_offset 的解释会有少许不同,重定位类型的含义始终不变。
4.7.2 重定位类型
重定位表项描述如何修改后面的指令和数据字段。一般,共享目标文件在创建时,其基本虚拟地址是 0,不过执行地址将随着动态加载而发生变化。
-
重定位的过程,按照如下标记:
- A 用来计算可重定位字段的取值的补齐。
- B 共享目标在执行过程中被加载到内存中的位置(基地址)。
- G 在执行过程中, 重定位项的符号的地址所处的位置 —— 全局偏移表的索引。
- GOT 全局偏移表( GOT)的地址。
- L 某个符号的过程链接表项的位置(节区偏移/地址)。过程链接表项把函数调用重定位到正确的目标位置。链接编辑器构造初始的过程链接表,动态链接器在执行过程中修改这些项目。
- P 存储单位被重定位(用 r_offset 计算) 到的位置(节区偏移或者地址)。
- S 其索引位于重定位项中的符号的取值。
重定位项的 r_offset 取值给定受影响的存储单位的第一个字节的偏移或者虚拟地址。重定位类型给出那些位需要修改以及如何计算它们的取值。