1. 什么是库
通常来说代码需要运行起来需要4个阶段(预处理、编译、汇编、链接),库通俗的来讲就是一些经过汇编后形成的**.o**文件的集合。库又分为静态库和动态库,静态库 .a[Linux]、.lib[windows] 动态库 .so[Linux]、.dll[windows]。我们只要说明Linux下的库,不论是静态库还是动态库都是以lib开头的,主要以后缀区分动静态库。去掉前后缀才是库的名字。
2.静态库
一个可执行程序可能链接很多的库,这些库有些是静态库有些是动态库,编译器默认是链接动态库,只有在该库里找不到动态库才会使用同名的静态库。也可以用gcc的-static强制链接静态库。静态库在链接是会链接到可执行文件中,程序运行时就不再需要静态库了。
2-1 静态库⽣成与使用
静态库在命令行下一般使用**++ar rc 静态库全称 .o文件++** 的形式来生成ar 是 gnu 归档⼯具, rc 表⽰ (replace and create)。想使用静态库时麻烦一点的方法是**++gcc -L加库路径 -l加库名 -static -I加头文件路径++**,库⽂件名称和引⼊库的名称:去掉前缀 lib ,去掉后缀 .so , .a ,如: libc.so -> c。还可以把头文件和库文件安装到系统路径下。
3.动态库
动态库在程序运行时才会链接的,动态库也叫共享库,它位于进程的共享区内。系统在创建进程的数据结构对象并进行一定初始化后再加载动态库到内存中,动态库可以在多个程序间共享,所以动态链接使得可执⾏⽂件更⼩,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的⼀份动态库被要⽤到该库的所有进程共⽤,节省了内存和磁盘空间。
3-1 动态库生成与使用
与静态库的生成不同动态库可以直接使用gcc编译器生成,即**++gcc 动态库全称 .o文件 -shared++** ,而且再形成.o文件时也要加上-fPIC。shared: 表⽰⽣成共享库格式 fPIC:产⽣位置⽆关码( position independent code)。动态库在链接阶段和静态库的使用方法一样,只不过经过链接形成的可执行文件不能直接运行,必须让加载器能找到你的动态库。要么将库拷贝到系统共享库路径下、要么向系统共享库路径下建立同名软连接、还可以改变环境变量LD_LIBRARY_PATH,但是环境变量需要每次开机时都要更改。
//Makefile
libmystdio.so:mystdio.o mystring.o
gcc -shared -o $@ $^
%.o:%.c
gcc -fPIC -c *.c
.PHONY:clean
clean:
rm -rf *.o libmystdio.so mylib
.phony:out
out:
mkdir -p mylib
mkdir -p mylib/include
mkdir -p mylib/lib
cp *.so mylib/lib
cp *.h mylib/include
4.⽬标⽂件与ELF文件
目标文件全称为可重定位的目标文件 ,也就是gcc -c后形成的二进制文件,文件的格式是ELF格式。可重定位文件、可执行文件、共享目标文件即.so动态库文件、内核转储 都是ELF文件。一个ELF文件分为ELF头、程序头表、节头表、节 这四个部分,ELF头 核心内容是文件类型、程序头表/节头表的偏移地址、大小、条目数等,它能告诉工具去哪找找什么。程序头表 仅可执行文件/共享库有,可重定位文件也有程序头表,只是通常为空。他把权限、内存属性相同的节打包成一个个的段,操作系统在加载文件时,内存分配的最小单位通常为 4KB(页大小),若单个节的大小不足 4KB,直接映射会导致内存页内空间浪费;而将同类节打包成段后,既能按段为内存区域分配读写 / 执行权限,也能减少内存页的碎片化,提升内存利用效率。段本质上是操作系统加载 ELF 文件时的内存管理单元,是节在运行时的逻辑聚合形式 。节头表 包含了对节的描述,有节的大小偏移量所属段等。节是ELF⽂件中的基本组成单位,包含了特定类型的数据。ELF⽂件的各种信息和 数据都存储在不同的节中,如代码节存储了可执⾏代码,数据节存储了全局变量和静态数据等。

