编译与链接 - 符号
前一篇{% include link.html uid=20231552 %}中,主要介绍了 ELF 文件的结构,我们知道 ELF 文件中包含多个 section 和 segment,但这仅仅是一个开始。后续的文章中,我将深入到不同的 section 和 segment 中,介绍诸如符号表、静态链接、动态链接等概念。
符号
在编写程序时,我们会给变量、函数命令,并在代码中使用名称来访问它们,这些名称就是符号。程序编译后,程序中使用的变量会被转换为内存地址或者偏移量。但这不意味着这些变量名称没用了,编译器生成机器代码的同时还导出了符号。这些符号在链接与调试的时候会有大用途,符号则存储在 ELF 文件中的某些 section 中。
链接器用来整合多个目标文件,并构造可执行程序或者动态库,对其他目标文件中符号的引用使用的正是符号的名称。在链接阶段,链接器需要基于名称来定位到符号的地址或者偏移量。如果没有符号信息,链接器将无能为力。在使用调试器的时候,我们通过名称来查看一个变量的内容,如果没有符号,我们就需要知道变量在内存中的地址,这显然是很困难的。在使用动态库的时候,我们通过名称来获取变量或函数的地址。机器在运行编译后的机器指令时并不需要符号的名称信息,但符号的名称不可或缺。
符号定义
在 ELF 文件中,每个符号使用如下结构体表示:
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;
该结构的各个字段说明如下:
st_name
一个整数,指向字符串表中该符号的名称的起始位置
st_info
包含符号的 bind 类型和符号类型,可以使用如下两个宏来获取 bind 和 type 信息
ELF64_ST_BIND(info) ((info) >> 4)
ELF64_ST_TYPE(info) ((info) & 0xf)
常见的符号类型如下:
- STT_NOTYPE: 符号类型未知
- STT_OBJECT: 该符号为变量
- STT_FUNC: 该符号为函数
常见的符号绑定类型有如下几种:
- STB_LOCAL: 该符号局部可见,只能被当前目标文件看到
- STB_GLOBAL: 全局符号,可以被其他目标文件看到
- STB_WEAK: 一个全局符号,但是可以被覆盖
st_other
包含符号的可见性(visibility)属性,当目标文件被链接为可执行文件或动态链接库后,符号的可见性用于控制该符号是否能被其他目标文件中访问到。常见的可见性类别如下:
- STV_DEFAULT: 符号的可见性由 bind 类型控制,bind 类型为 STB_GLOBAL 和 STB_WEAK 类型的则能被其他目标文件访问到,而 bind 属性为 STB_LOCAL 的则不行。
- STV_PROTECTED: 符号可被其他目标文件访问到,但是不能被覆盖。
- STV_HIDDEN: 该符号只能在当前目标文件中被访问到,可见性为 STV_HIDDEN 的变量,在链接阶段会被删除或者转为一个绑定类型为 STB_LOCAL 的变量。
符号的绑定类型和可见性有点让人迷惑。绑定类型用于在链接阶段控制符号能否被其他目标文件访问到,比如我们将变量使用 static 修饰,该变量的绑定类型就是 STB_LOCAL,它就只能在当前目标文件中被访问。
符号的可见性则是在运行时控制当前的动态库中的符号是否能被其他动态库看到。在实现动态库的时候,一般会有多个 .c 文件,编译后有多个 .o 目标文件,多个目标文件中会涉及到符号的相互访问,因此把符号都设置为 static 类型是不现实的,需要在其他目标文件中使用的符号的绑定类型需要被设置 STB_GLOBAL。当动态库构造完成后,希望该动态库中的符号可以隐藏起来,只能在本动态库中被访问到,这个时候就可以设置符号的可见性,控制符号是否能被其他动态库或者当前加载动态库的程序看到。
st_shndx
包含一个 index,可以通过 ELF 中的 section header table 定位到对应的 section,表示此符号定义在那个 section 中。
st_value
符号的值,解读方式与 ELF 文件类型有关,对于 REL 类型(即可可重定位文件)而言,这个字段中是该符号在 section 中的相对偏移量。对于 EXEC、DYN(可执行文件、动态库)类型,st_value 中保存的是虚拟地址,如果 section 类型为 SHT_UNDEF 且 st_value 为 0,则说明这是一个需要在运行时重定位的符号。
st_size
符号的大小,如果此值为 0 则表示符号大小未知。
字符串表
符号的结构体 Elf64_Sym 中的 st_name 为符号的名称,这里 st_name 并不是一个字符串而是一个数字,它指向字符串表中某个字符串,该字符串为符号的名称。
下面我们实际动手来观察一下 ELF 文件的符号表。下面使用 hello world 程序 hello 来演示,首先使用 readelf -S 查看 section 表:
$ readelf -S hello
There are 37 section headers, starting at offset 0x4678:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
...
[35] .strtab STRTAB 0000000000000000 00004360
00000000000001d2 0000000000000000 0 0 1
.strtab 这个 section 中存放的是字符串,我们重点关注它。
可以看到 .strtab 的偏移量为 0x4360 = 17248,大小为 0x1d2 = 466,可以使用如下命令把这部分内容从 ELF 文件截取出来:
$ dd if=hello of=str.txt bs=1 skip=17248 count=466
$ hexdump -C str.txt | head -n 10
00000000 00 63 72 74 73 74 75 66 66 2e 63 00 5f 5f 43 54 |.crtstuff.c.__CT|
00000010 4f 52 5f 4c 49 53 54 5f 5f 00 5f 5f 44 54 4f 52 |OR_LIST__.__DTOR|
00000020 5f 4c 49 53 54 5f 5f 00 64 65 72 65 67 69 73 74 |_LIST__.deregist|
00000030 65 72 5f 74 6d 5f 63 6c 6f 6e 65 73 00 5f 5f 64 |er_tm_clones.__d|
00000040 6f 5f 67 6c 6f 62 61 6c 5f 64 74 6f 72 73 5f 61 |o_global_dtors_a|
00000050 75 78 00 63 6f 6d 70 6c 65 74 65 64 2e 37 32 36 |ux.completed.726|
00000060 38 00 64 74 6f 72 5f 69 64 78 2e 37 32 37 30 00 |8.dtor_idx.7270.|
00000070 66 72 61 6d 65 5f 64 75 6d 6d 79 00 5f 5f 43 54 |frame_dummy.__CT|
00000080 4f 52 5f 45 4e 44 5f 5f 00 5f 5f 46 52 41 4d 45 |OR_END__.__FRAME|
00000090 5f 45 4e 44 5f 5f 00 5f 5f 64 6f 5f 67 6c 6f 62 |_END__.__do_glob|
在 .strtab 中包含的都是字符串,每个字符串都是以 \0 结尾,并一个挨着一个存储。前面提到表示符号的结构体中有 st_name 字段,该字段是一个整型,实际上就是字符串表中的一个 offset,通过这个 offset 可以查询在字符串表中查到对应的符号名称。因为所有的符号都是 \0 结尾的,所以没有必要记录符号名称的长度。
下面是字符串表的一个图例:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | \0 | n | a | m | e | \0 | s | t | r | i |
| 10 | n | g | \0 | p | r | i | n | t | \0 | m |
| 20 | a | i | n | \0 |
这里面包含如下符号:
| index | 字符串 |
|---|---|
| 0 | null |
| 1 | name |
| 5 | string |
| 13 | |
| 19 | main |
符号表
一个 ELF 文件中最多包含两个符号表,它们放置在 .symtab 和 .dynsym 这两个 section 中。
$ readelf -S hello
There are 37 section headers, starting at offset 0x4678:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
...
[ 5] .dynsym DYNSYM 0000000000400348 00000348
0000000000000060 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 00000000004003a8 000003a8
000000000000003f 0000000000000000 A 0 0 1
...
[34] .symtab SYMTAB 0000000000000000 00003c70
00000000000006f0 0000000000000018 35 56 8
[35] .strtab STRTAB 0000000000000000 00004360
00000000000001d2 0000000000000000 0 0 1
...
观察 hello 程序 section 列表,可以看到 .dynsym 的 link 字段指向 .dynstr,.symtab 的 link 字段指向 .strtab。这是因为这两个符号表使用了不同的字符串表。
.dynsym 中存方的是动态加载的符号,可以使用如下命令观察它们:
$ readelf --dyn-syms hello
Symbol table '.dynsym' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
直接读取 .dynstr 中的内容:
$ dd if=hello of=str.txt bs=1 skip=936 count=63
$ hexdump -C str.txt
00000000 00 6c 69 62 63 2e 73 6f 2e 36 00 70 72 69 6e 74 |.libc.so.6.print|
00000010 66 00 5f 5f 6c 69 62 63 5f 73 74 61 72 74 5f 6d |f.__libc_start_m|
00000020 61 69 6e 00 47 4c 49 42 43 5f 32 2e 32 2e 35 00 |ain.GLIBC_2.2.5.|
00000030 5f 5f 67 6d 6f 6e 5f 73 74 61 72 74 5f 5f 00 |__gmon_start__.|
为什么需要两个符号表呢?
符号表 .dynsym 中的符号会被装载器使用到,比如 hello 中的 __libc_start_main 则是程序的入口,装载器把 hello 程序载入到内存后,做好必要的初始化工作后,就会查找 __libc_start_main 符号,并跳转到该函数。另外动态库中的符号表 .dynsym 会在运行时动态加载符号时被使用到,所以 .dynsym 这个符号表示不可或缺的。
.symtab 只是在调试和静态连接的时候会被使用到,对可执行程序和动态库而言,如果不需要被调试,完全可以不需要 .dynsym 和 .strtab 这两个 section。
当你的程序比较大时候,符号表中就会存在很多的符号,可执行文件的大小相应的也会大不少。这个时候可以使用 strip 命令将符号表和字符串表全部删除掉。
$ gcc hello.c -o hello
$ ll hello
-rwxrwxr-x 1 work work 20408 Aug 11 15:12 hello
$ readelf -s hello # 查看符号
Symbol table '.dynsym' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
Symbol table '.symtab' contains 74 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004002a8 0 SECTION LOCAL DEFAULT 1
2: 00000000004002dc 0 SECTION LOCAL DEFAULT 2
3: 0000000000400300 0 SECTION LOCAL DEFAULT 3
4: 0000000000400328 0 SECTION LOCAL DEFAULT 4
5: 0000000000400348 0 SECTION LOCAL DEFAULT 5
...
$ strip -s hello # 删除符号
$ readelf -s hello # 再次查看符号
Symbol table '.dynsym' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
$ ll hello
-rwxrwxr-x 1 work work 14376 Aug 11 15:12 hello
$ ./hello # 执行程序
hello world
使用 strip 命令删除掉符号表 .symtab 之后,不会影响程序的运行,同时程序的体积会减少不少,缺点自然是该程序调试起来会很难。
总结
本文描述了 ELF 文件中符号相关的细节,通过本文介绍,我们可以了符号在 ELF 文件中的存储格式,符号的绑定类型和可见性等属性。
参考资料: