库制作与原理
这份文档讲解Linux下静态库、动态库的制作、使用,ELF目标文件格式、编译链接、程序加载、虚拟地址空间、动态链接原理,配套gcc选项、Makefile示例、调试命令。
一、库基础概念
库是预编译、可复用的二进制代码;Linux分静态库(.a) 、动态库(.so) ;Windows对应.lib、.dll。
- 头文件
.h:提供函数声明,给编译器看; - 库文件:提供函数二进制实现,给链接器使用。
1.静态库(.a)
- 原理 :编译链接阶段,把库里面的
.o目标文件拷贝合并进可执行程序;程序运行不再依赖.a文件。 - 制作步骤
gcc -c xxx.c得到.o目标文件(只编译不链接)ar -rc libxxx.a *.o归档打包,ar是归档工具。Makefile示例给出完整构建脚本。
- 使用
-I:指定头文件搜索路径;-L:指定库文件搜索路径;-lxxx:链接库(去掉lib前缀、.a后缀);-static:强制使用静态链接。
静态库链接完成后,删除
.a,程序依旧正常运行。
2.动态库(共享库 .so)
-
原理 :编译时只记录依赖,不拷贝代码;运行时由操作系统把.so加载进内存 。多个进程内存可以共享同一份动态库代码,节省磁盘、内存空间;程序运行必须存在
.so文件。 -
制作
- 编译生成位置无关代码:
gcc -fPIC -c xxx.c - 生成动态库:
gcc -shared -o libxxx.so *.o
-fPIC生成位置无关代码,让库可以加载到内存任意地址。 - 编译生成位置无关代码:
-
编译使用 :同样用
-I、-L、-l;编译找库路径≠运行找库路径。 -
动态库运行查找(重点易错)
编译时-L只是告诉链接器 去哪里找库;程序运行时动态链接器会另外搜索库 ,找不到会报not found。
解决4种方案: -
将
.so复制到系统库目录/usr/lib、/lib64; -
创建软链接指向库;
-
设置环境变量
LD_LIBRARY_PATH(临时生效); -
修改
/etc/ld.so.conf.d/配置文件,执行ldconfig更新系统库缓存,永久生效。 -
调试命令:
ldd 可执行程序查看程序依赖哪些动态库,是否找到。
gcc默认优先动态链接;找不到同名
.so才会用静态库。
二、目标文件与ELF格式
.o目标文件、可执行程序、.so动态库、core转储文件,全部属于ELF格式 。
ELF提供两个视图:
- 链接视图(节section,节头表Section Header Table) :编译链接阶段使用,细粒度划分代码数据。
.text:程序机器指令代码;.data:已经初始化的全局、静态变量;.rodata:只读常量,字符串字面量;.bss:未初始化全局/静态变量,只预留内存,不占文件大小;.symtab符号表:记录函数、变量符号;UND代表未定义符号;.got/.got.plt全局偏移表,用于动态链接。
- 执行视图(段segment,程序头表Program Header Table):操作系统加载程序到内存使用。链接器会把属性相同(可读、可写、可执行)的多个section合并为一个segment,减少内存页碎片,方便设置内存权限。
工具命令
readelf -h 文件:查看ELF头部信息;readelf -S 文件:查看所有section节;readelf -l 文件:查看segment段;objdump -d 文件:反汇编查看代码段机器指令。
三、静态链接过程
- 源文件经过
gcc -c编译得到若干.o目标文件。每个.o内部调用外部函数时,不知道外部函数真实地址,call指令地址填0,依靠重定位表后期修正。符号表里标记UND未定义符号。 - 链接器工作:
- 把各个
.o的.text、.data等section合并; - 根据重定位表,修正函数调用地址(静态重定位,编译链接阶段完成地址修正);
- 把静态库里面需要的
.o提取出来,一并合并;
- 把各个
- 输出独立可执行文件,不再依赖原来的
.o和静态库。
四、进程虚拟地址空间 & ELF加载
- ELF可执行文件本身就带有虚拟地址,不是加载到内存才分配地址。
- 操作系统加载可执行程序:读取ELF程序头表,按照segment信息,映射到进程虚拟地址空间;内核task_struct、mm_struct、vm_area_struct结构体描述进程地址空间布局(代码段、数据段、堆、栈、内核空间)。
- 物理内存和进程虚拟地址靠页表映射。
五、动态链接原理(GOT / PLT / PIC)
动态链接把符号地址解析工作推迟到程序运行时。
- PIC位置无关代码:动态库不能写死绝对内存地址,全部用相对寻址。
- GOT全局偏移表 :存函数、变量真实地址;GOT所在段是可读写;代码段.text只读,不能直接修改机器指令,修改地址全部改GOT表。每个进程拥有自己GOT。
- PLT过程链接表,延迟绑定(懒加载) :
- 程序刚启动,不解析全部动态库函数;
- 第一次调用库函数:PLT桩代码调用动态链接器解析符号,把真实地址回填GOT;
- 后续再次调用该函数:直接查GOT跳转,不再解析,提升性能。
- 动态链接器
ld‑linux‑x86‑64.so:程序启动_start入口,调用动态链接器加载所有依赖.so库,完成符号重定位,之后调用__libc_start_main,才执行用户的main函数。 - 动态库被mmap映射进进程虚拟地址空间;物理内存一份库,可以映射到多个不同进程虚拟地址,实现代码共享。
六、gcc编译选项汇总
| 选项 | 作用 |
|---|---|
-c |
只编译,不链接,输出.o目标文件 |
-I(大写i) |
头文件.h搜索目录 |
-L(大写L) |
库文件(.a/.so)搜索目录(编译链接阶段) |
-lxxx(小写L) |
链接libxxx库,去掉lib前缀和后缀 |
-fPIC |
生成位置无关代码,制作动态库必须 |
-shared |
生成动态共享库.so |
-static |
强制静态链接 |
七、重要命令清单
ar -rc:生成静态库;ar -tv查看静态库内部.o;ldd:查看可执行/动态库依赖;readelf:解析ELF头、节、段;objdump -d:反汇编看机器指令;ldconfig:刷新系统动态库缓存。
。