目录
1.动态库文件的位置无关的解释
使用OS40.【Linux】动态库和静态库 自制动态库的代码:
dynamic_lib.h:
#pragma once
double mydiv(double x,double y);
dynamic_lib.c:
int myerrno = 0;
double mydiv(double x,double y)
{
if (y == 0)
{
myerrno = 1;
return -1;
}
return x/y*1.0;
}
重写Makefile:
lib=libmydiv.so
$(lib):dynamic_lib.o
gcc -shared -o $@ $^
dynamic_lib.o:dynamic_lib.c
gcc -fPIC -c $^
.PHONY:clean
clean:
rm -f dynamic_lib.o libmydiv.so
.PHONY:relase
release:
mv libmydiv.so ./lib/libdiv/libmydiv.so
依次执行:
make
make release
最终生成libmydiv.so文件
-fPIC
带fPIC选项通常用于生成动态库,PIC的全称是Position-Independent Code,即位置无关代码
现在调用自制的动态库:
#include <stdio.h>
#include "lib/include/static_lib.h"
int main()
{
extern int myerrno;
double ret = mydiv(1,2);
printf("1 / 2 = %lf myerrno = %d\n",ret,myerrno);
}
临时添加环境变量:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/guest/lib/libdiv
编译命令:
gcc -g main.c -I ./lib/include/ -L ./lib/libdiv/ -lmydiv -o main.out
objdump生成反汇编文件:
objdump -d -M intel main.out > main.asm
查看main函数中调用动态库函数的指令:

call指令是调用动态库函数mydiv:
call 1070 <mydiv@plt>
上文OS41.【Linux】程序的入口地址、可执行文件的位置无关的解释说过,像0x1070这样的其实是相对偏移,call指令跳到
0000000000001070 <mydiv@plt>:
1070: f3 0f 1e fa endbr64
1074: ff 25 56 2f 00 00 jmp QWORD PTR [rip+0x2f56] # 3fd0 <mydiv@Base>
107a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0]
注意到jmp QWORD PTR [rip+0x2f56]这个关键指令,采用rip+0x2f56访问mydiv函数,,rip是基址,不是固定值,是位置无关(PIC),RIP寄存器的值由操作系统决定,是随机的,而0x2f56是写死的
结论: 动态库中的函数和变量地址使用相对偏移量来生成地址,而不是绝对地址
为什么动态库加载的虚拟内存地址是随机的?
假设一下,如果动态库加载的虚拟内存地址是固定的,由于一个进程可能会调用成千上百个库,那么这些动态库都要加载到固定的虚拟内存位置,换句话说,操作系统就只能采用在地址空间的固定位置映射动态库
但是有两个问题:
1.有操作系统知道的动态库,也有操作系统不知道的动态库,操作系统不可能说给每一个动态库都提前规划好空间!操作系统管理已加载动态库难度很大
2.其次,动态库的加载顺序是未知的,如何保证每个库都要加载到固定位置呢? 显然成本太高
所以动态库加载到固定地址空间的位置是不可能的,换句话来说,动态库加载到虚拟内存中的绝对地址是不行的
结论1: 动态库被加载到不同程序的共享区可能加载的位置不同,所以这些函数变量就不能使用固定的地址,要用偏移量,这样获取加载的起始地址后就可以计算出各个函数和变量的实际地址了
那么动态库必定要采用相对地址的方式加载: 编译的时候编译器提供每个函数或方法在自己动态库中的偏移量,这样动态库就可以在进程地址空间的动态库区域的任意位置处进行加载,加载后动态库的最开始的位置处就是共享库的起始地址,这样可以通过基地址+相对偏移量的方式来访问动态库的函数和变量
结论2 fPIC的含义: 动态库可以在虚拟内存的任意位置加载,与位置无关
下面补充一个和上述分析相关的名词ASLR
ASLR
可以通过ldd验证: 动态库每次加载到虚拟内存中的位置都不一样
ldd /usr/bin/ls #查看ls命令需要的动态库

