4. 静态库与动态库的构建与使用

在跨平台 C/C++ 工程中,库的形态直接决定了编译期与运行期的行为差异。静态库在链接阶段被完整复制进可执行文件,动态库则在运行时由加载器映射进进程地址空间。理解 ar、ranlib、libtool 这些工具,以及 .a、.lib、.so、.dylib、.dll 这些后缀背后的真实含义,是排查“链接找不到符号”“运行时找不到库”两类问题的前提。

一、静态库:归档文件与符号索引

静态库本质上是若干目标文件(.o / .obj)的归档包。在类 Unix 系统上,它由 ar 创建,扩展名为 .a;在 Windows 上,静态库通常由 lib.exe 生成,扩展名为 .lib。两者格式不同,不能互换。

以 Linux 为例,先编译出位置无关性无关的目标文件,再归档:

gcc -c math_util.c -o math_util.o
gcc -c string_util.c -o string_util.o
ar rcs libutil.a math_util.o string_util.o

其中 r 表示插入或替换成员,c 表示创建归档,s 表示写入符号索引。符号索引是静态库能被正确链接的关键:链接器 ld 在解析未定义符号时,需要快速知道哪个成员目标文件提供了该符号。如果归档时漏掉 s,就需要单独执行 ranlib libutil.a 来补建索引。可以用 ar t libutil.a 查看成员列表,用 nm -s libutil.a 查看符号索引内容。

链接静态库时,顺序很重要。链接器从左到右扫描,只从静态库中提取当前尚未解析的符号所对应的成员:

gcc main.o -L. -lutil -o app

如果 libutil.a 依赖另一个静态库 libbase.a,则 -lutil 必须写在 -lbase 之前,否则 libbase.a 中的符号在扫描时还未被引用,会被整体跳过,最终报 undefined reference。

二、动态库:共享对象与运行时加载

动态库在 Linux 上是 .so(Shared Object),macOS 上是 .dylib,Windows 上是 .dll。它们都要求编译目标文件时使用位置无关代码(PIC),Linux/macOS 下加 -fPIC:

gcc -fPIC -c math_util.c -o math_util.o
gcc -shared -o libutil.so math_util.o

macOS 上使用 -dynamiclib 生成 .dylib,并需要指定 -install_name 来记录运行时查找路径。Windows 上生成 DLL 时,通常还需要一个导入库(.lib),它只包含符号跳转桩,供链接期使用,与静态库的 .lib 同名但内容完全不同。

动态库的命名遵循 lib<name>.so.<major>.<minor>.<patch> 的惯例,例如 libutil.so.1.2.0。通常创建两个符号链接:libutil.so 指向最新版本供链接期使用,libutil.so.1 指向主版本供运行时使用。主版本号变化意味着 ABI 不兼容,必须重新编译依赖方。

三、libtool:跨平台封装

GNU libtool 是一层封装脚本,它把“生成静态库还是动态库”“用什么后缀”“如何设置运行时路径”这些平台差异隐藏起来。使用 libtool 时,编译命令统一写成:

libtool --mode=compile gcc -c math_util.c
libtool --mode=link gcc -o libutil.la math_util.lo -rpath /usr/local/lib

libtool 会同时生成 .a 和 .so(或 .dylib、.dll),并生成 .la 描述文件记录依赖与安装路径。在 Autotools 工程中,LT_INIT 宏会初始化 libtool,lib_LTLIBRARIES 声明要构建的库。它的价值在于同一份 Makefile.am 能在 Linux、macOS、Windows(MinGW/MSYS2)上产出对应格式的库。

四、链接与运行时加载的关键差异

静态链接在构建期完成符号解析,可执行文件自包含,部署简单,但体积大、更新库需重新链接。动态链接把符号解析推迟到加载期,多个进程可共享同一份物理内存中的库代码,更新库无需重新编译主程序,但引入了运行时查找路径问题。