5. ELF从形成到加载轮廓
ELF 可执行文件的生成核心是链接(Linking) 过程:链接器(如 ld)会将编译生成的多个可重定位目标文件(.o)、以及程序依赖的静态库(归档文件 .a,本质是多个 .o 的集合)/ 动态库(.so)进行整合 ------ 不仅是简单的二进制内容合并,还会完成符号解析 (绑定函数 / 变量的引用与定义)、重定位 (修正符号的内存地址),最终生成单一的 ELF 可执行文件。操作系统加载 ELF 可执行文件时,核心流程为:1. 加载器(Loader)首先读取 ELF 头,从中解析出程序头表的起始偏移地址、表项数量等信息;2. 遍历程序头表中的每个表项(对应一个段),每个表项包含该段的 "文件偏移(在 ELF 文件中的位置)、目标虚拟地址(加载到内存的地址)、段大小、权限属性(读 / 写 / 执行)" 等;3. 加载器根据这些信息,将 ELF 文件中的段从 "文件偏移位置" 读取出来,映射到进程虚拟地址空间的 "目标虚拟地址" 处(按页大小 4KB 对齐),并为该内存区域设置对应的权限;**4.**所有段加载完成后,加载器跳转到 ELF 头指定的入口地址(_start),开始执行程序
6.理解连接与加载
6.1静态链接
静态链接就是将库和自己的目标文件的同名节合并成一个完整的节,再把所以为解析的地址(如call函数)全部重定位修改符号表的过程。
6.2ELF加载与进程地址空间
ELF 文件在未加载到内存时已分配虚拟地址(而非物理地址),但不同类型的 ELF 文件地址编排规则不同:
- 可重定位目标文件(.o):编译器采用 "地址无关编码" 模式,为代码 / 数据段分配相对偏移地址(通常从 0 开始),此时的地址仅用于链接阶段的符号解析和重定位,并非最终运行地址;
- 可执行 ELF 文件:链接器完成静态链接后,会为整个程序分配预定义的虚拟地址空间 (如 Linux 系统默认起始基址为 0x400000),并将程序入口地址(_start 函数的虚拟地址)记录在 ELF 头(ELF Header)的
e_entry字段中(注:现代系统开启 ASLR 后,基址会随机化,入口地址也会动态调整)。
当可执行文件被执行时,操作系统(OS)的加载流程和 CPU 寻址流程如下:
步骤 1:进程创建与虚拟地址空间初始化
OS 首先创建进程的核心数据结构(如 task_struct、mm_struct),为进程分配独立的虚拟地址空间,并初始化页表(此时页表仅建立虚拟地址区间的 "占位",无实际物理地址映射)。
步骤 2:ELF 加载与虚拟地址映射
加载器读取 ELF 头的程序头表,将可执行文件的段(Segment)映射到进程虚拟地址空间的指定区间(即 "分配起始虚拟地址 + 段内偏移" 得到段内每个指令 / 数据的虚拟地址)。此时仅建立 "虚拟地址 - 磁盘文件偏移" 的映射,未分配物理内存。
步骤 3:缺页中断与物理地址分配
CPU 执行指令时,内存管理单元(MMU)会通过页表将虚拟地址转换为物理地址:
- 若虚拟地址对应的页表项为空(无物理地址映射),MMU 触发缺页中断;
- 内核响应中断,为该虚拟页分配物理内存页,将磁盘中的指令 / 数据加载到物理页,然后更新页表(填充 "虚拟地址 - 物理地址" 的映射关系);
- 缺页中断处理完成后,CPU 重新执行该指令(页表填充是按需进行的,而非一次性完成)。
步骤 4:CPU 指令执行与地址寻址流程
- OS 将 ELF 头中记录的程序入口虚拟地址写入 CPU 的指令指针寄存器(EIP/RIP),EIP 始终存储 "下一条要执行的指令的虚拟地址";
- CPU 读取 EIP 中的虚拟地址,MMU 首先读取页表基址寄存器(CR3)(CR3 存储当前进程页表的物理基址),通过 CR3 找到全局页目录(PGD);
- MMU 逐级遍历页表(PGD → PUD → PMD → PTE),将虚拟地址转换为物理地址;
- CPU 从物理地址中读取指令并执行,执行完成后 EIP 自动增加(或根据指令跳转),指向 "下一条指令的虚拟地址";
- 重复上述寻址 - 执行流程,直至程序结束。

6.3动态链接与动态库加载
动态库加载大体上跟ELF没有区别主要是加载时机重定位方式基址处理有差别。在进程对象创建初始化一会后先加载动态库再加载可执行文件,由于动态库形成之前的-fPIC也就是与位置无关码导致其没有固定的基地址,因此加载器会给动态库分配一个起始虚拟地址然后通过修改GOT表里原本的虚拟地址再加上偏移量形成真正的虚拟地址,再结合物理地址修改页表。由于动态库是加载到内存中的而静态库是把所以目标文件结合到一块因此才有了动态库比静态库更节省空间的说法。

1.由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不 同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的 每个动态库都有独⽴的GOT表,所以进程间不能共享GOT表。 2.在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利⽤CPU的相对寻址来找 到GOT表。 3.在调⽤函数的时候会⾸先查表,然后根据表中的地址来进⾏跳转,这些地址在动态库加载的时候会 被修改为真正的地址。 4. 这种⽅式实现的动态链接就被叫做 PIC 地址⽆关代码 。换句话说,我们的动态库不需要做任何修 改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给 编译器指定-fPIC参数的原因,PIC=相对编址+GOT。