持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情
这一系列是《程序员的自我修养》的阅读笔记:
程序员的自我修养之链接的符号
前言
之前我们聊到一个C语言程序到一个可执行文件会经过预编译、编译、汇编、链接四个过程,链接过程的本质就是要把多个不同的目标文件之间相互“粘”到一起,为了使不同目标文件之间能够相互粘合,这些目标文件之间必须有固定的规则才行。在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。
在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。
链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(SymbolTable),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(SymbolValue)。
符号的类型
-
函数和变量
-
全局符号
-
段名
-
行号信息
-
局部符号
$ nm hello.o
0000000000000000 T main
U puts
0000000000000000 b static_var2.2361
0000000000000000 d static_var.2360
其中,符号类型的表示的对应关系如下:
A Global absolute 符号。
a Local absolute 符号。
B Global bss 符号。
b Local bss 符号。
D Global data 符号。
d Local data 符号。
f 源文件名称符号。
T Global text 符号。
t Local text 符号。
U 未定义符号。
ELF符号表结构
ELF文件中的符号表往往是文件中的一个段,段名一般叫“.symtab”。符号表的结构很简单,它是一个Elf64_Sym结构(64位ELF文件)的数组,每个Elf64_Sym结构对应一个符号。这个数组的第一个元素,也就是下标0的元素为无效的“未定义”符号。Elf64_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;
- 符号所在段
如果符号定义在本目标文件中,那么这个成员表示符号所在的段在段表中的下标;但是如果符号不是定义在本目标文件中,或者对于有些特殊符号,sh_shndx的值有些特殊,如下表所示:
| 宏定义名 | 值 | 说明 |
|---|---|---|
| SHN_ABS | 0xfff1 | 表示该符号包含了一个绝对的值。比如表示文件名的符号就属于这种类型的 |
| SHN_COMMON | 0xfff2 | 表示该符号是一个"common块"类型的符号,一般来说,未初始化的全局符号定义就是这种类型的 |
| SHN_UNDFF | 0 | 表示该符号未定义。表示该符号在本目标文件被引用到,但是定义在其他目标文件中 |
- 符号值
在目标文件中,如果是符号的定义并且该符号不是“COMMON块”类型的(即st_shndx不为SHN_COMMON,“COMMON块”之后会讲),则st_value表示该符号在段中的偏移。即符号所对应的函数或变量位于由st_shndx指定的段,偏移st_value的位置。
如果符号是“COMMON块”类型的(即st_shndx为SHN_COMMON),则st_value表示该符号的对齐属性。
在可执行文件中,st_value表示符号的虚拟地址。这个虚拟地址对于动态链接器来说十分有用。
- 符号类型与绑定信息
该成员低4位表示符号的类型(Symbol Type),高28位表示符号绑定信息(Symbol Binding)。
ELF符号表结构实例
根据上面的介绍,我们对ELF文件的符号表有了大致的了解,接着将以hello.o里面的符号为例子,分析各个符号在符号表中的状态。使用readelf工具来查看ELF文件的符号。
$ readelf -s hello.o
Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.2361
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 3 static_var.2360
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 32 FUNC GLOBAL DEFAULT 1 main
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
readelf 的输出格式与上面描述的 Elf64_Sym 的各个成员几乎一一对应,第一列 Num 表示符号表数组的下标,从0开始,共13个符号;第二列 Value 就是符号值,即 st_value ;第三列 Size 为符号大小,即 st_size ;第四列和第五列分别为符号类型和绑定信息,即对应 st_info 的低4位和高28位;第六列 Vis 目前在C/C++语言中未使用,我们可以暂时忽略它;第七列 Ndx 即 st_shndx ,表示该符号所属的段;当然最后一列也最明显,即符号名称。
main 函数是定义在 hello.c 里面的,所在的位置都为代码段,所以 Ndx 为1,即hello.o里面,.text段的下标为1。这一点可以通过 readelf –a 或 objdump–x 得到验证,也可以回顾下 程序员的自我修养之ELF文件格式 。因为是函数,所以类型是 STT_FUNC;全局可见的,所以是 STB_GLOBAL ;Size 表示函数指令所占的字节数;Value表示函数相对于代码段起始位置的偏移量。
static_var.2360和 static_var2.2361 是两个静态变量,它们的绑定属性是 STB_LOCAL ,即只是编译单元内部可见。
对于那些 STT_SECTION 类型的符号,它们表示下标为 Ndx 的段的段名。它们的符号名没有显示,其实它们的符号名即它们的段名。比如2号符号的 Ndx 为1,那么它即表示 .text 段的段名,该符号的符号名应该就是“.text”。
“hello.c”这个符号表示编译单元的源文件名。
“puts” 符号是导入的 stdio.h 库中定义的。该符号在SimpleSection.c里面被引用,但是没有被定义。所以它的Ndx是SHN_UNDEF。