动态库(.so文件)是Linux开发中常用的共享代码模块。本文通过实际测试项目,深入解析动态库的编译、链接和运行时加载过程,帮助开发者理解其底层实现原理。
测试项目简介
为了更好地理解Linux动态库,建议准备一个实际的测试项目进行调试学习。测试项目的目录结构如下:

example.c和example.h文件将被编译为libexample.so动态库文件,其中定义了两个函数(hello函数和add函数)以及一个全局变量global。
main.c文件将被编译成可执行文件,main函数调用hello和add函数,并将global变量重新赋值。编译可执行文件时需要链接libexample.so动态库。
动态库文件特性
动态库文件是在程序运行时才被加载和链接的共享代码模块。Linux系统中通常以.so(Shared Object) 结尾,例如libm.so(数学库)、libc.so(C标准库)。
动态库具有以下特点:
- 代码共享:多个程序可以共享同一份动态库代码,节省磁盘空间和内存资源
- 运行时加载:动态库在程序运行时才被加载到内存
- 位置无关代码(PIC):动态库可加载到不同的内存地址,避免地址冲突
动态库文件本质上是一种ELF文件。ELF文件基础介绍可参考计算机基础相关内容。
动态库文件的ELF格式如下:

通过readelf -h 动态库文件命令查看ELF文件头,可以看到动态库文件类型是DYN(Shared object file),表示文件包含位置无关代码(通过gcc -fPIC编译)。
动态库文件的程序入口点(Entry point address)为0,表示动态库不是独立程序,没有自己的入口点。动态库文件无INTERP段(不指定动态链接器路径),因为其主要功能是提供函数和数据供其他程序调用,而不是独立运行。
编译和链接阶段
动态库的编译必须使用-fPIC编译选项,该选项告诉编译器生成位置无关代码:
gcc -fPIC -shared -o libexample.so example.c
编译可执行ELF文件链接动态库的命令如下:
gcc main.c -o a.out -I. -L. -lexample -Wl,-rpath='$ORIGIN':/opt
参数说明:
-L选项:用于编译时指定动态库搜索路径
-l选项:用于编译时指定动态库文件名(去掉lib前缀和扩展名)
-Wl,-rpath选项:用于向可执行文件中嵌入运行时搜索路径(Runpath)
编译完成后,可执行文件(a.out)的ELF格式如下:

可执行文件如果链接了动态库,ELF文件中会有几个特殊的节:.interp节、.dynsym节、.dynamic节。
.interp节
.interp节包含动态链接器路径,用于指定可执行文件运行时使用哪个动态链接器来加载和解析动态库。
通过readelf -d 可执行文件命令可以查看.interp节信息:
# readelf -p .interp a.out
String dump of section '.interp':
[ 0] /lib/ld-linux-aarch64.so.1
/lib/ld-linux-aarch64.so.1为动态链接器路径。当操作系统启动一个可执行文件时,它会读取.interp节中的路径,找到并加载指定的动态链接器。
注意:动态链接器也是一个动态库,负责在程序运行时加载和解析动态库,并将动态库定义的符号(函数和变量)绑定到程序中。
.dynsym节
.dynsym节是ELF文件中的动态符号表,包含动态链接时所需的符号信息。这些符号通常是全局变量和函数,在程序运行时被动态加载和解析。
通过readelf -sD 可执行文件命令可以查看.dynsym节:

字段解析:
- Num:符号表中的条目编号
- Value:符号的地址或值。对于已定义符号,这通常是一个虚拟地址;对于未定义的符号(如UND),该值为0
- Size:符号的大小,以字节为单位
- Type:符号的类型(NOTYPE、SECTION、FUNC、OBJECT等)
- Bind:符号的绑定属性(LOCAL、GLOBAL、WEAK)
- Vis:符号的可见性(DEFAULT、HIDDEN、INTERNAL、PROTECTED)
- Ndx:符号所在的节的索引
- Name:符号的名称及版本信息
.dynamic节
.dynamic节存储动态链接器在运行时需要的信息,包括动态库依赖关系、动态库搜索路径、节的位置等。
通过readelf -d 可执行文件命令可以查看.dynamic节:

.dynamic节中需要关注的信息:
- NEEDED:程序运行时需要加载的动态库
- RUNPATH:运行时库搜索路径,优先级低于LD_LIBRARY_PATH,高于系统默认路径
运行时动态链接阶段
当一个可执行文件链接了动态库,系统启动可执行程序时,除了要加载可执行文件的LOAD段,还需要通过动态连接器加载动态库文件的LOAD段。整个过程如下图所示:

用户程序通过调用execve系统调用(或execve家族函数)来加载可执行文件。execve会用新程序的代码、数据、堆和栈来替换当前进程的虚拟地址空间的内容,并执行新程序。
execve加载ELF文件的主要流程在load_elf_binary函数中实现,该函数主要完成两件事情:加载可执行文件和加载动态库文件。
加载可执行文件
步骤1:加载程序头表
程序头表每个条目都是一个段(如INTERP段、LOAD段等),内核通过解析段将ELF文件相关数据加载至内存。
步骤2:解析INTERP段
INTERP段只包含一个节(.interp节),其中存储的是动态链接器路径。解析INTERP段的目的是加载动态链接器,为后续加载动态库文件做准备。
步骤3:设置栈区
执行新程序需要重新设置栈区,保证新程序有正确的执行环境。
步骤4:加载LOAD段
LOAD段记录的是需要加载进内存的节(如.text、.data、.bss等)。内核会通过mmap文件或匿名映射将可执行文件的LOAD段加载至内存。
步骤5:加载动态链接器LOAD段
将动态链接器通过mmap文件映射方式映射至内存映射区。动态链接器也是动态库,加载其LOAD段即可。
步骤6:设置堆区
同栈区设置。
步骤7:跳转至动态链接器入口点
完成以上工作后,跳转至动态库链接器入口点启动动态链接器。
加载动态库
程序控制权转移至动态链接器后,动态链接器开始加载可执行文件依赖的动态库。
步骤1:解析ELF可执行文件DYNAMIC段
DYNAMIC段记录可执行文件依赖的动态库路径以及自定义动态库搜索路径(RUNPATH)。
步骤2:顺序搜索动态库
根据DYNAMIC段提供的信息,动态链接器在指定路径下搜索动态库。按照优先级从高到低的顺序,动态库搜索路径排序如下:
- LD_PRELOAD指定路径
- LD_LIBRARY_PATH指定路径
- ELF文件RUNPATH指定路径
- ld.so.cache缓存文件中查找
- /lib、/usr/lib等默认路径
只要编译的动态库处于以上几种方式指定的路径中,就能够被动态链接器搜索到,正确解析动态库符号。
搜索到动态库后,需要解析动态库,并将其LOAD段通过mmap方式加载至内存映射区。
步骤3:外部符号重定位
外部符号是指ELF可执行文件.dynsym节中未定义(UND)的符号,这些符号通常在动态库中定义。动态库文件中定义的是位置无关代码(-fPIC),这些代码只有相对地址,需要由动态链接器进行重定位绑定实际虚拟地址。
步骤4:跳转至ELF可执行程序入口点
动态链接器完成全部工作后,程序跳转至ELF可执行程序入口点,开始执行新程序。
最后,通过cat /proc/<pid>/maps命令查看进程虚拟地址空间内存布局,验证上述理论知识:

总结
动态库是Linux系统中非常重要的组件,通过深入理解其底层实现原理,开发者能够更高效地使用动态库,解决实际开发中遇到的编译原理相关问题。