(上图显示的地址是每个动态库文件在虚拟内存中的基地址)
动态库每次加载到虚拟内存中的位置都不一样,这称为地址空间配置随机加载(Address Space Layout Randomization,简称ASLR)
注意: ASLR 是对可执行文件的虚拟地址随机化主要影响的是运行时加载的地址,不是ELF文件中的虚拟地址,ELF 文件中的虚拟地址是编译时链接器确定的静态地址,与ASLR无关
为什么静态库不谈加载,不谈位置无关?
1.因为静态库写死在可执行文件中,所以不谈加载
2.因为静态库的函数和变量的地址是固定的,所以不谈位置无关
多个进程共享库的做法
由上面的分析可以推出: 将共享库映射到每个进程的地址空间中就能让多个进程共享库
2.底层分析共享库中的函数调用——PLT表和GOT表
案例引入
上面提到的代码有一个小细节:
call 1070 <mydiv@plt>
mydiv后的@plt,PLT的全称是Procedure Linkage Table,即过程链接表,下面详细分析
详细分析
编译此代码为可执行文件:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Hello World!");
exit(0);
}
附:编译环境:

执行自己的main代码前,需要做一些前置工作,可以使用stace命令看看从可执行文件执行的那一刻到运行结束,都用了哪一些系统调用:

由上图得知,会发现执行main函数开始,需要先加载库文件,然后main函数才能调用库函数
Cutter反汇编工具初步分析
分析静态的可执行文件
进入主界面,首先点击

注意main函数调用printf的指令:

双击section..plt.sec,跳到这里:

enbr64和控制流的安全有关,不是本文要分析的重点,这里不管,看下个jmp指令:
jmp qword [printf] ; 0x3fc8
上方指令是这样执行的: 先从printf标号的地址处取出8个字节的数据,然后jmp会将这个数据当作地址跳转到那里执行
双击printf,跳到这里:

旁边有个注释:
; reloc.target.printf; RELOC 64 printf
reloc是relocate的简写,即重定向,猜测执行可执行文件时printf标号处的8字节数据可能会被修改(动态库加载的虚拟内存地址是随机的,那么动态库中的库函数的虚拟地址也是随机的),以便于jmp qword [printf]跳到正确的位置执行
启动Cutter调试功能,进一步分析
在jmp qword [printf]下断点(F2):

点击调试按钮:

命令行参数保持空白:

单击继续,直接来到jmp qword [printf]:

会发现此时printf标号处的8字节数据被修改了:


单击步进,进入printf标号处所存储的地址:

如何判断是否正在执行加载的库内的函数printf?
先取得正在调试的进程的PID,用pmap查看映射,找到so文件映射的地址,然后看看rip寄存器的值是不是在so文件映射的地址内
查看PID:
ps ajx | head -1 && ps ajx | grep a.out

pmap查看映射:
pmap -x2 pid
1. printf是可执行函数,那么一定有可执行权限
2.printf的动态库函数,那么一定是映射到libc(C Library)so文件

猜测printf映射到这里了:
![]()
验证:
RIP寄存器的值:0x7936dfa60100

而0x7936dfa60100在00007936dfa00000~00007936dfa28000之间,猜测正确
结论: jmp qword [printf]跳转到动态库文件的printf函数中执行
而main函数执行exit函数时:

call exit到:

那么jmp qword[exit]也是执行动态库文件中的exit函数
查看exit标号处的地址,发现也存储了动态库文件中的exit函数的地址:

更让人惊奇的是动态库文件中的exit函数和printf函数的地址是紧邻着存储的!

由上图中的注释"section size 80 named .got"注释说明: 这两个函数的地址紧邻这存储是有原因的: 它们存储在GOT表中!
GOT表的定义
GOT全称是Global Offset Table,即全局偏移表,从上述的分析来看,这个表的作用是: 存储动态链接库函数的地址
为了减小可执行文件的大小,动态库函数不存放在可执行文件中,而是存放在动态库中
在CTF 101网https://ctf101.org/binary-exploitation/what-is-the-got/上也提到了这个定义:
The Global Offset Table (or GOT) is a section inside of programs that holds addresses of functions that are dynamically linked. As mentioned in the page on calling conventions, most programs don't include every function they use to reduce binary size. Instead, common functions (like those in libc) are "linked" into the program so they can be saved once on disk and reused by every program.
怎么查看是谁修改了GOT表中的数据?
GDB深入分析
GOT表中的printf标记和exit标记处的8字节数据一开始不是库函数对应在动态链接库的地址,那么是谁修改了GOT表中的数据?
答: 用GDB下硬件断点(Cutter也有这个功能,但有bug),这个之前在OS28.【Linux】自制简单的Shell的修bug记录文章讲过,这里直接用了
1.先让gdb控制进程停在第一条用户态指令:使用starti命令,确保在printf标记和exit标记处的8字节数据没有被修改
startiThe ‘starti’ command does the equivalent of setting a temporary breakpoint at the first instruction of a program’s execution and then invoking the ‘run’ command. For programs containing an elaboration phase, the
starticommand will stop execution at the start of the elaboration phase.


