☰
【Linux指南】动静态库系列(十):GOT 与 PIC:动态库为什么可以加载到任意地址
2026/10/2 11:40:57 网站建设 项目流程

文章目录

    • 一、动态库面临的地址难题
    • 二、什么是位置无关代码 PIC
    • 三、磁盘 .so 中的地址可以先看成相对偏移
    • 四、为什么不能直接修改代码段
      • 1. 代码段通常是只读的
      • 2. 代码段需要被多个进程共享
    • 五、GOT 是什么
    • 六、为什么每个进程都有自己的 GOT
    • 七、PIC = 相对编址 + GOT
    • 八、用 readelf 查看 GOT
    • 九、用 objdump 观察间接访问
    • 十、全局变量访问为什么也需要 GOT
    • 十一、GOT 解决了什么,没解决什么
    • 十二、为什么 -fPIC 对动态库很重要
    • 十三、一个完整实验
    • 十四、总结

上一篇我们讲到,动态库会在程序运行时被映射到进程地址空间中,而且加载地址并不固定。进程 A 中的libmyc.so可能在一个地址,进程 B 中的同一个库可能在另一个地址。

这就带来一个核心问题:动态库内部的函数调用、全局变量访问,如何在不同加载地址下都能正常工作?

答案是 PIC 和 GOT。

一、动态库面临的地址难题

静态链接时,链接器可以在生成可执行程序时确定大部分地址。

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

例如同一个libmyc.so:

进程 A 中加载基址:0x7f1000000000 进程 B 中加载基址:0x7f2000000000

如果动态库代码中写死绝对地址:

跳转到 0x7f1000001234

那么它在进程 B 中就可能完全错误。

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

二、什么是位置无关代码 PIC

PIC 是 Position Independent Code,位置无关代码。

它的目标是:

同一份代码无论被加载到进程地址空间的哪个位置,都能正常执行。

编译动态库时常用:

gcc-fPIC-clib.c-olib.o

-fPIC告诉编译器生成适合动态库使用的位置无关代码。

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

相对位置 基址 + 偏移 查表跳转

三、磁盘 .so 中的地址可以先看成相对偏移

为了理解动态库地址,可以先建立一个模型。

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

0x2000

某个全局变量位于偏移:

0x3500

这些偏移不是进程中的最终虚拟地址。它们只是库文件内部相对位置。

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

假设:

运行时基址 = 0x7f1000000000 foo 偏移 = 0x2000

那么foo的运行时虚拟地址就是:

0x7f1000000000 + 0x2000 = 0x7f1000002000

换一个进程,基址不同,最终地址也不同。

所以动态库必须能接受:

真实地址 = 运行时基址 + 内部偏移

四、为什么不能直接修改代码段

一种看似简单的办法是:动态库加载后,把代码段中所有地址都改成真实地址。

但这会带来严重问题。

1. 代码段通常是只读的

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

r-xp

运行时直接修改代码段不符合权限设计。

2. 代码段需要被多个进程共享

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

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

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

解决思路是:

代码段尽量保持不变; 把需要变化的地址放到可写数据区中。

这就引出了 GOT。

五、GOT 是什么

GOT 是 Global Offset Table,全局偏移表。

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

表里保存:

外部函数的真实地址 全局变量的真实地址 运行时需要修正的地址信息

程序调用某些动态库函数或访问某些全局符号时,不直接把最终地址写死在代码里,而是通过 GOT 查表。

简化流程:

代码中使用相对方式找到 GOT -> 从 GOT 表项中取出真实地址 -> 跳转或访问该地址

GOT 位于可写区域,动态链接器可以在加载时修改 GOT 表项。

六、为什么每个进程都有自己的 GOT

动态库代码段可以共享,但 GOT 通常不能简单共享。

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

所以:

进程 A 的 GOT 里记录的是进程 A 地址空间中的真实地址; 进程 B 的 GOT 里记录的是进程 B 地址空间中的真实地址。

它们的代码段可以指向同一份物理页,但 GOT 表项需要各自独立。

代码段只读可共享; GOT 表在每个进程中独立维护。

七、PIC = 相对编址 + GOT

可以用一句话概括 PIC 的核心思想:

PIC = 相对编址 + GOT

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

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

这就实现了:

动态库加载到任意地址,代码仍然可以正常运行。

八、用 readelf 查看 GOT

可以用readelf -S查看动态库中的 section:

readelf-Slibmyc.so|grep-E"got|plt"

你可能看到:

.got .got.plt .plt

这些 section 就和动态链接密切相关。

也可以查看可执行程序:

readelf-Smain|grep-E"got|plt"

动态链接程序中通常也能看到 GOT/PLT 相关区域。

九、用 objdump 观察间接访问

反汇编动态链接程序:

objdump-dmain|less

你可能看到类似:

callq puts@plt

或者看到通过寄存器、GOT 表项进行间接跳转的指令。

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

动态链接下,外部函数调用并不总是直接 call 一个固定绝对地址。

它往往会经过 PLT/GOT 这样的间接结构。

十、全局变量访问为什么也需要 GOT

函数调用需要地址,全局变量访问同样需要地址。

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

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

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

十一、GOT 解决了什么,没解决什么

GOT 解决的是:

动态库加载地址不固定时,如何找到正确函数和变量地址。

但 GOT 本身还不完全等于“高效”。

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

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

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

函数第一次被调用时再解析。

这就是 PLT 和延迟绑定要解决的问题。

十二、为什么 -fPIC 对动态库很重要

回到动态库制作命令:

gcc-fPIC-clib.c-olib.o gcc-shared-olibdemo.so lib.o

现在就能理解-fPIC的意义了:

它让编译器生成适合被加载到任意地址的代码。

如果不使用-fPIC,某些平台或某些代码情况下,生成动态库可能出现警告或错误,或者导致运行时重定位成本上升、代码段难以共享。

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

十三、一个完整实验

准备lib.c:

#include<stdio.h>intg_val=100;voidshow(void){printf("g_val=%d\n",g_val);}

编译动态库:

gcc-fPIC-clib.c-olib.o gcc-shared-olibdemo.so lib.o

观察 GOT/PLT:

readelf-Slibdemo.so|grep-E"got|plt"objdump-dlibdemo.so|less

再写main.c:

voidshow(void);intmain(){show();return0;}

链接并观察:

gcc main.c -L.-ldemo-Wl,-rpath=.-omain readelf-Smain|grep-E"got|plt"objdump-dmain|grep-A8"show"

通过这些命令,你可以看到动态链接程序中确实存在 GOT/PLT 相关结构。

十四、总结

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

核心机制是:

PIC = 相对编址 + GOT

代码段保持只读、可共享;运行时会变化的函数和变量地址放在 GOT 中,由动态链接器填充和修正。

下一篇,我们继续讲 PLT 和延迟绑定,看看动态库函数第一次被调用时,到底发生了什么。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询