
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux操作系统从入门到实践》《Qt从入门到实践》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[1.1 进程如何看到动态库?](#1.1 进程如何看到动态库?)
[1.2 进程间如何共享库的?](#1.2 进程间如何共享库的?)
[2.1 程序运行前的动态链接准备](#2.1 程序运行前的动态链接准备)
[2.1.1 从可执行文件到进程](#2.1.1 从可执行文件到进程)
[2.1.2 _start 函数的作用](#2.1.2 _start 函数的作用)
[2.1.3 __libc_start_main 的作用](#2.1.3 __libc_start_main 的作用)
[2.2 动态库的地址无关性:PIC 编译](#2.2 动态库的地址无关性:PIC 编译)
[2.3 运行时的地址重定位:从符号到实际地址](#2.3 运行时的地址重定位:从符号到实际地址)
[3.1 全局偏移量表 GOT(Global Offset Table)](#3.1 全局偏移量表 GOT(Global Offset Table))
[3.2 过程链接表 PLT(Procedure Linkage Table):延迟绑定优化](#3.2 过程链接表 PLT(Procedure Linkage Table):延迟绑定优化)
[3.3 库间依赖的处理](#3.3 库间依赖的处理)
[4.1 对比表格](#4.1 对比表格)
[4.2 应用场景选择](#4.2 应用场景选择)
[五、ELF 文件分析工具详解(补充)](#五、ELF 文件分析工具详解(补充))
[5.1 ELF 结构查看工具](#5.1 ELF 结构查看工具)
[5.1.1 ELF Header](#5.1.1 ELF Header)
[5.1.2 Program Header Table](#5.1.2 Program Header Table)
[5.1.3 Section Header Table](#5.1.3 Section Header Table)
[5.1.4 查看符号表](#5.1.4 查看符号表)
[5.1.5 查看重定位表](#5.1.5 查看重定位表)
[5.2 反汇编工具](#5.2 反汇编工具)
[5.2.1 objdump](#5.2.1 objdump)
[5.3 查看动态依赖](#5.3 查看动态依赖)
前言
在 Linux 程序开发中,动态库是实现代码复用、精简可执行文件体积、节约系统内存资源的关键技术,动态链接则是支撑动态库运行、实现运行时符号绑定的底层核心机制。相较于静态链接将库代码直接合并到可执行程序不同,动态链接将符号重定位推迟至程序运行阶段,实现了一份库文件被多个进程共享调用,极大提升了系统资源利用率。本文将围绕进程与动态库的关联、动态链接底层原理、GOT/PLT 延迟绑定机制等核心内容,深度拆解 Linux 下动态库的加载与调用全过程,带你吃透 Linux 底层开发的核心知识点。
一、进程如何感知并加载动态库
动态库 本质上是一个符合 ELF 格式 的二进制文件 ,进程要使用动态库中的函数和数据,首先要让动态库被加载到内存 并映射到进程的虚拟地址空间中,这是进程能访问动态库的前提。
1.1 进程如何看到动态库?
进程本身并不能直接识别磁盘上的动态库文件,而是通过操作系统的文件操作和内存映射机制 实现对动态库的访问。
当程序运行时,操作系统会根据程序的依赖信息,找到对应的动态库文件并打开,随后通过mmap 系统调用将动态库的代码段、数据段等映射到进程的虚拟地址空间的共享区,让进程在虚拟地址层面能 "看到" 动态库的内容。

1.2 进程间如何共享库的?
Linux 系统中,多个依赖同一动态库的进程,并不会在物理内存中加载多份库的副本,而是通过虚拟内存的页表映射机制实现共享:
- 动态库 被加载到物理内存后,操作系统会为其建立一份物理内存映射;
- 每个使用该动态库的进程,其页表会将虚拟地址空间共享区的一段地址,映射到这份物理内存;
- 进程对动态库的访问,最终都会转化为对同一份物理内存的访问,从而实现物理内存层面的库共享。
这种机制极大节省了物理内存资源,也是动态链接相比静态链接的核心优势之一。

二、动态链接的核心工作原理
动态链接的核心是将符号解析和地址重定位从编译链接阶段推迟到程序运行阶段。编译器编译生成可执行程序时,并不会将动态库的函数地址、变量地址直接写入程序,而只是记录下依赖的动态库和符号信息;当程序运行时,动态链接器会完成符号的解析和地址的重定位,让程序能正确调用动态库中的函数。
2.1 程序运行前的动态链接准备
2.1.1 从可执行文件到进程
当用户执行一个程序时:
- 操作系统创建进程,为其分配资源
- 读取 ELF 头,检查文件格式和类型
- 根据程序头表将各个段加载到内存
- 如果是动态链接 的程序,加载动态链接器
- 跳转到程序入口点 _start
2.1.2 _start 函数的作用
在 C/C++ 程序中,当程序开始执行时,它首先并不会直接跳转到 main 函数。实际上,程序的入口点并非我们编写的 main 函数,而是链接器提供的 _start 函数。这是一个由 C 运行时库(通常是 glibc)或链接器(如 ld)提供的特殊函数。
动态链接的初始化工作正是在 _start 函数中完成的,其流程如下:
- 设置堆栈:为程序创建初始的堆栈环境,保证函数调用的栈操作正常;
- 初始化数据段:将初始化的全局变量、静态变量从可执行程序复制到内存,清零未初始化的bss段;
- 加载动态链接器 :调用系统接口加载 Linux 的动态链接器ld-
linux.so,由其负责后续的动态链接工作; - 解析库依赖:动态链接器读取可执行程序的动态段信息,解析出程序依赖的所有动态库(可通过ldd命令查看程序的库依赖);
- 加载并映射动态库:按依赖顺序加载所有动态库,将其映射到进程的虚拟地址空间;
- 调用__libc_start_main: 完成信号处理、线程库初始化等工作后,最终调用 main 函数 ,将程序控制权交给用户代码。
其中,动态链接器 (如 ld-linux.so)是动态链接 的核心执行者,Linux 下的 ld-linux.so 负责处理所有动态库的加载、符号解析和地址重定位。
# ldd命令⽤于打印程序或者库⽂件所依赖的共享库列表。
$ ldd main.exe
linux-vdso.so.1 => (0x00007ffefd43f000)
libc.so.6 => /lib64/libc.so.6 (0x00007f533380b000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5333bd9000)

2.1.3 __libc_start_main 的作用
一旦动态链接完成 ,_start 函数会调用 __libc_start_main(这是 glibc 提供的一个函数)。__libc_start_main 函数负责执行一些额外的初始化工作,比如:
- 设置信号处理函数
- 初始化线程库(如果使用了线程)
- 调用程序的 main 函数
当main 函数返回 时,__libc_start_main 会负责处理这个返回值 ,并最终调用 _exit 函数来终止程序。
2.2 动态库的地址无关性:PIC 编译
动态库被加载到进程虚拟地址空间的地址是不固定的 ,操作系统会根据当前内存的使用情况,为动态库分配合适的虚拟地址区间。为了让动态库能在任意地址加载后都能正常运行,动态库必须采用位置无关代码(Position Independent Code,PIC) 编译。
还记得前面我们手动实现一个动态库的制作,在编译 .c 文件变成 .o 文件时,是不是加了一个 -fPIC 的选项,其实也就是采用位置无关代码编译,但下面还会对这个选项进一步讲解。
PIC 的核心是相对编址 :动态库中的函数调用、变量访问,均使用相对于当前指令的偏移量 进行编址,而非绝对地址 。这样无论动态库被加载到虚拟地址空间的哪个位置,只要根据偏移量计算,就能正确找到目标函数或变量,实现地址无关性。
提问:我们的程序,怎么和库具体映射起来的?
- 动态库也是一个文件,要访问也是要被先加载,要加载也是要被打开的
- 让我们的进程找到动态库的本质:也是文件操作,不过我们访问库函数,通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中

2.3 运行时的地址重定位:从符号到实际地址
当动态库被加载到进程的虚拟地址空间 后,其虚拟起始地址就被确定了。动态链接器会完成两步核心工作,实现程序对动态库符号的访问:
- 符号解析:根据可执行程序记录的符号名(如函数名、变量名),在已加载的动态库中找到对应的符号;
- 地址计算:结合动态库的虚拟起始地址和符号在库中的相对偏移量,计算出符号的实际虚拟地址;
- 地址重定位:将计算出的实际虚拟地址写入程序的指定位置,让程序能通过该地址调用访问动态库函数。
提问:我们的程序,怎么进行库函数调用?
- 动态库已经被我们映射到了当前进程的地址空间中
- 库的虚拟起始地址我们也已经知道了
- 库中每个方法的偏移量地址我们也知道
- 所以:访问库中任意方法 ,只需要知道库的起始虚拟地址 + 方法偏移量即可定位库中的方法
- 而且:整个调用过程,是从代码区跳转到共享区,调用完毕再返回到代码区,整个过程完全在进程地址空间中进行的。
简单来说,程序调用动态库函数的地址,最终是动态库起始虚拟地址 + 函数在库中的相对偏移量,这也是进程能正确调用动态库函数的关键。
补充点:
在软件 / 进程视角 :我们只使用虚拟地址 完成函数跳转、调用、返回 ,感知不到物理地址、页表、MMU;
在硬件层面 :CPU 通过 MMU + 页表自动完成虚拟地址→物理地址的翻译,访问物理内存执行指令;
因此软件层面可以理解为:虚拟地址映射绑定物理地址后,直接用虚拟地址即可完成函数调用,硬件翻译过程对上层软件完全透明、不可见。所以这也间接解释了为什么调用的整个过程完全在进程地址空间中进行的

三、GOT/PLT:动态链接的核心实现机制
程序的代码段 在内存中是只读 的,无法直接在代码段中修改函数调用的地址,因此 Linux 通过全局偏移量表(GOT) 和过程链接表(PLT) 解决这一问题,实现了只读代码段的动态地址重定位,也是 PIC 的核心实现。
3.1 全局偏移量表 GOT(Global Offset Table)
问题:
- 我们程序运行之前,先把所有库加载并映射,所有库的起始虚拟地址 都应该提前知道
- 然后对我们加载到内存中的程序的库函数调用 进行地址修改 ,在内存中二次完成地址设置 (这个叫做加载地址重定位)
- 等等,修改的是代码区?不是说代码区在进程中是只读的吗?怎么修改?能修改吗?
回答:
代码区是只读的完全不可修改 ,这是事实了。那难道上面的图中下面部分难道是错的吗?是的!
所以我们不能够在代码区中对地址进行操作,那怎么实现加载地址重定向呢?代码区是只读的,但是数据区是可读可写的。
解决方案:
动态链接 采用的做法是在 数据区 .data (可执行程序或者库自己)中专门预留一片区域 用来存放函数的跳转地址 ,它也被叫做全局偏移表 GOT,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。
因为 .data 区域是可读写的,所以动态链接器在程序运行时,能够动态修改 GOT 表中的地址值。
关键特性:
- 由于代码段只读,我们不能直接修改代码段。但有了 GOT 表,代码便可以被所有进程共享。但在不同进程 的地址空间 中,各动态库的绝对地址、相对位置都不同 。反映到 GOT 表上,就是每个进程的每个动态库都有独立的 GOT 表 ,所以进程间不能共享 GOT 表。
- 在单个 .so 下,由于GOT 表与 .text 的相对位置是固定的,我们完全可以利用 CPU 的相对寻址来找到 GOT 表。
- 在调用函数的时候会首先查表,然后根据表中的地址来进行跳转,这些地址在动态库加载 的时候会被修改为真正的地址。
- 这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定**-fPIC** 参数的原因,PIC = 相对编址 + GOT。
-fPIC选项的补充点:

3.2 过程链接表 PLT(Procedure Linkage Table):延迟绑定优化
动态链接器如果在程序启动时,就对所有动态库符号进行解析和重定位,会增加程序的启动时间 ------ 因为程序运行过程中,很多动态库函数可能一次都不会被调用。为了解决这一问题,Linux 引入了延迟绑定(Lazy Binding) 机制,其核心实现就是过程链接表(PLT) 。
PLT 是一段位于程序代码段的桩代码(stub code),每个动态库函数对应一个 PLT 条目,其工作流程分为第一次调用和后续调用:
(1)函数第一次被调用
- 程序调用动态库函数时,首先跳转到该函数对应的 PLT 条目;
- PLT 条目会读取 GOT 表中对应的条目,此时 GOT 表中的值指向 PLT 条目的下一条指令;
- 该指令会调用动态链接器的符号解析函数,动态链接器会解析出函数的实际虚拟地址,并将其写入 GOT 表对应的条目;
- 动态链接器跳转到函数的实际地址,执行函数逻辑。
(2)函数后续被调用
- 程序再次跳转到 PLT 条目时,会直接读取 GOT 表中的值,此时该值已经是函数的实际虚拟地址;
- 程序直接跳转到该地址执行函数,不再经过动态链接器的解析,实现了调用的优化。
延迟绑定将符号解析的工作推迟到函数第一次被调用时,大幅减少了程序的启动时间,是 Linux 动态链接的重要优化手段。
延迟绑定的实现思路:
GOT中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码/stup。在我们第一次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。

3.3 库间依赖的处理
动态库之间也存在依赖关系(如库 A 依赖库 B),其处理方式与程序依赖动态库一致:
- 动态链接器会按依赖顺序加载所有的动态库,包括库的依赖库;
- 每个动态库也都有自己独立的 GOT 表,动态链接器会依次解析所有库间的符号依赖,完善各个 GOT 表,这也就是为什么大家都是 ELF 的格式;
- 库间的函数调用,同样通过GOT 表 + 相对偏移的方式实现,保证了库间调用的地址无关性。
所有动态库的 GOT 表完善后,整个程序的动态链接过程才算完成,程序才能正常运行。
四、动态链接与静态链接的核心对比
4.1 对比表格
为了更清晰地理解动态链接的优势和特点,我们将其与静态链接做核心维度的对比,如下表所示:
| 对比维度 | 静态链接/静态库 | 动态链接/动态库 |
|---|---|---|
| 链接时机 | 编译时 | 运行时 |
| 文件后缀 | .a | .so |
| 可执行程序体积 | 可执行文件较大,包含所有库代码 | 可执行文件较小,仅记录库依赖和符号信息 |
| 内存占用 | 每个进程独立加载一份库代码 | 多个进程共享物理内存中的库副本 |
| 磁盘占用 | **高,**多个程序包含重复库代码 | **低,**系统中仅存一份动态库文件 |
| 加载速度 | 较快 | 较慢 |
| 更新方式 | 需重新编译 | 替换库文件即可 |
| 依赖性 | 不依赖外部库 | 依赖动态库存在 |
| 编译选项 | -static 强制静态链接 | -shared 生成动态库 |
| 生成工具 | ar -rc | gcc -shared -fPIC |
| 兼容性 | 好,可执行程序独立运行 | 依赖库版本,库版本不兼容可能导致程序崩溃 |
- 静态链接的出现,提高了程序的模块化水平。对于一个大的项目,不同的人可以独立地测试和开发自己的模块。通过静态链接,生成最终的可执行文件。
- 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并成一个独立的可执行文件,其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。
- 而动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间,然后通过.GOT方式进行调用(运行重定位,也叫做动态地址重定位)。
可以看到,动态链接以微小的运行性能开销,换来了系统资源的高效利用和程序的灵活更新,这也是 Linux 系统中绝大多数程序都采用动态链接的原因。
4.2 应用场景选择
静态库适用场景:
- 需要在没有安装对应动态库的环境中运行
- 对程序启动速度有较高要求
- 程序体积不是主要考虑因素
- 嵌入式系统或资源受限环境
动态库适用场景:
- 多个程序共享同一库
- 需要减小可执行文件体积
- 希望能够独立更新库而不需要重新编译程序
- 系统中存在多个依赖同一库的程序
五、ELF 文件分析工具详解(补充)
5.1 ELF 结构查看工具
5.1.1 ELF Header
使用**-h** 或 --file-header 选项:
bash
$ readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060
Start of program headers: 64 (bytes into file)
Start of section headers: 14768 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
关键字段解释:
- Magic :ELF 文件标识符(魔数),固定为
7f 45 4c 46 - Class:文件类,32 位 (01) 或 64 位 (02)
- Data:数据编码方式,小端序或大端序
- Type:文件类型,如可重定位文件(REL)、可执行文件(EXEC)、共享目标文件(DYN)等
- Machine:目标架构,如 x86‑64
- Entry point address:程序入口点虚拟地址
- Start of program headers:程序头表起始偏移
- Start of section headers:节头表起始偏移
对比目标文件和可执行文件:
目标文件 hello.o:
- Type: REL (Relocatable file)
- Entry point address:
0x0 - Start of program headers:
0(bytes into file)(目标文件没有程序头表)
可执行文件 a.out:
- Type: DYN (Shared object file) 或 EXEC (Executable file)
- Entry point address 有具体值(如
0x1060) - 有完整的程序头表和节头表
5.1.2 Program Header Table
使用**-l** 或**--program-headers**选项:
bash
$ readelf -l a.out
Elf file type is EXEC (Executable file)
Entry point 0x4003e0
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000744 0x0000000000000744 R E 200000
LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x0000000000000218 0x0000000000000220 RW 200000
DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
0x00000000000001d0 0x00000000000001d0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000005a0 0x00000000004005a0 0x00000000004005a0
0x000000000000004c 0x000000000000004c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x00000000000001f0 0x00000000000001f0 R 1
关键字段:
- Type :段类型,如
LOAD(需要加载到内存)、INTERP(解释器信息)、DYNAMIC(动态链接信息) - Offset:段在文件中的偏移量
- VirtAddr:段在虚拟地址空间中的地址
- PhysAddr:段在物理地址空间中的地址(现代 OS 中可以不考虑)
- FileSiz:段在文件中的大小
- MemSiz :段在内存中的大小(可能大于 FileSiz,如
.bss段) - Flags :访问权限,
R(可读)、W(可写)、E(可执行) - Align:对齐要求
Section to Segment mapping:
bash
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr
.gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text
.fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
...
这显示了哪些 Section 被合并到哪个 Segment 中。
5.1.3 Section Header Table
使用**-S** 或**--section-headers**选项:
bash
$ readelf -S a.out
There are 31 section headers, starting at offset 0x19d8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
...
[11] .init PROGBITS 0000000000400390 00000390
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400590 00000590
00000000000000a0 0000000000000010 AX 0 0 16
[14] .text PROGBITS 0000000000400640 00000640
00000000000004b2 0000000000000000 AX 0 0 16
[23] .got PROGBITS 0000000000600ff8 00000ff8
0000000000000008 0000000000000000 WA 0 0 8
[24] .got.plt PROGBITS 0000000000601000 00001000
0000000000000060 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000601060 00001060
0000000000000004 0000000000000000 WA 0 0 1
[26] .bss NOBITS 0000000000601064 00001064
0000000000000004 0000000000000000 WA 0 0 1
...
关键字段:
- Name:节名称
- Type:节类型,如 PROGBITS(程序内容)、NOTE(注释信息)、NOBITS(不占用空间,如 .bss)
- Address:节在虚拟地址空间中的地址
- Offset:节在文件中的偏移量
- Size:节的大小
- Flags:访问权限,A(Allocatable)、W(可写)、X(可执行)、M(Merge)、S(Strings)
- Link:关联的其他节
- Info:附加信息
- Align:对齐要求
5.1.4 查看符号表
使用**-s** 或**--symbols**选项:
bash
$ readelf -s a.out
Symbol table '.dynsym' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
...
符号类型说明:
- FUNC:函数
- **OBJECT:**对象(变量)
- **NOTYPE:**未指定类型
- **SECTION:**节
- **FILE:**文件名
绑定属性(Bind):
- **LOCAL:**局部符号,只在目标文件内部可见
- **GLOBAL:**全局符号,可被其他目标文件引用
- **WEAK:**弱符号,如果存在同名的全局符号,弱符号会被忽略
5.1.5 查看重定位表
使用**-r** 或 --relocs选项:
bash
$ readelf -r a.out
Relocation section '.rela.dyn' at offset 0x360 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
0000000000600e20 000000000008000000 R_X86_64_RELATIVE 600e10
重定位类型:
- **R_X86_64_RELATIVE:**相对重定位
- **R_X86_64_JUMP_SLOT:**跳转槽重定位(用于 PLT/GOT)
- **R_X86_64_64:**64 位绝对地址重定位
5.2 反汇编工具
5.2.1 objdump
反汇编代码段:
bash
$ objdump -d a.out
反汇编并显示源码 (需要编译时加 -g选项):
bash
$ objdump -S a.out
查看特定 section:
bash
$ objdump -s -j .text a.out
5.3 查看动态依赖
bash
$ ldd a.out
linux-vdso.so.1 => (0x00007fffdd85f000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f42c025a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f42c0068000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f42bffd7000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f42bffd1000)
/lib64/ld-linux-x86-64.so.2 (0x00007f42c02b6000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f42bffae000)
六、总结
到此, Linux 动静态库的制作与使用、ELF 文件结构、程序加载过程等核心知识点我们就全部讲解完了。对此我们将这些内容进行一个总览:
核心要点回顾
-
库的本质:库是写好的、现有的、成熟的、可以复用的代码,本质上是一种可执行代码的二进制形式。
-
静态库:在编译链接时将库代码链接到可执行文件中,程序运行时不再需要静态库。
-
动态库:在运行时才去链接动态库的代码,多个程序可以共享同一份动态库的物理内存。
-
ELF 文件结构:
(1)ELF 头:描述文件的主要特性
(2)程序头表:列举所有有效的段和它们的属性(执行视图)
(3)节头表:包含对节的描述(链接视图)
(4)节:ELF 文件中的基本组成单位 -
链接视图 vs 执行视图:
(1)链接视图:对应节头表,用于静态链接分析
(2)执行视图:对应程序头表,用于操作系统加载程序 -
静态链接过程:将多个目标文件和静态库合并成可执行文件,包括符号解析和地址重定位。
-
动态链接过程:
(1)加载动态链接器
(2)解析程序依赖关系
(3)加载动态库到进程地址空间
(4)符号解析和重定位(通过 GOT/PLT 实现)
(5)执行初始化函数
(6)跳转到程序入口点 -
GOT 和 PLT:
(1)GOT(全局偏移表):存储外部符号的真实地址,位于数据段,可读写
(2)PLT(过程链接表):提供跳转到 GOT 的代码片段,位于代码段,只读
(3)延迟绑定:将符号解析推迟到第一次调用时 -
程序启动流程:
(1)操作系统加载 ELF 文件
(2)跳转到_start函数
(3)_start调用__libc_start_main
(4)__libc_start_main执行初始化,包括动态链接
(5)调用main函数
(6)main返回后,程序退出