取得printf标记和exit标记处的8字节数据的地址:
info address printf@got.plt
info address exit@got.plt

在printf标记和exit标记处的8字节数据下如图所示的硬件断点:
watch *(void**)0x555555557fc8
watch *(void**)0x555555557fd0
c命令继续执行,发现硬件断点被触发了:

注意到rip停在了elf_dynamic_do_Rela+1601处

sourceware网站https://sourceware.org/glibc/wiki/DynamicLoader显示: 这个elf_dynamic_do_Rela是属于Dynamic Loader Operation(动态加载操作)的一部分!

再使用pmap查看RIP寄存器停在哪个位置:
p $rip #查看rip寄存器的值
info inferior #查看被调试进程的PID
shell pmap -x pid #查看pid进程的映射关系

rip在ld-linux-x86-64.so.2动态库文件里面,得出:
结论: GOT表数据的修改靠ld-linux-x86-64.so.2动态库文件的Dynamic Loader Operation(动态加载操作),换句话说,GOT表数据的修改靠加载器,加载器会将其中的条目填充为正确的地址,这个过程就是重定位,证明了与位置无关
PLT表的定义
理解了GOT表,那么PLT表的含义迎刃而解
main函数中的call section..plt.sec跳到了PLT表处

在section..plt.sec的上方还有一个段: section..plt

PLT的全称是Procedure Linkage Table,即过程链接表,作用是: PLT配合全局偏移表GOT实现延迟绑定,即在函数第一次被调用时才进行地址解析和重定位(懒加载机制),提高效率
而且PLT表每一表项都是一小段代码,也被称为桩函数(stub function)
3.本文的所有结论
结论1: 动态库中的函数和变量地址使用相对偏移量来生成地址,而不是绝对地址
结论2: 动态库被加载到不同程序的共享区可能加载的位置不同,所以这些函数变量就不能使用固定的地址,要用偏移量,这样获取加载的起始地址后就可以计算出各个函数和变量的实际地址了
结论3 fPIC的含义: 动态库可以在虚拟内存的任意位置加载,与位置无关
结论4: jmp qword [printf]跳转到动态库文件的printf函数中执行,而printf标号处的数据在GOT表中
结论5: GOT全称是Global Offset Table,即全局偏移表,从上述的分析来看,这个表的作用是: 存储动态链接库函数的地址
结论6: GOT表数据的修改靠ld-linux-x86-64.so.2动态库文件的Dynamic Loader Operation(动态加载操作),换句话说,GOT表数据的修改靠加载器,加载器会将其中的条目填充为正确的地址,这个过程就是重定位,证明了与位置无关
结论7: 过程链接表PLT配合全局偏移表GOT实现延迟绑定,即在函数第一次被调用时才进行地址解析和重定位,提高效率
4.stackexchange上的回答
https://reverseengineering.stackexchange.com/questions/1992/what-is-plt-got
采纳的回答:
PLT stands for Procedure Linkage Table which is, put simply, used to call external procedures/functions whose address isn't known in the time of linking, and is left to be resolved by the dynamic linker at run time.
GOT stands for Global Offsets Table and is similarly used to resolve addresses. Both PLT and GOT and other relocation information is explained in greater length in this article.
简而言之,PLT是它用于调用在链接时地址未知的外部例程或函数,并由动态链接器在运行时解析
5.拓展阅读
有关更多链接和加载的知识请参见:
1.《深入理解计算机系统》 David R. O'Hallaron Randal E. Bryant 的第7章链接
2.《程序员的自我修养——链接、装载与库》俞甲子 、石凡、潘爱民
3.《Linkers and Loaders (The Morgan Kaufmann Series in Software Engineering and Programming)》John R. Levine
1409





