## .text 已编译程序的机器代码。 ## .rodata 只读数据,比如printf语句中的格式串和开关(switch)语句的跳转表。 ## .data 已初始化的全局C变量。局部C变量在运行时被保存在栈中,既不出现在.data中,也不出现在.bss节中。 ## .bss 未初始化的全局C变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分初始化和未初始化变量是为了空间效率在:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。 ## .symtab 一个符号表(symbol table),它存放在程序中被定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的表目。 ## .rel.text 当链接噐把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非使用者显式地指示链接器包含这些信息。 ## .rel.data 被模块定义或引用的任何全局变量的信息。一般而言,任何已初始化全局变量的初始值是全局变量或者外部定义函数的地址都需要被修改。 ## .debug 一个调试符号表,其有些表目是程序中定义的局部变量和类型定义,有些表目是程序中定义和引用的全局变量,有些是原始的C源文件。只有以-g选项调用编译驱动程序时,才会得到这张表。 ## .line 原始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译驱动程序时,才会得到这张表。 ## .strtab 一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串序列。 ## .interp 动态链接器在操作系统中的位置不是由系统配置决定,也不是由环境参数指定,而是由 ELF 文件中的 .interp 段指定。该段里保存的是一个字符串,这个字符串就是可执行文件所需要的动态链接器的位置,常位于 /lib/ld-linux.so.2。(通常是软链接) ## .dynamic 该段中保存了动态链接器所需要的基本信息,是一个结构数组,可以看做动态链接下 ELF 文件的“文件头”。存储了动态链接会用到的各个表的位置等信息。 ## .dynsym 该段与 “.symtab”段类似,但只保存了与动态链接相关的符号,很多时候,ELF文件同时拥有 .symtab 与 .synsym段,其中 .symtab 将包含 .synsym 中的符号。该符号表中记录了动态链接符号在动态符号字符串表中的偏移,与.symtab中记录对应。 ## .dynstr 该段是 .dynsym 段的辅助段,.dynstr 与 .dynsym 的关系,类比与 .symtab 与 .strtab 的关系。 ## .hash 在动态链接下,需要在程序运行时查找符号,为了加快符号查找过程,增加了辅助的符号哈希表,功能与 .dynstr 类似。 ## .rel.dyn 对数据引用的修正,其所修正的位置位于 “.got”以及数据段(类似重定位段 "rel.data")。 ## .rel.plt 对函数引用的修正,其所修正的位置位于 “.got.plt”。 ## .got 动态链接过程中的重定位并不像静态链接那样方便,对于静态链接而言,重定位过程可以直接修改指令中对数据的引用地址,因为静态链接操作的是 elf 文本,而动态链接则做不到,因为这时候指令已经被加载到内存中,且映射为只读属性。 有些朋友就有疑问了,为什么要强行把代码段的数据映射为只读属性,映射成读写属性不行吗,这样动态链接过程就可以直接修改指令了,实际上还真不行。一方面,映射为只读属性是出于保护代码不被修改的目的,另一方面,如果动态库 A 引用了动态库 B,在重定位过程中修改了动态库 A 中的指令部分,但是,动态库是进程之间共享的,某一个进程修改动态库会导致其它进程的引用出错。 那么问题来了,如果不能修改指令,那怎么完成重定位过程?毕竟指令中编码的地址是不能直接使用的。答案是通过数据部分进行一次跳转。 对于所有的进程而言,动态库的数据部分是有独立的一份副本的,这也很好理解,程序的数据部分是读写属性的,对数据的操作由进程说了算,所以,在重定位过程中,既然不能修改代码部分,那么我们只能通过修改数据部分来完成重定位的过程。 这种实现的机制使用了 GOT 表,全名为 global offset table,即全局偏移表。在执行的指令中,本来需要引用符号 A ,但是 A 存在于动态库中,链接过程并不知道它的地址,于是将 A 的地址部分改写为 GOT 表中某一项数据,在编译阶段 GOT 表中是没有真实数据的,但是在动态链接的加载阶段,动态链接器就可以将符号 A 的真实地址填写到 GOT 表中对应的数据项中,这样指令对 A 就产生了正确的引用。 GOT 中每一个表项占用 4 个字节(32位),表示运行时的符号的真实地址。 对于函数而言,可以在加载阶段通过 GOT 表获取到函数的地址保存到寄存器中,然后跳转到该地址,同样可以实现加载时的重定位,但是在实际的指令重定位中,并不单单使用 GOT 表,还使用了另一个 PLT 表实现。 GOT 表是针对外部符号引用的,而 PLT 针对外部跳转引用,通常就是函数跳转。 ## .plt 延迟绑定。相对于静态链接而言,动态链接的确非常灵活,随之也带来了一些性能的牺牲,毕竟将编译链接过程的重定位工作放到了运行时完成,可以想到,当我们在动态链接一个库时,以 libc 为例,需要先对其中的每个函数和全局变量的引用进行重定位,即使是那些没有用到的诸如错误处理的函数,这种情况下共享库越大,链接的时间越长,程序的启动时间也就越长,而且大多数的重定位都是没有必要的,对用户来说这通常是不能忍受的。 于是,延迟绑定的概念被提出,延迟绑定的规则为只有在符号被真正引用时才进行重定位,而不是在刚开始就对所有的动态符号进行重定位,一方面加快了程序的启动,将整个动态加载时间分摊到程序运行期间,另一方面,对于共享库中没有用到的符号,不再进行重定位,节省了重定位的时间,随着共享库的发展更新,这种优势变得越来越明显。 延迟绑定由 plt 表来实现,在 elf 文件中,plt 表和 got 表几乎是时时刻刻伴随着的,在上一段中说到因为运行时不能修改指令,所以通过数据部分的 got 表进行运行时符号真实地址的传递,而 plt 是一小段跳转指令。 ## 参考 * [ELF中与动态链接相关的段](https://blog.csdn.net/farmwang/article/details/72848038) * [程序的动态链接1 - plt 和 got](https://zhuanlan.zhihu.com/p/363489654)