☰
ELF加载、动态链接、GOT与PLT
2026/10/1 6:35:30 网站建设 项目流程

前面已经弄清楚了.o、ELF、Section、Segment 和静态链接。

程序编译链接完成以后,还差最后一步:

它到底是怎么跑起来的?

比如我在终端输入:

./main

程序显然不会凭空出现在内存里。

操作系统需要先读取 ELF 文件,建立进程的地址空间,把程序需要的内容加载进去。

如果程序还依赖了:

libc.so libmystdio.so

这些动态库也得处理。

这一篇主要把这部分串起来。


一、ELF还没加载进内存的时候,有地址吗?

刚开始接触这个问题时,我会觉得:

程序都还在磁盘里,没有进入内存,怎么可能已经有地址?

但 ELF 文件本身已经有自己的地址组织方式了。

在现代计算机的平坦模式下,程序中的代码和数据会进行统一编址。

所以,即使程序此时还在磁盘上,这些代码和数据也已经拥有对应的逻辑地址关系。

使用:

objdump-Smain

或者查看 ELF Header:

readelf-hmain

都可以看到这些信息。


二、程序从哪里开始执行?

一个 ELF 可执行文件里面有一个很重要的字段:

Entry point address

也就是程序入口地址。

比如:

Entry point address: 0x1060

程序启动的时候并不是直接跑到我们写的:

main()

而是先从入口位置开始。

Linux 下通常会先进入:

_start

然后经过 C 运行时环境的初始化,最后才进入:

main

所以:

程序启动 ↓ _start ↓ 初始化 ↓ 动态链接等处理 ↓ __libc_start_main ↓ main

这个过程平时写 C/C++ 的时候基本看不到,但真正运行的时候确实存在。


三、Segment和进程地址空间

前面研究 ELF 的时候已经知道,Section 最终会根据属性组合成 Segment。

程序加载的时候,真正和内存布局关系比较密切的就是 Segment。

一个 Segment 会有自己的:

起始地址 长度 权限

这些信息可以拿来初始化进程地址空间中的对应区域。

可以粗略地理解成:

ELF ↓ Program Header Table ↓ 找到各个需要加载的 Segment ↓ 建立进程地址空间 ↓ 把对应内容映射进去

所以 Program Header Table 对运行时加载非常重要。


四、是不是所有Section都会加载到内存?

不是。

ELF 里有很多 Section 是为了链接、调试或者描述文件结构准备的,并不是每一个都需要作为程序内容加载到内存。

真正和加载有关的是 Program Header Table 里面的 Segment。

例如:

readelf-lmain

可以看到:

LOAD LOAD DYNAMIC ...

其中LOAD表示这一部分后面要被加载。

所以:

Section ↓ ELF文件内部怎么组织 Segment ↓ 运行时怎么加载

这个区别到这里就比较好理解了。


五、静态链接和动态链接最大的区别

静态链接的时候:

.o + 静态库中的.o ↓ 一起链接 ↓ 独立可执行文件

库里的代码已经进入最终程序。

动态链接则不一样。

比如:

ldd main.exe

可能看到:

libc.so.6 /lib64/ld-linux-x86-64.so.2

这些动态库并没有在编译链接时把完整代码直接塞进可执行文件。

程序真正启动以后,动态链接器才会去处理这些库。

所以可以把动态链接理解成:

把一部分链接工作推迟到程序运行的时候。


六、动态库为什么不会每个进程都复制一份?

假设机器上同时运行:

程序A 程序B 程序C

它们都需要:

libc.so

如果每个进程都在内存里完整复制一份,那肯定浪费。

动态链接的一个重要特点就是:

多个进程可以共享同一份动态库代码。

动态库加载到内存以后,不同进程都可以通过自己的地址空间去访问它,从而减少内存和磁盘空间的浪费。


七、动态库加载以后,地址一定一样吗?

不一定。

一个进程和另一个进程的地址空间并不相同。

而且一个动态库每次加载到内存的位置也可能不同。

所以动态库不能简单地写死:

我永远在 0x12345678

这种绝对地址。

它更适合使用:

相对地址。

也就是:

动态库起始地址 + 函数在库中的偏移量 = 函数真正的地址

这样动态库换一个位置以后,只要起始地址变化,内部的偏移关系仍然有效。


八、动态库到底怎么和当前进程对应起来?

可以把过程想成这样:

进程 ↓ 加载动态库 ↓ 动态库映射到进程地址空间 ↓ 知道动态库起始地址 ↓ 知道某个函数在库里的偏移量 ↓ 找到函数

比如某个函数在动态库中的偏移是:

0x500

当前动态库加载到:

0x70000000

那么函数地址就可以理解成:

0x70000000 + 0x500

