引言

文件在编译链接时,是不知道glibc动态库内printf的函数地址的,而只是写了一个“占位符”在汇编中,glibc动态库内printf的函数地址只有装载进内存时才能够知道

但是进程运行起来后,glibc动态库装载了,printf函数地址也确定了,那call指令是怎么重定位地址的呢?

一种简单的方法就是将指令中的“占位符”修改成printf函数的真实地址即可。但是随之又出现两个问题:

  1. 现代操作系统不允许修改代码段,只能修改数据段
  2. 如果函数是在一个动态库(.so)中,修改了代码段,那么它就无法做到系统内所有进程共享同一个动态库
    所以prinf函数的地址只能回写到数据段,而不是代码段,这样的回写其实叫做运行时重定位,与之相应的还有链接时重定位

编译与链接

每个编译单元(通常是一个.cpp文件)都会经历编译和链接两个阶段

编译阶段是将.c源代码编译成汇编指令的中间文件,即.o中间文件。由于编译阶段无法知道printf函数的地址,所以call指令后会预先放一个占位符,然后用重定位项来描述:这个地址在链接时需要修正,它的修正值是根据printf函数地址(更确切应该说是符号,链接器眼中只有符号,没有函数、变量)来修正的,它的修正方式按相对引用方式。而这个过程叫做链接时重定位,与运行时重定位工作原理完全一样,只是修正时机不同

链接阶段是将一个或者多个中间文件(.o)通过链接器将它们链接成一个可执行文件,链接阶段主要完成以下事情:

  • 各个中间文件之间的同名section合并
  • 对代码段、数据段以及各符号进行地址分配
  • 链接时重定位修正
    除了重定位过程,其它动作是无法修改中间文件中函数体内指令的,而重定位过程也只能修改指令中的操作数,无法修改编译过程生成的汇编指令

编译器并不知道printf函数是在glibc动态库,而不是定义在其它.o文件中的所以编译器只能老老实实生成调用printf函数的汇编指令,如果在其它.o中定义了printf函数,那么链接阶段,printf函数地址已经确定,可以直接重定位。但是如果printf定义在动态库内,链接阶段无法做重定位

我们前面知道了,运行时重定位是无法修改代码段的,只能将printf重定位到数据段。而编译阶段已生成好的call指令,如何找到这个已重定位好的数据段内容呢?
所以链接器会生成一段额外的小代码片段,通过这段代码支获取printf函数地址,并完成对它的调用
链接器生成额外的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text
...
//调用printf的call指令
call printf_stub
...
printf_stub:
mov rax, [printf函数的储存地址] //获取printf重定位后的地址
jmp rax //跳转执行printf函数

.data
...
printf函数的储存地址:
这里储存printf函数重定位后的地址
...

链接阶段可以知道printf在哪定义,只不过定义在动态库内时并不知道它的地址。当链接阶段发现printf定义在动态库时,链接器会生成一段小代码printf_stub,然后printf_stub地址取代原来的printf。因此转化为链接阶段对printf_stub做链接时重定位,而运行时才对printf做运行时重定位(其目的就是把本需要写入代码段的运行时地址,转移到数据段去写入

PLT表与GOT表

动态链接需要考虑的因素说白了就两点:

  • 需要存放外部函数的数据段
  • 获取数据段存放函数地址的一小段额外代码
    如果可执行文件中调用多个动态库函数,那么每个函数都需要这两样东西,这样就形成了两个表,每个函数使用中的一项
    于是,存放函数地址的数据表,我们称为全局偏移表(GOT,Global Offset Table),位于数据段;而额外代码段表,称为程序链接表(PLT,Procedure Link Table),位于代码段它们各司其职又相互配合,完成重定位这个过程