Linux 下运行时查找顺序为:DT_RPATH(已废弃)、LD_LIBRARY_PATH、DT_RUNPATH、/etc/ld.so.cache、默认路径 /lib 与 /usr/lib。链接时用 -Wl,-rpath,/opt/app/lib 可写入 DT_RUNPATH。macOS 使用 DYLD_LIBRARY_PATH 与 @rpath、@loader_path、@executable_path 等占位符。Windows 则按可执行文件所在目录、当前目录、PATH 顺序查找 DLL。

排查“运行时找不到库”时,Linux 用 ldd ./app 查看依赖解析结果,用 readelf -d ./app | grep -E 'RPATH|RUNPATH|NEEDED' 查看动态段;macOS 用 otool -L ./app;Windows 用 dumpbin /dependents app.exe。若显示 not found,说明该库不在查找路径中,需要设置 LD_LIBRARY_PATH、写入 rpath 或安装到系统路径并执行 ldconfig。

判断一个 .lib 是静态库还是导入库:用 dumpbin /headers xxx.lib 查看,若包含 .dll 导入描述则为导入库;也可用 lib /list xxx.lib 查看成员,静态库会列出多个 .obj,导入库通常只列出少量桩对象。
静态库与动态库:构建、链接与运行时加载路径 源文件 math_util.c / string_util.c 编译目标文件 gcc -c / -fPIC -c 静态库归档 ar rcs libutil.a *.o ranlib 补建符号索引 动态库生成 gcc -shared -o libutil.so macOS: -dynamiclib → .dylib 链接期 gcc main.o -L. -lutil 静态库按顺序提取成员 运行时加载 ld.so / dyld / LoadLibrary 查找 .so / .dylib / .dll 可执行文件 app 静态:自包含 / 动态:依赖外部 运行时库查找顺序(Linux) 1. DT_RPATH(已废弃) 2. LD_LIBRARY_PATH 环境变量 3. DT_RUNPATH(-Wl,-rpath) 4. /etc/ld.so.cache → /lib → /usr/lib 依赖排查工具 Linux: ldd ./app | readelf -d ./app macOS: otool -L ./app | install_name_tool Windows: dumpbin /dependents app.exe .a / .lib(静态) .so / .dylib / .dll(动态)

五、跨平台命名与格式对照

平台静态库动态库导入库创建工具
Linuxlibxxx.alibxxx.so—ar / gcc -shared
macOSlibxxx.alibxxx.dylib—ar / clang -dynamiclib
Windows (MSVC)xxx.libxxx.dllxxx.liblib.exe / link.exe
Windows (MinGW)libxxx.axxx.dlllibxxx.dll.aar / gcc -shared

注意 Windows 下 MSVC 的静态库与导入库都叫 .lib,仅凭后缀无法区分,必须用工具检查内容。MinGW 则用 libxxx.dll.a 作为导入库,与静态库 libxxx.a 区分开。

六、实操检查清单

  1. 确认目标文件是否带 -fPIC:动态库必须,静态库可选。用 readelf -r math_util.o 查看重定位类型,出现 R_X86_64_32 说明未使用 PIC。
  2. 归档后检查符号索引:nm -s libutil.a 应显示 Archive index 段,否则执行 ranlib libutil.a。
  3. 链接顺序:被依赖的库放在依赖它的库之后,即 -lutil -lbase。
  4. 动态库版本链接:ln -sf libutil.so.1.2.0 libutil.so.1,ln -sf libutil.so.1 libutil.so。
  5. 运行时验证:ldd ./app 确认所有依赖均解析成功,无 not found。
  6. 跨平台构建:优先使用 CMake 的 add_library(util STATIC/SHARED ...) 或 libtool,避免手写平台分支命令。
常见误区:把 LD_LIBRARY_PATH 当作长期方案。它会影响所有子进程,且优先级高于系统路径,容易引入版本冲突。生产环境应使用 rpath 或安装到系统路径后执行 ldconfig。

掌握这些工具与格式的对应关系后,面对“undefined reference”和“cannot open shared object file”两类报错,就能快速定位是链接期符号未解析,还是运行期路径未命中,从而选择正确的修复手段。