【Linux指南】动静态库系列:GOT 与 PIC:动态库为什么可以加载到任意地址

一、动态库面临的地址难题
静态链接 时,链接器可以在生成可执行程序时确定大部分地址。

但动态库不同。.so 文件在磁盘上时,并不知道未来会被加载到哪个进程、哪个虚拟地址。

例如同一个 libmyc.so:

进程 A 中加载基址:0x7f1000000000
进程 B 中加载基址:0x7f2000000000
AI代码解释
text
1
2
如果动态库代码中写死绝对地址:

跳转到 0x7f1000001234
AI代码解释
text
1
那么它在进程 B 中就可能完全错误。

所以动态库必须避免依赖固定绝对地址。

二、什么是位置无关代码 PIC
PIC 是 Position Independent Code,位置无关代码。

它的目标是:

同一份代码无论被加载到进程地址空间的哪个位置,都能正常执行。
AI代码解释
text
1
编译动态库时常用:

gcc -fPIC -c lib.c -o lib.o
AI代码解释
bash
1
-fPIC 告诉编译器生成适合动态库使用的位置无关代码。

通俗理解,PIC 不把绝对地址写死在代码里,而是尽量使用:

相对位置
基址 + 偏移
查表跳转
AI代码解释
text
1
2
3
三、磁盘 .so 中的地址可以先看成相对偏移
为了理解动态库地址,可以先建立一个模型。

磁盘上的 libmyc.so 内部,某个函数 foo 位于文件内部偏移:

0x2000
AI代码解释
text
1
某个全局变量位于偏移:

0x3500
AI代码解释
text
1
这些偏移不是进程中的最终虚拟地址。它们只是库文件内部相对位置。

当动态库被加载进进程时,动态链接器为它分配一个运行时基址。

假设:

运行时基址 = 0x7f1000000000
foo 偏移 = 0x2000
AI代码解释
text
1
2
那么 foo 的运行时虚拟地址就是:

0x7f1000000000 + 0x2000 = 0x7f1000002000
AI代码解释
text
1
换一个进程,基址不同,最终地址也不同。

所以动态库必须能接受:

真实地址 = 运行时基址 + 内部偏移
AI代码解释
text
1
四、为什么不能直接修改代码段
一种看似简单的办法是:动态库加载后,把代码段中所有地址都改成真实地址。

但这会带来严重问题。

1. 代码段通常是只读的
动态库的 .text 代码段通常映射为只读、可执行:

r-xp
AI代码解释
text
1
运行时直接修改代码段不符合权限设计。

2. 代码段需要被多个进程共享
动态库节省内存的关键是多个进程共享同一份只读代码页。

如果每个进程都要把代码段改成自己专属地址,那么代码页就不能简单共享了。

所以动态链接需要避免频繁修改 .text。

解决思路是:

代码段尽量保持不变;
把需要变化的地址放到可写数据区中。
AI代码解释
text
1
2
这就引出了 GOT。

五、GOT 是什么
GOT 是 Global Offset Table,全局偏移表。

它可以理解为动态链接中的一张地址表。

表里保存:

外部函数的真实地址
全局变量的真实地址
运行时需要修正的地址信息
AI代码解释
text
1
2
3
程序调用某些动态库函数或访问某些全局符号时,不直接把最终地址写死在代码里,而是通过 GOT 查表。

简化流程:

代码中使用相对方式找到 GOT
-> 从 GOT 表项中取出真实地址
-> 跳转或访问该地址
AI代码解释
text
1
2
3
GOT 位于可写区域,动态链接器可以在加载时修改 GOT 表项。

六、为什么每个进程都有自己的 GOT
动态库代码段可以共享,但 GOT 通常不能简单共享。

原因是不同进程中动态库的加载地址可能不同,依赖库也可能映射在不同位置。

所以:

进程 A 的 GOT 里记录的是进程 A 地址空间中的真实地址;
进程 B 的 GOT 里记录的是进程 B 地址空间中的真实地址。
AI代码解释
text
1
2
它们的代码段可以指向同一份物理页,但 GOT 表项需要各自独立。

代码段只读可共享;
GOT 表在每个进程中独立维护。
AI代码解释
text
1
2
七、PIC = 相对编址 + GOT
可以用一句话概括 PIC 的核心思想:

