库、链接与执行
本文将介绍三个部分:可执行文件的编译与运行,静态库的创建以及动态库的创建, 在每个部分中将阐述静态链接器、动态链接器以及Linux内核在三个部分中各自发挥的作用。**
一、可执行文件的编译与运行
概括为:
一个有依赖的程序/动态库 = 嵌入静态库代码 + 嵌入动态库名字 + 可选择的嵌入动态库路径(倘若动态库不在标准路径)
1. 可执行文件的诞生:编译阶段
调用 gcc
可直接生成可执行文件:
gcc -o program.out main.c utils.c (动态库文件) -l库名 -L库路径以通知静态链接器如何找到该库
动态库可无需-l
选项,直接与源文件放在一起。而静态库不行。
在此期间,gcc
完成了以下工作:
- 编译源码:将每个
.c
源文件编译成.o
目标文件(实际由cc1
编译器完成)。其中编译器找头文件的路径为本地/标准路径(/include,usr/include,usr/local/include...) - 调用静态链接器
ld
:将 所有.o
目标文件与静态库中main需要的目标文件链接成最终的可执行文件,并在可执行文件写下其依赖的动态库(如libc.so
)。其中ld要么在标准路径下找库文件,即/lib、/usr/lib等,可通过以下命令查看:
$ ld --verbose | grep SEARCH_DIR
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
,但若指定了-L选项,则优先在该选项指定的路径下找。
如需要将来运行时提示动态链接器所依赖的动态库的地址,可以让ld在程序中写下运行时需要的动态库的地址,供动态链接器查找,即指定选项-Wl,-rpath,/动态库地址/
。一些开箱即用的程序(程序与动态库打包在一起的应用)就是利用了这一特性,将动态库地址指定为'$ORIGIN'/lib
,通知动态链接器该程序需要的共享库在此程序所在目录下的lib目录下。
最终生成的 program.out
中:
- 包含所有
.o
目标文件的代码和静态链接的符号 - 记录动态库依赖信息(如
libc.so.6
),但不会嵌入动态库的代码(即只是记录动态库的名字)
2. 可执行文件的运行
通过 ./program.out
运行程序时:
内核加载可执行文件
- 检查文件头是否为
ELF
格式 - 若需要动态链接,内核加载动态链接器(如
/lib64/ld-linux-x86-64.so.2
)
- 检查文件头是否为
动态链接器工作流程
动态链接器会:- 解析可执行文件的动态段(
.dynamic
),获取依赖的共享库列表 按优先级搜索共享库:
RPATH → LD_LIBRARY_PATH → /etc/ld.so.cache → /lib → /usr/lib
请注意:ld.so.cache由ldconfig程序所维护,ldconfig是引导动态链接器并维护更新三方库的强力工具。
让ldconfig监视除了标准路径外的其他路径的方法:
在/etc/ld.so.conf.d目录下,新建一个.conf配置文件在其中输入你想要让其监视的目录的绝对路径即可,每条路径间换行分隔。
让ldconfig根据其监视的目录下的变化,如新增了一个指定了soname的库,更新ld.so.cache以让动态链接器找到该库,则可以调用以下命令:ldconfig -v | grep 你的库名
- 加载共享库到内存并完成符号重定位
- 执行共享库的初始化代码(如构造函数)
- 将控制权交给程序的
main
函数
- 解析可执行文件的动态段(
二、静态库的创建
编译源文件生成目标文件:
gcc -c utils1.c utils2.c -o utils1.o utils2.c
打包静态库:
ar r libutils.a utils1.o utils2.o
生成
libutils.a
,本质是.o
文件的集合
三、动态库的创建
编译位置无关代码:
gcc -c -fPIC utils.c -o utils.o
-fPIC
生成地址无关代码(该代码会根据进程的不同在不同内存偏移量处产生变量),确保库可被多个进程共享生成共享库:
gcc -shared -o libutils.so utils.o
与静态库一样,还是打包目标文件放在一起。但静态库使用的打包工具是ar
,而动态库的打包直接可以用gcc
编译器。
动态库的版本必须命名如下:libxxx.so.a.b.c
,这里不解释。
如希望默认使用版本a下的最新版本,需指定其soname,即使用gcc的选项-Wl,-soname,/别名/
,soname去掉b.c,且会被ldconfig用来创建一个符号链接,指向版本a下的最新版库。此时便可使用-l:libxxx.so.a
链接版本a下的最新次要版本库。
如希望默认使用该库所有版本的最新版,则需要手动创建一个名为libxxx.so
符号链接指向最新版的共享库。此时该库名称被称为链接器名称。
四、核心组件职责总结
组件 | 作用阶段 | 核心功能 |
---|---|---|
静态链接器 ld | 编译阶段 | 合并目标文件、解析静态符号、生成可执行文件或动态库 |
动态链接器 ld-linux.so | 运行时阶段 | 加载共享库、动态符号解析、地址重定位、管理延迟绑定 |
Linux内核 | 加载阶段 | 验证ELF格式、加载可执行文件到内存、初始化用户态栈、调用动态链接器 |
关键区别总结
特性 | 静态链接 | 动态链接 |
---|---|---|
库代码存储 | 嵌入可执行文件 | 独立存储在 .so 文件 |
内存占用 | 每个进程独立加载 | 多个进程共享同一份库内存 |
更新维护 | 需重新编译程序 | 替换 .so 文件即可生效 |
启动速度 | 较快(无运行时加载开销) | 较慢(需动态链接) |
磁盘空间 | 较大 | 较小 |
发生阶段 | 可执行文件生成阶段 | 运行阶段 |
任务完成者 | 静态链接器ld(被gcc调用) | 动态链接器ld-linux.so |
动静态库即打包的目标文件,通过编译器调用的静态链接器与用户源代码静态链接(合并代码,嵌入共享库名字)即可得到可执行文件。可执行文件需要运行时再动态链接共享库。
可执行文件 = 编译 + 静态链接
库 = 编译 + 打包
通过理解这些机制,开发者可以更好地优化程序架构,在模块化、性能和部署灵活性之间做出平衡。