核心思想就是:

起始地址 + 偏移量。


九、问题来了:代码区不是只读的吗?

这里就出现一个很有意思的问题。

程序中的.text一般是代码区,而代码区不能随便修改。

但是动态链接又需要在运行时确定函数地址。

那怎么办?

总不能直接把:

.text

里面的指令改来改去吧。

于是就出现了:

GOT(Global Offset Table,全局偏移量表)


十、GOT到底是干什么的?

GOT 可以理解成程序专门准备的一张“地址表”。

它通常位于可读写的数据区域,因此运行时可以修改。

里面保存的是程序当前需要访问的外部函数或者全局变量的地址。

所以原来的思路:

代码区 ↓ 直接写死外部函数地址

变成:

代码区 ↓ 查GOT ↓ 找到真正地址 ↓ 跳过去

这样就不用直接修改代码区。


十一、为什么每个进程都需要自己的GOT?

假设:

进程A

里面的:

libc.so

加载到了:

0x70000000

而:

进程B

里的:

libc.so

可能加载到了另一个位置。

那么:

进程A里的GOT

和:

进程B里的GOT

当然不能完全一样。

所以动态库代码本身可以共享,但 GOT 不能简单地让所有进程共用。每个进程都要根据自己的地址空间建立对应关系。


十二、PIC为什么和GOT有关?

动态库制作的时候,我们执行过:

gcc-fPIC-cmy_stdio.c

以前只知道:

-fPIC就是生成位置无关代码。

现在再看就容易理解很多了。

动态库需要能够:

加载到不同的地址

还要:

正常访问其中的函数和数据

这就需要相对寻址和 GOT 一起工作。

可以简单记成:

PIC = 相对编址 + GOT

这也是为什么生成动态库的时候需要:

-fPIC

十三、PLT又是干什么的?

如果一个程序依赖很多动态库函数,那么程序启动的时候把所有函数地址全部处理一遍,会比较耗时间。

假设程序依赖:

printf write close strlen ...

但是实际运行的时候,可能只调用其中几个。

那一开始把所有函数都重定位一遍,就有点浪费了。

所以又出现了一种优化:

延迟绑定。

对应的就是:

PLT(Procedure Linkage Table,过程链接表)


十四、延迟绑定是怎么工作的?

一开始:

GOT中的函数地址 ↓ 辅助代码

第一次调用某个函数的时候:

调用函数 ↓ 进入PLT ↓ 查找真正函数地址 ↓ 修改GOT ↓ 跳转到真正函数

等第二次再调用:

调用函数 ↓ PLT ↓ GOT里已经有真正地址 ↓ 直接跳转

也就是说:

第一次调用的时候麻烦一点,后面就可以直接用了。

这样就避免了程序启动时把大量暂时用不到的函数全部处理一遍。


十五、把整个动态链接过程串起来

到这里,可以把动态链接整个过程串起来了。

程序运行:

./main

↓

读取 ELF

↓

找到程序需要的 Segment

↓

建立进程地址空间

↓

加载程序本身

↓

根据依赖关系找到动态库

↓

把动态库映射到当前进程地址空间

↓

确定动态库的加载地址

↓

处理 GOT

↓

调用动态库函数

↓

通过 PLT/GOT 找到真正地址

↓

进入动态库函数执行

这样整个过程就顺起来了。


十六、静态链接和动态链接放在一起

最后再对比一下。

静态链接

hello.o code.o 静态库 ↓ 链接 ↓ 地址重定位 ↓ 独立可执行程序

程序生成以后,静态库本身不需要再参与运行。

动态链接

.o ↓ 生成可执行程序 ↓ 程序运行 ↓ 加载动态库 ↓ 建立地址映射 ↓ GOT / PLT ↓ 调用动态库函数

动态链接把一部分工作放到了程序运行的时候完成,因此需要处理库加载和运行时地址重定位。


十七、这部分知识终于串起来了

以前看到:

.a .so ELF GOT PLT Segment Section

感觉一个比一个陌生。

现在把它们按照程序运行的顺序排起来,就比较容易理解:

源代码 ↓ 编译 ↓ .o ↓ 链接 ↓ ELF可执行文件 ↓ 加载 ↓ 进程地址空间 ↓ 动态库 ↓ GOT ↓ PLT ↓ 真正的库函数

而静态库和动态库的区别,也可以放在这里一起看:

静态库 ↓ 链接阶段进入程序 动态库 ↓ 运行阶段加载和处理

这样再去看:

ldd readelf objdump

这些命令,就不只是记它们的用法了,而是知道自己到底在看什么。

这一部分对我来说比较重要的一点,就是开始慢慢把“编译出来的文件”和“真正运行起来的程序”联系到了一起。

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

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

立即咨询