PIC = 相对编址 + GOT
AI代码解释
text
1
在单个 .so 内部,.text 和 .got 的相对位置是固定的。即使整个库被加载到不同基址,代码仍然可以通过相对寻址找到自己的 GOT。

然后通过 GOT 中的表项访问真正地址。

这就实现了:

动态库加载到任意地址,代码仍然可以正常运行。
AI代码解释
text
1
八、用 readelf 查看 GOT
可以用 readelf -S 查看动态库中的 section:

readelf -S libmyc.so | grep -E "got|plt"
AI代码解释
bash
1
你可能看到:

.got
.got.plt
.plt
AI代码解释
text
1
2
3
这些 section 就和动态链接密切相关。

也可以查看可执行程序:

readelf -S main | grep -E "got|plt"
AI代码解释
bash
1
动态链接程序中通常也能看到 GOT/PLT 相关区域。

九、用 objdump 观察间接访问
反汇编动态链接程序:

objdump -d main | less
AI代码解释
bash
1
你可能看到类 似:

callq puts@plt
AI代码解释
text
1
或者看到通过寄存器、GOT 表项进行间接跳转的指令。

初学阶段不需要逐条汇编完全读懂,但要观察到一点:

动态链接下,外部函数调用并不总是直接 call 一个固定绝对地址。
AI代码解释
text
1
它往往会经过 PLT/GOT 这样的间接结构。

十、全局变量访问为什么也需要 GOT
函数调用需要地址,全局变量访问同样需要地址。

如果动态库中访问一个外部全局变量,变量最终在哪个模块、哪个地址,可能要到加载后才能确定。

于是编译器也会通过 GOT 访问这类符号。

这样,无论变量最终地址在哪里,只要动态链接器把 GOT 表项填好,代码就能通过查表访问正确位置。

十一、GOT 解决了什么,没解决什么
GOT 解决的是:

动态库加载地址不固定时,如何找到正确函数和变量地址。
AI代码解释
text
1
但 GOT 本身还不完全等于“高效”。

如果程序启动时就把所有动态库函数的 GOT 表项全部解析好,启动成本可能很高。

因为很多库函数在整个程序运行期间可能一次都不会被调用。

于是系统又做了进一步优化:

函数第一次被调用时再解析。
AI代码解释
text
1
这就是 PLT 和延迟绑定要解决的问题。

十二、为什么 -fPIC 对动态库很重要
回到动态库制作命令:

gcc -fPIC -c lib.c -o lib.o
gcc -shared -o libdemo.so lib.o
AI代码解释
bash
1
2
现在就能理解 -fPIC 的意义了:

它让编译器生成适合被加载到任意地址的代码。
AI代码解释
text
1
如果不使用 -fPIC,某些平台或某些代码情况下,生成动态库可能出现警告或错误,或者导致运行时重定位成本上升、代码段难以共享。

所以写动态库时,-fPIC 是非常重要的习惯。

十三、一个完整实验
准备 lib.c:

#include <stdio.h>

int g_val = 100;

void show(void)
{
printf("g_val=%d\n", g_val);
}
AI代码解释
c
运行
1
2
3
4
5
6
7
8
编译动态库:

gcc -fPIC -c lib.c -o lib.o
gcc -shared -o libdemo.so lib.o
AI代码解释
bash
1
2
观察 GOT/PLT:

readelf -S libdemo.so | grep -E "got|plt"
objdump -d libdemo.so | less
AI代码解释
bash
1
2
再写 main.c:

void show(void);

int main()
{
show();
return 0;
}
AI代码解释
c
运行
1
2
3
4
5
6
7
链接并观察:

gcc main.c -L. -ldemo -Wl,-rpath=. -o main
readelf -S main | grep -E "got|plt"
objdump -d main | grep -A8 "show"
AI代码解释
bash
1
2
3
通过这些命令,你可以看到动态链接程序中确实存在 GOT/PLT 相关结构。

十四、总结
动态库可以加载到任意地址,不是因为它不需要地址,而是因为它不把绝对地址写死在代码里。

核心机制是:

PIC = 相对编址 + GOT
AI代码解释
text
1
代码段保持只读、可共享;运行时会变化的函数和变量地址放在 GOT 中,由动态链接器填充和修正。
————————————————
版权声明:本文为CSDN博主「倔强的石头_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2302_78391795/article/details/166936593

上一篇 办公三件套:Word、Excel、PPT
下一篇 windows 电脑出现病毒和威胁防护无法启动