前置知识: C

动态库与静态库

20 min中级

库的创建、链接机制、符号解析、加载策略与跨平台实践

前置知识

学习目标

  • 掌握「1. 历史动机与演化」的核心机制、典型用法与常见陷阱
  • 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
  • 掌握「3. 理论推导与证明」的核心机制、典型用法与常见陷阱
  • 掌握「4. 代码示例」的核心机制、典型用法与常见陷阱
  • 掌握「5. 对比分析」的核心机制、典型用法与常见陷阱

1. 历史动机与演化

1.1 库的概念起源

“库”(library)一词在计算机科学中最早可追溯至 1947 年 Maurice Wilkes 在 EDSAC 项目中提出的”子程序库”思想。早期程序员必须为每台机器重新编写数学函数,Wilkes 倡导把通用子程序预先写好、保存在磁带上,按需调用,这是软件复用的萌芽。

在 C 语言诞生(1972 年,Dennis Ritchie 于 Bell Labs)之前,Fortran 与汇编语言已经存在”子程序库”的概念,但缺乏统一的二进制接口标准。每个操作系统、每种编译器都自成体系,二进制级别的代码复用极为困难。

1.2 Unix 早期:ar 与静态库

1970 年代 Unix 第七版(V7)首次引入 ar(archiver)工具,用于把多个 .o 目标文件归档为一个 .a 文件。早期 ar 仅做简单归档,链接器(ld)必须扫描整个归档以查找符号,效率低下。BSD 4.3 引入了符号索引(__.SYMDEF),System V 改名为 /,GNU ar 默认在归档时通过 s 选项生成符号索引,ranlib 工具可单独为旧归档补建索引。

静态库的设计哲学是”按需拷贝”:链接器扫描归档,只把被引用的目标文件拷贝进最终可执行文件。这种模型简单可靠,但存在”静态库符号冲突”、“重复拷贝导致体积膨胀”等问题。

1.3 动态库的诞生

1980 年代中期,SunOS 4.0(1988)首次引入共享库(shared library,.so)机制,目标是:

  1. 减小可执行文件体积:多个进程共享一份 .so 文件,磁盘与内存占用均降低。
  2. 便于升级:替换 .so 文件即可升级,无需重新编译所有依赖程序。
  3. 支持插件:程序可在运行时按需加载库,实现可扩展架构。

Microsoft 在 Windows NT 3.1(1993)引入了 DLL(Dynamic-Link Library)机制,并设计了 LoadLibrary / GetProcAddress API,沿用至今。Apple 在 Mac OS X 10.0(2001)引入了 Mach-O 格式的 .dylib,并在 10.5 引入” umbrella framework “概念。

1.4 C 标准演化对库的影响

标准版本年份对库的关键影响
K&R C1978首次定义标准库函数(printf、malloc 等),但未规定 ABI。
C89/C901989/1990ISO/IEC 9899:1990 正式标准化 <stdio.h>、<stdlib.h> 等头文件,库函数集合稳定。
C991999引入 <stdbool.h>、<complex.h>、<inttypes.h>、<tgmath.h> 等新头文件;inline 语义复杂化,影响头文件库设计。
C112011引入 <stdatomic.h>、<threads.h>、<stdalign.h>、<stdnoreturn.h>,并新增可选边界检查库 Annex K(<stdio.h> 中的 *_s 函数)。
C17/C182018主要为缺陷修复,未引入新库;明确 stdatomic.h 不再可选。
C232024引入 <stdbit.h>、<stdckdint.h>、<stdfloat.h>;新增 #embed 指令,可在编译期把二进制资源嵌入程序,影响静态资源库设计。
C2y草案探讨反射、契约、模块化(import std;)等特性,未来可能改变头文件库的发布方式。

1.5 现代生态

  • Linux:以 ELF(Executable and Linkable Format)为基础,glibc 提供核心 C 运行时;musl libc 为轻量替代;systemd 引入 ld.so 缓存机制加速动态链接。
  • Windows:以 PE(Portable Executable)为基础,kernel32.dll、ucrtbase.dll(Universal CRT,VS 2015 起统一)提供 C 运行时;WinSxS(Side-by-Side Assembly)解决 DLL Hell。
  • macOS/iOS:以 Mach-O 为基础,libSystem.dylib 提供 C 运行时;引入 @rpath、@loader_path、@executable_path 等动态路径变量解决嵌入式 framework 定位问题。

2. 形式化定义

2.1 静态库的形式化定义

设 O={o1,o2,…,on}O = \{o_1, o_2, \ldots, o_n\} 为一组目标文件(object file)的集合,每个 oio_i 包含:

  • 符号定义集 D(oi)={s∣s 在 oi 中定义}D(o_i) = \{s \mid s \text{ 在 } o_i \text{ 中定义}\}
  • 符号引用集 U(oi)={s∣s 在 oi 中引用但未定义}U(o_i) = \{s \mid s \text{ 在 } o_i \text{ 中引用但未定义}\}
  • 节(section)集合 Σ(oi)={.text,.data,.bss,.rodata,…}\Sigma(o_i) = \{.text, .data, .bss, .rodata, \ldots\}

静态库 LstaticL_{static} 是一个归档文件:

Lstatic=⟨ArchiveHeader,{(oi,metai)}i=1n,SymIndex⟩L_{static} = \langle \text{ArchiveHeader}, \{(o_i, \text{meta}_i)\}_{i=1}^{n}, \text{SymIndex} \rangle

其中 SymIndex:Symbol→ObjectFile\text{SymIndex}: \text{Symbol} \to \text{ObjectFile} 是符号到目标文件的映射,用于加速链接器查找。链接器对静态库的解析过程可形式化为:

Resolve(Ufinal,Lstatic):iterate until fixpoint Ufinal←Ufinal∖D(o)∪U(o), for o=SymIndex(s),s∈Ufinal\text{Resolve}(U_{\text{final}}, L_{static}) : \text{iterate until fixpoint } U_{\text{final}} \leftarrow U_{\text{final}} \setminus D(o) \cup U(o), \text{ for } o = \text{SymIndex}(s), s \in U_{\text{final}}

该过程重复直至 UfinalU_{\text{final}} 不再变化(不动点),即所有可解析符号都被纳入。

2.2 动态库的形式化定义

动态库 LdynamicL_{dynamic} 是一个可被多个进程共享装载的目标文件,包含:

Ldynamic=⟨Header,ProgramHeaders,SymTabledyn,RelaTable,VerSym,VerDef,VerNeed⟩L_{dynamic} = \langle \text{Header}, \text{ProgramHeaders}, \text{SymTable}^{dyn}, \text{RelaTable}, \text{VerSym}, \text{VerDef}, \text{VerNeed} \rangle

其中:

  • SymTabledyn\text{SymTable}^{dyn}:动态符号表(.dynsym),仅包含导出与导入的符号,体积小于完整符号表 .symtab。
  • RelaTable\text{RelaTable}:重定位表(.rela.plt、.rela.dyn),描述运行期需要修复的地址。
  • VerSym,VerDef,VerNeed\text{VerSym}, \text{VerDef}, \text{VerNeed}:GNU 符号版本机制(.gnu.version、.gnu.version_d、.gnu.version_r),允许同一符号存在多个版本。

加载时刻由动态链接器(dynamic linker / interpreter,PT_INTERP 段指向的程序,Linux 上通常为 /lib64/ld-linux-x86-64.so.2)执行符号绑定:

Bind(p,Ldynamic):∀r∈RelaTable(Ldynamic),fix r.addr←SymTabledyn(loaded deps)[r.symbol]\text{Bind}(p, L_{dynamic}) : \forall r \in \text{RelaTable}(L_{dynamic}), \text{fix } r.addr \leftarrow \text{SymTable}^{dyn}(\text{loaded deps})[r.symbol]

2.3 链接时机的三分类

LinkTime={Link-time静态链接,t=tlinkLoad-time动态链接器在 execve 时绑定Run-time通过 dlopen/LoadLibrary  在运行时显式绑定\text{LinkTime} = \begin{cases} \text{Link-time} & \text{静态链接,} t = t_{\text{link}} \\ \text{Load-time} & \text{动态链接器在 } execve \text{ 时绑定} \\ \text{Run-time} & \text{通过 } dlopen / \text{LoadLibrary \text{ 在运行时显式绑定}} \end{cases}

3. 理论推导与证明

3.1 静态库解析的”按需拷贝”性质

命题:静态库链接满足”惰性拷贝”性质,即未引用的目标文件不会进入最终可执行文件。

证明:设可执行文件初始未定义符号集为 U0U_0(来自用户代码与启动例程 crt0)。链接器遍历静态库,若 ∃s∈U0,s∈D(o)\exists s \in U_0, s \in D(o),则将 oo 加入可执行文件,更新 U0←(U0∖D(o))∪U(o)U_0 \leftarrow (U_0 \setminus D(o)) \cup U(o);否则 oo 被跳过。由于目标文件 oo 加入与否仅依赖于 D(o)∩U0D(o) \cap U_0 是否非空,未引用的 oo 不会被加入。□\square

推论:静态库不会导致”过度膨胀”,但同一符号若在多个静态库中均有定义,则会出现”重复定义”错误。

3.2 动态库的”位置无关”性质

命题:PIC(Position Independent Code)代码在被映射到任意虚拟地址时,无需修改代码段即可正确执行。

证明思路:PIC 代码对所有全局符号的引用通过 GOT(Global Offset Table)间接完成,对所有函数调用通过 PLT(Procedure Linkage Table)间接完成。GOT 与 PLT 位于数据段,可在加载时由动态链接器填充实际地址。代码段只包含相对偏移指令(如 mov rax, [rip + offset]),不包含绝对地址,因此与加载地址无关。□\square

推论:PIC 允许同一份 .so 文件被多个进程映射到不同的虚拟地址,但物理内存只需一份(通过 mmap 共享),这是动态库节省内存的理论基础。

3.3 符号版本与兼容性

设 Ver(s)=v\text{Ver}(s) = v 表示符号 ss 的版本为 vv。GNU 符号版本机制允许同一 .so 同时导出多个版本的 printf@@GLIBC_2.2.5 与 printf@@GLIBC_2.17。运行时,动态链接器根据可执行文件 .gnu.version_r 中记录的需求版本选择对应实现。

兼容性定理:若库 LL 的版本 v2v_2 同时保留了 v1v_1 的所有符号版本,则依赖 v1v_1 的可执行文件在 v2v_2 上仍可运行(向后兼容)。

4. 代码示例

4.1 创建静态库

以下示例展示如何创建一个简单的数学静态库 libmymath.a,包含加法与乘法两个函数。

文件 mymath.h:

#ifndef MYMATH_H
#define MYMATH_H

/* 加法运算 */
int my_add(int a, int b);

/* 乘法运算 */
int my_mul(int a, int b);

#endif /* MYMATH_H */

文件 mymath.c:

#include "mymath.h"

int my_add(int a, int b)
{
    return a + b;
}

int my_mul(int a, int b)
{
    return a * b;
}

编译并打包为静态库:

# 步骤 1:编译为目标文件(-c 仅编译不链接,-fPIC 生成位置无关代码以便后续也可用于动态库)
gcc -c -Wall -O2 -fPIC mymath.c -o mymath.o

# 步骤 2:使用 ar 归档为静态库(r=插入,c=创建,s=生成符号索引)
ar rcs libmymath.a mymath.o

# 步骤 3:验证符号索引
nm -s libmymath.a

调用方 main.c:

#include <stdio.h>
#include "mymath.h"

int main(void)
{
    printf("3 + 4 = %d\n", my_add(3, 4));
    printf("3 * 4 = %d\n", my_mul(3, 4));
    return 0;
}

编译并链接静态库:

# 方式一:通过 -L 指定库搜索路径,-l 指定库名(去掉前缀 lib 与后缀 .a)
gcc -Wall main.c -L. -lmymath -o main_static

# 方式二:直接指定静态库文件名
gcc -Wall main.c libmymath.a -o main_static

# 验证:可执行文件中应包含 my_add 与 my_mul 的实现,不依赖外部库
./main_static
ldd main_static  # 输出中不应出现 libmymath

4.2 创建动态库

编译为动态库:

# 关键参数:
#   -fPIC        生成位置无关代码(必需)
#   -shared      生成共享库
#   -Wl,-soname,libmymath.so.1  设置 SONAME,用于版本管理
gcc -c -Wall -O2 -fPIC mymath.c -o mymath_pic.o
gcc -shared -Wl,-soname,libmymath.so.1 -o libmymath.so.1.0.0 mymath_pic.o

# 创建 SONAME 软链接
ln -sf libmymath.so.1.0.0 libmymath.so.1
# 创建开发链接(-l 查找时使用)
ln -sf libmymath.so.1 libmymath.so

链接动态库并运行:

# 编译时链接(链接器查找 libmymath.so)
gcc -Wall main.c -L. -lmymath -o main_dynamic

# 运行时需要让动态链接器找到 libmymath.so.1
LD_LIBRARY_PATH=. ./main_dynamic

# 查看依赖
ldd main_dynamic
# 输出应包含:libmymath.so.1 => ./libmymath.so.1

4.3 运行时加载(dlopen)

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

typedef int (*math_func_t)(int, int);

int main(void)
{
    /* RTLD_LAZY: 延迟绑定;RTLD_NOW: 立即绑定所有符号 */
    void *handle = dlopen("./libmymath.so.1", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        return EXIT_FAILURE;
    }

    /* 清空错误状态 */
    dlerror();

    /* 通过符号名查找函数地址 */
    math_func_t add = (math_func_t)dlsym(handle, "my_add");
    const char *err = dlerror();
    if (err) {
        fprintf(stderr, "dlsym my_add failed: %s\n", err);
        dlclose(handle);
        return EXIT_FAILURE;
    }

    math_func_t mul = (math_func_t)dlsym(handle, "my_mul");
    if ((err = dlerror()) != NULL) {
        fprintf(stderr, "dlsym my_mul failed: %s\n", err);
        dlclose(handle);
        return EXIT_FAILURE;
    }

    printf("dlopen: 3 + 4 = %d\n", add(3, 4));
    printf("dlopen: 3 * 4 = %d\n", mul(3, 4));

    dlclose(handle);
    return 0;
}

编译并运行:

# 注意:必须链接 libdl(旧 glibc)或在 glibc 2.34+ 中已合并入 libc
gcc -Wall main_dlopen.c -ldl -o main_dlopen
./main_dlopen

4.4 Windows 平台 DLL 创建

/* mymath_dll.h */
#ifdef MYMATH_EXPORTS
#define MYMATH_API __declspec(dllexport)
#else
#define MYMATH_API __declspec(dllimport)
#endif

MYMATH_API int my_add(int a, int b);
MYMATH_API int my_mul(int a, int b);
/* mymath_dll.c */
#define MYMATH_EXPORTS
#include "mymath_dll.h"

int my_add(int a, int b) { return a + b; }
int my_mul(int a, int b) { return a * b; }

使用 MSVC 编译:

cl /LD mymath_dll.c        # 生成 mymath_dll.dll + mymath_dll.lib (导入库)
cl main.c mymath_dll.lib   # 编译调用方

使用 MinGW 编译:

gcc -shared -o mymath_dll.dll mymath_dll.c -Wl,--out-implib,libmymath_dll.a
gcc main.c -L. -lmymath_dll -o main.exe

4.5 符号导出控制

在 Linux 上使用版本脚本精确控制符号导出:

# mymath.map 版本脚本
cat > mymath.map <<'EOF'
MYMATH_1.0 {
    global:
        my_add;
        my_mul;
    local:
        *;
};
EOF

gcc -shared -fPIC -Wl,--version-script=mymath.map -o libmymath.so mymath.c
nm -D libmymath.so  # 仅应看到 my_add 与 my_mul

在 Windows 上使用 .def 文件:

# mymath.def
LIBRARY MYMATH
EXPORTS
    my_add @1
    my_mul @2

4.6 静态库归档格式内部剖析

ar 归档文件由”全局头 + 多个文件成员”组成,理解其内部结构有助于排查”为什么我的符号没被链接进去”等问题。

BSD/SysV/GNU 三种格式:

!<arch>\n              <- 8 字节全局 magic
"mymath.o/0"           <- 成员名(以 / 结尾表示 SysV/GNU 格式)
"1234567890"           <- 修改时间戳(10 字节)
"0"                    <- 所有者 ID(6 字节)
"0"                    <- 组 ID(6 字节)
"100644"               <- 文件模式(8 字节)
"1024"                 <- 文件大小(10 字节)
"`\n"                  <- 结束标记(2 字节)
[文件内容 1024 字节]
[0 或 1 个填充字节,使下个成员起始地址为偶数]

GNU ar 的 s 选项在归档末尾追加名为 __.SYMDEF(BSD 风格)或 /(SysV 风格)的特殊成员,存储”符号名 → 偏移量”的哈希表,链接器据此快速定位符号所在的目标文件,无需扫描整个归档。

验证归档内容:

# 列出归档中的所有成员
ar t libmymath.a
# 输出:mymath.o

# 详细查看(含大小、时间戳)
ar tv libmymath.a

# 提取归档中的目标文件
ar x libmymath.a mymath.o

# 查看符号索引
nm -s libmymath.a | head -20
# 输出:
# Archive index:
# my_add in mymath.o
# my_mul in mymath.o

手动构建带符号索引的归档:

# 老式分两步(先 ar 再 ranlib)
ar rc libmymath.a mymath.o
ranlib libmymath.a

# 现代方式一步到位
ar rcs libmymath.a mymath.o

4.7 ELF 动态链接内部机制

动态库 .so 文件本质上是 ELF 格式的特殊目标文件。理解其内部结构对调试符号问题至关重要。

ELF 文件结构:

flowchart TD
    B0["ELF Header | <- 64 字节(64 位系统)"]
    B1["Program Header Table | <- 描述运行时加载的段"]
    B0 --> B1
    B2[".text | <- 代码段 / .rodata | <- 只读数据 / .data | <- 已初始化数据 / .bss | <- 未初始化数据(运行时清零) / .got | <- 全局偏移表 / .got.plt | <- PLT 全局偏移表 / .plt | <- 过程链接表 / .rela.plt | <- PLT 重定位项 / .rela.dyn | <- 数据重定位项 / .dynsym | <- 动态符号表 / .dynstr | <- 动态字符串表 / .gnu.version | <- 符号版本索引 / .gnu.version_d | <- 版本定义 / .gnu.version_r | <- 版本需求 / .dynamic | <- 动态段(链接器使用)"]
    B1 --> B2
    B3["Section Header Table | <- 描述链接时的节"]
    B2 --> B3

GOT/PLT 工作机制(x86-64 示例):

假设动态库中函数 my_add 被 main 调用,编译器生成的代码不会直接跳转到 my_add 的地址,而是通过 PLT 间接跳转:

# main 函数中调用 my_add:
call my_add@PLT

# my_add@PLT 桩代码(位于 .plt 段):
jmp [rip + offset_to_got_entry]   # 跳到 GOT 表项中存储的地址
push index_in_rela_plt            # 压入重定位索引
jmp PLT[0]                        # 跳到 PLT 第一项(动态链接器桩)

# PLT[0] 桩代码:
push [GOT+8]                      # 压入 link_map 指针
jmp [GOT+16]                      # 跳到 _dl_runtime_resolve

首次调用时,GOT 表项中存储的是 PLT 桩中 push index 指令的地址,因此跳回 PLT,触发 _dl_runtime_resolve 解析符号并更新 GOT。后续调用直接通过 GOT 跳转到真实函数地址(“延迟绑定”)。

可以通过环境变量 LD_BIND_NOW=1 或编译时 -Wl,-z,now 强制立即绑定所有符号,牺牲启动速度换取可预测性与安全性(防止 ROP 攻击修改 GOT)。

验证动态库内部结构:

# 查看程序头表(运行时视图)
readelf -l libmymath.so

# 查看节头表(链接时视图)
readelf -S libmymath.so

# 查看动态段
readelf -d libmymath.so
# 关键字段:
#   NEEDED    0x...   (依赖的其他 .so)
#   SONAME    0x...   (本库的 SONAME)
#   RPATH     0x...   (运行期搜索路径)
#   RUNPATH   0x...   (现代版 RPATH)
#   GNU_HASH  0x...   (GNU 哈希表,加速符号查找)
#   STRTAB    0x...   (动态字符串表)
#   SYMTAB    0x...   (动态符号表)
#   INIT      0x...   (库初始化函数)
#   FINI      0x...   (库终止函数)
#   INIT_ARRAY 0x...  (初始化函数数组)
#   FINI_ARRAY 0x...  (终止函数数组)

# 查看重定位项
readelf -r libmymath.so

# 反汇编 PLT
objdump -d -j .plt libmymath.so

4.8 库的初始化与终止函数

动态库可以注册在加载/卸载时执行的函数,用于全局状态初始化与资源清理。

方式一:__attribute__((constructor)) / __attribute__((destructor)):

#include <stdio.h>

__attribute__((constructor))
static void my_init(void)
{
    /* 在 dlopen 返回前 / 进程启动时执行 */
    fprintf(stderr, "[libmymath] constructor called\n");
}

__attribute__((destructor))
static void my_fini(void)
{
    /* 在 dlclose 时 / 进程退出时执行 */
    fprintf(stderr, "[libmymath] destructor called\n");
}

方式二:__attribute__((constructor(priority)))(GNU 扩展):

__attribute__((constructor(101)))  /* 优先级 0-100 保留给 CRT */
static void early_init(void) { /* ... */ }

__attribute__((constructor(65535)))  /* 数值越小越早执行 */
static void late_init(void) { /* ... */ }

方式三:C 标准的 __cxa_atexit(C++ 全局对象析构使用):

#include <stdlib.h>

static void cleanup(void *arg)
{
    /* 在进程退出或 dlclose 时执行 */
}

__attribute__((constructor))
static void init_with_cleanup(void)
{
    __cxa_atexit(cleanup, NULL, NULL);
}

Windows DLL 的 DllMain:

#include <windows.h>

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
        /* 进程首次加载本 DLL */
        break;
    case DLL_THREAD_ATTACH:
        /* 进程中新建线程 */
        break;
    case DLL_THREAD_DETACH:
        /* 线程退出 */
        break;
    case DLL_PROCESS_DETACH:
        /* 进程卸载本 DLL */
        if (!lpvReserved) {
            /* 显式 FreeLibrary 调用 */
        } else {
            /* 进程退出,进程状态已不一致,避免释放 CRT 资源 */
        }
        break;
    }
    return TRUE;
}

注意:DllMain 内禁止调用 LoadLibrary、CreateThread 等可能持有加载器锁的 API,否则会导致死锁。

4.9 跨语言调用示例:C 库供 Python 调用

/* greet.c - 提供 say_hello 函数 */
#include <stdio.h>

void say_hello(const char *name)
{
    printf("Hello, %s!\n", name);
}

int add(int a, int b)
{
    return a + b;
}
# 编译为共享库
gcc -fPIC -shared -o libgreet.so greet.c
# Python 调用方
import ctypes

lib = ctypes.CDLL("./libgreet.so")

# 配置参数类型(C 函数 say_hello 接收 const char*)
lib.say_hello.argtypes = [ctypes.c_char_p]
lib.say_hello.restype = None

# 调用
lib.say_hello(b"World")

# add 函数
lib.add.argtypes = [ctypes.c_int, ctypes.c_int]
lib.add.restype = ctypes.c_int
print(lib.add(3, 4))
python3 caller.py
# 输出:Hello, World!
#       7

5. 对比分析

5.1 静态库 vs 动态库

维度静态库 (.a / .lib)动态库 (.so / .dll / .dylib)
链接时机编译期加载期或运行期
可执行文件体积大(包含库代码)小(仅引用符号)
启动速度快(无需运行期解析)略慢(需 ld.so 解析符号)
内存共享否(每进程独立拷贝)是(多进程共享同一份物理页)
升级便利性差(需重新编译)好(替换 .so 即可)
ABI 兼容性要求低(编译期已绑定)高(运行期需匹配 ABI)
安全性高(依赖固化)易受 DLL 劫持
调试难度简单(符号完整)较复杂(涉及动态链接器)

5.2 与其他语言的对比

5.2.1 C++ 的库机制

C++ 在 C 的基础上增加了 name mangling(名称修饰),不同编译器(GCC、Clang、MSVC)的 mangling 方案不同,导致 C++ 动态库的 ABI 跨编译器兼容性极差。C++ 通常通过 extern "C" 导出 C 接口以提升兼容性。C++20 引入 modules(import),未来可能取代部分头文件库场景。

5.2.2 Rust 的 crate 与 cdylib

Rust 默认使用静态链接的 crate 模型(cargo build 默认静态链接所有依赖)。Rust 通过 crate-type = ["cdylib"] 生成 C ABI 兼容的动态库,可通过 dlopen 从 C 调用。Rust 的 ABI 不稳定(unstable ABI),跨版本二进制兼容需使用 cbindgen 生成 C 头文件并通过 C ABI 暴露。

5.2.3 Go 的 plugin

Go 1.8 引入 plugin 包,支持在 Linux/macOS 上动态加载 .so,但功能受限:必须使用相同 Go 版本编译,不支持 Windows,且无法卸载。Go 默认静态编译,可执行文件通常不依赖任何外部 .so,便于容器化部署。

5.2.4 Java 的 JAR

Java 通过 JAR 文件打包字节码,由 JVM 在加载时解析。JAR 本质上是”动态库”,但 JVM 字节码跨平台、跨架构,与 C 的本地动态库机制完全不同。JNI(Java Native Interface)允许 Java 调用 C/C++ 动态库。

5.3 选型决策树

flowchart TD
    T0["是否需要在不重新编译主程序的前提下升级库?"]
    T1["是 → 是否需要 ABI 稳定? → 是 → 动态库 + 严格版本管理"]
    T2["否 → 动态库 + dlopen 插件化"]
    T3["否 → 目标平台是否禁止动态库(如某些嵌入式环境)?"]
    T4["是 → 静态库"]
    T5["否 → 是否需要最小化可执行文件体积?"]
    T6["是(如多进程共享)→ 动态库"]
    T7["否(如 CLI 工具)→ 静态库"]
    T0 --> T1
    T2 --> T3
    T3 --> T4
    T3 --> T5
    T5 --> T6
    T5 --> T7

6. 常见陷阱与反模式

6.1 静态库顺序陷阱

反模式:在 gcc main.c -lmymath -lfoo 中,若 main.c 引用 libfoo,而 libfoo 又引用 libmymath,链接器从左到右扫描,先处理 libmymath 时未发现引用,跳过;后处理 libfoo 时发现引用 libmymath,但 libmymath 已被跳过,报”未定义符号”错误。

正确做法:依赖库按”被依赖者后置”顺序排列,必要时重复列出:

gcc main.c -lfoo -lmymath -lfoo  # 重复列出解决循环依赖

或使用 --start-group / --end-group 让链接器反复扫描:

gcc main.c -Wl,--start-group -lfoo -lmymath -Wl,--end-group

6.2 DLL Hell

反模式:在 Windows 系统目录放置多个版本的 libfoo.dll,不同程序依赖不同版本,导致升级一个程序破坏另一个程序。

正确做法:

  • 应用程序与依赖的 DLL 放在同一目录,不放入 System32。
  • 使用 WinSxS(Side-by-Side Assembly)+ manifest 文件声明依赖版本。
  • 使用 SetDllDirectory 或 LoadLibraryEx 的 LOAD_LIBRARY_SEARCH_APPLICATION_DIR 标志限制搜索路径,防止 DLL 劫持。

6.3 LD_LIBRARY_PATH 滥用

反模式:在 ~/.bashrc 中全局设置 LD_LIBRARY_PATH=/opt/myapp/lib,影响系统中所有程序的库搜索路径,可能引发冲突。

正确做法:

  • 在链接时使用 -Wl,-rpath,/opt/myapp/lib 把运行期搜索路径嵌入可执行文件。
  • 使用 patchelf --set-rpath /opt/myapp/lib main 事后修改。
  • 系统级库安装到 /usr/lib 或 /usr/local/lib,并运行 ldconfig 更新缓存。

6.4 符号版本冲突

反模式:可执行文件链接 libfoo.so.1(依赖 printf@@GLIBC_2.17),但运行环境的 libc.so.6 仅提供 printf@@GLIBC_2.2.5,运行时报 version GLIBC_2.17’ not found`。

正确做法:

  • 编译时使用与目标运行环境一致或更老的 glibc。
  • 使用 objdump -T main | grep GLIBC 检查实际依赖的 GLIBC 版本。
  • 容器化部署保证运行环境与编译环境一致。

6.5 C++ 跨编译器 ABI 不兼容

反模式:用 GCC 编译的 .so 中导出 std::string 类型的符号,用 Clang 或 MSVC 编译的调用方链接该 .so,运行时崩溃。

正确做法:

  • 动态库导出接口仅使用 C 兼容类型(POD、char*)。
  • 使用 extern "C" 包裹导出函数。
  • 跨语言接口避免传递 STL 容器,改为传递裸指针 + 长度。

6.6 静态库与动态库混合链接的符号遮蔽

反模式:可执行文件同时链接 libfoo.a(静态)与 libbar.so(动态),两者都定义 foo_init,链接器选择静态库版本,但 libbar.so 内部调用 foo_init 仍指向动态库版本,导致同一程序中存在两份 foo_init,状态不一致。

正确做法:使用 --exclude-libs 隐藏静态库符号,或使用版本脚本统一管理符号可见性。

7. 工程实践与最佳实践

7.1 ABI 稳定性设计

  1. PIMPL 模式(Pointer to IMPLementation):在头文件中仅暴露不透明指针,实现细节放在 .cpp 中,修改实现不破坏 ABI。
  2. 虚函数表稳定:发布的类一旦有虚函数,不能新增虚函数(会改变 vtable 布局);新增功能通过新增非虚函数或子类完成。
  3. 避免内联:导出的函数不要在头文件中内联,否则客户端可能编译期绑定旧实现。
  4. 数据成员对齐:发布的结构体不要随意增删字段,如需扩展使用”预留字段”或”扩展结构体”模式。

7.2 版本号管理(SemVer + SONAME)

语义化版本变化ABI 影响SONAME 变化
MAJOR(不兼容)破坏 ABIlibfoo.so.2
MINOR(向后兼容)新增符号,不破坏旧 ABIlibfoo.so.1 不变,但 .so.1.1.0 文件名变化
PATCH(修复)无 ABI 变化仅 .so.1.0.1 文件名变化

命令约定:

libfoo.so          -> libfoo.so.1    (开发链接,-lfoo 使用)
libfoo.so.1        -> libfoo.so.1.0.0 (SONAME 软链接)
libfoo.so.1.0.0    (实际文件)

7.3 符号可见性控制

  • 默认隐藏:编译时加 -fvisibility=hidden,所有符号默认不导出。
  • 显式导出:使用 __attribute__((visibility("default"))) 标记需要导出的符号。
  • 宏统一管理:
#if defined(_WIN32)
    #if defined(MYLIB_EXPORTS)
        #define MYLIB_API __declspec(dllexport)
    #else
        #define MYLIB_API __declspec(dllimport)
    #endif
#else
    #if defined(MYLIB_EXPORTS)
        #define MYLIB_API __attribute__((visibility("default")))
    #else
        #define MYLIB_API
    #endif
#endif

MYLIB_API int my_add(int, int);

7.4 依赖管理

  • 静态库传递依赖:静态库 libfoo.a 依赖 libbar.a,链接主程序时必须显式列出两者,且 libbar.a 在 libfoo.a 之后。
  • 动态库传递依赖:动态库 libfoo.so 在自身链接时记录对 libbar.so 的依赖,主程序链接 libfoo.so 时自动拉入 libbar.so。可通过 -Wl,--as-needed 抑制无用依赖。
  • 静态 + 动态混合:用 -Wl,-Bstatic -lfoo -Wl,-Bdynamic -lbar 精确控制每个库的链接方式。

7.5 跨平台构建示例(CMake)

cmake_minimum_required(VERSION 3.16)
project(mymath C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# 通用源文件
set(MYMATH_SOURCES mymath.c)

# 静态库目标
add_library(mymath_static STATIC ${MYMATH_SOURCES})
set_target_properties(mymath_static PROPERTIES OUTPUT_NAME mymath)

# 动态库目标
add_library(mymath_shared SHARED ${MYMATH_SOURCES})
set_target_properties(mymath_shared PROPERTIES
    OUTPUT_NAME mymath
    SOVERSION 1
    VERSION 1.0.0
)

# Windows 上设置导出宏
if(WIN32)
    target_compile_definitions(mymath_shared PRIVATE MYMATH_EXPORTS)
endif()

# 安装规则
include(GNUInstallDirs)
install(TARGETS mymath_static mymath_shared
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(FILES mymath.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

7.6 调试技巧

工具平台用途
nm -D libfoo.soLinux列出动态符号表
objdump -T libfoo.soLinux列出动态符号(更详细)
readelf -d libfoo.soLinux查看动态段(NEEDED、SONAME、RPATH)
ldd mainLinux查看可执行文件的动态依赖
ldconfig -pLinux列出系统缓存的所有 .so
patchelfLinux修改 ELF 的 RPATH、SONAME、解释器
dumpbin /exports foo.dllWindows列出 DLL 导出符号
dumpbin /dependents main.exeWindows列出 EXE 依赖的 DLL
Dependency Walker / DependenciesWindowsGUI 工具,递归显示依赖树
otool -L libfoo.dylibmacOS列出动态依赖
install_name_toolmacOS修改 dylib 的 install name 与 rpath

8. 案例研究

8.1 Linux 内核模块(.ko)

Linux 内核模块(Kernel Module,.ko)本质上是一种特殊的”动态库”,在运行时通过 insmod / modprobe 加载到内核地址空间。与用户态动态库的对比:

  • 链接机制:内核模块在加载时由内核自身的链接器(kernel/module.c)解析符号,依赖内核导出的 ksymtab。
  • 符号导出:内核通过 EXPORT_SYMBOL、EXPORT_SYMBOL_GPL 显式导出符号给模块使用。
  • 版本管理:模块的 vermagic 字符串必须与内核版本严格匹配(除非启用 CONFIG_MODVERSIONS,使用 CRC 校验符号 ABI)。
  • 安全性:模块运行在内核态,错误会导致整个系统崩溃;签名验证(CONFIG_MODULE_SIG_FORCE)防止加载未签名模块。

8.2 Android NDK 的 libc.so

Android 自 5.0(Lollipop)起统一使用 Bionic libc 作为 C 运行时。Bionic 的设计取舍:

  • 轻量:剥离了大量 POSIX 兼容特性,体积仅为 glibc 的 1/3,适合嵌入式。
  • 不稳定 ABI:Bionic 的内部 ABI 在不同 Android 版本间可能变化,NDK 通过 libc.so 的符号版本与 android/support 头文件层屏蔽差异。
  • 应用打包:Android 应用 APK 中不允许携带 libc.so,所有应用共享系统的 Bionic,避免内存浪费。

8.3 LLVM 的插件化架构

LLVM 通过 llvm::sys::DynamicLibrary::LoadLibraryPermanently 在运行时加载 .so / .dll 插件,插件通过 RegisterPass 等模板类在静态初始化时向 LLVM 注册扩展。这种架构允许 opt、clang 等工具在不重新编译的前提下加载第三方优化 pass。

8.4 Windows DLL 的延迟加载

MSVC 支持”延迟加载 DLL”(/DELAYLOAD:foo.dll),程序启动时不加载 foo.dll,首次调用其函数时由 CRT 桩代码触发 LoadLibrary。优势:

  • 启动速度提升。
  • 可选功能模块按需加载,节省内存。
  • 跨平台部署时,缺失的 DLL 仅在调用时报错而非启动时崩溃。

8.5 嵌入式场景:静态链接为唯一选择

在某些 RTOS(如 FreeRTOS、Zephyr)与裸机环境中,没有动态链接器,所有代码必须静态链接为单一镜像。此时:

  • 库以源码或 .a 形式提供。
  • 编译期裁剪未使用函数(--gc-sections + -ffunction-sections)。
  • 链接脚本(linker script)精确控制段布局。
  • 静态库的”按需拷贝”特性可减小镜像体积。

8.6 Python C 扩展模块

Python 通过 CPython API 提供 C 扩展机制,扩展模块本身是一个动态库(Linux .so、Windows .pyd、macOS .dylib),由 Python 解释器在 import 时通过 dlopen / LoadLibrary 加载。扩展模块必须使用与 CPython 解释器相同的编译器、相同架构、相同 CRT(Windows 上为 ucrtbase.dll),否则加载失败。

8.7 完整案例:基于 dlopen 的日志后端切换

以下案例展示一个日志库,允许用户在运行时切换日志后端(文件、控制台、网络),通过 dlopen 加载不同的后端实现。

日志库头文件 logger.h:

#ifndef LOGGER_H
#define LOGGER_H

#include <stddef.h>

typedef enum {
    LOG_LEVEL_DEBUG = 0,
    LOG_LEVEL_INFO,
    LOG_LEVEL_WARN,
    LOG_LEVEL_ERROR,
} log_level_t;

typedef struct logger_backend logger_backend_t;

/* 后端接口(每个后端 .so 必须实现这些函数) */
typedef struct {
    const char *name;
    int  (*init)(const char *config);
    void (*log)(log_level_t level, const char *msg);
    void (*flush)(void);
    void (*fini)(void);
} logger_backend_vtable_t;

/* 主 API */
int  logger_load_backend(const char *so_path, const char *config);
void logger_log(log_level_t level, const char *fmt, ...);
void logger_close(void);

#endif /* LOGGER_H */

主程序 logger.c:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <dlfcn.h>
#include "logger.h"

static logger_backend_vtable_t *g_vtable = NULL;
static void *g_handle = NULL;

int logger_load_backend(const char *so_path, const char *config)
{
    if (g_handle) {
        logger_close();
    }

    g_handle = dlopen(so_path, RTLD_NOW | RTLD_LOCAL);
    if (!g_handle) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        return -1;
    }

    /* 后端 .so 必须导出 logger_backend_get_vtable 函数 */
    typedef const logger_backend_vtable_t *(*get_vtable_fn)(void);
    dlerror();
    get_vtable_fn get_vtable = (get_vtable_fn)dlsym(g_handle, "logger_backend_get_vtable");
    const char *err = dlerror();
    if (err) {
        fprintf(stderr, "dlsym failed: %s\n", err);
        dlclose(g_handle);
        g_handle = NULL;
        return -1;
    }

    g_vtable = (logger_backend_vtable_t *)get_vtable();
    if (g_vtable->init) {
        if (g_vtable->init(config ? config : "") != 0) {
            fprintf(stderr, "backend init failed\n");
            dlclose(g_handle);
            g_handle = NULL;
            g_vtable = NULL;
            return -1;
        }
    }
    return 0;
}

void logger_log(log_level_t level, const char *fmt, ...)
{
    if (!g_vtable || !g_vtable->log) return;

    char buf[1024];
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);

    g_vtable->log(level, buf);
}

void logger_close(void)
{
    if (!g_handle) return;
    if (g_vtable) {
        if (g_vtable->flush) g_vtable->flush();
        if (g_vtable->fini) g_vtable->fini();
    }
    dlclose(g_handle);
    g_handle = NULL;
    g_vtable = NULL;
}

控制台后端 console_backend.c:

#include <stdio.h>
#include <time.h>
#include "logger.h"

static const char *level_str(log_level_t level)
{
    switch (level) {
    case LOG_LEVEL_DEBUG: return "DEBUG";
    case LOG_LEVEL_INFO:  return "INFO ";
    case LOG_LEVEL_WARN:  return "WARN ";
    case LOG_LEVEL_ERROR: return "ERROR";
    default: return "?????";
    }
}

static int console_init(const char *config)
{
    (void)config;
    return 0;
}

static void console_log(log_level_t level, const char *msg)
{
    time_t now = time(NULL);
    struct tm tm_buf;
    localtime_r(&now, &tm_buf);
    char time_str[32];
    strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &tm_buf);
    fprintf(stderr, "[%s] [%s] %s\n", time_str, level_str(level), msg);
}

static void console_flush(void)
{
    fflush(stderr);
}

static const logger_backend_vtable_t vtable = {
    .name  = "console",
    .init  = console_init,
    .log   = console_log,
    .flush = console_flush,
    .fini  = NULL,
};

const logger_backend_vtable_t *logger_backend_get_vtable(void)
{
    return &vtable;
}

文件后端 file_backend.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "logger.h"

static FILE *g_fp = NULL;

static int file_init(const char *config)
{
    /* config 为日志文件路径,默认 logger.log */
    const char *path = (config && *config) ? config : "logger.log";
    g_fp = fopen(path, "a");
    return g_fp ? 0 : -1;
}

static void file_log(log_level_t level, const char *msg)
{
    if (!g_fp) return;
    time_t now = time(NULL);
    struct tm tm_buf;
    localtime_r(&now, &tm_buf);
    char time_str[32];
    strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &tm_buf);
    fprintf(g_fp, "[%s] [%d] %s\n", time_str, (int)level, msg);
}

static void file_flush(void)
{
    if (g_fp) fflush(g_fp);
}

static void file_fini(void)
{
    if (g_fp) {
        fclose(g_fp);
        g_fp = NULL;
    }
}

static const logger_backend_vtable_t vtable = {
    .name  = "file",
    .init  = file_init,
    .log   = file_log,
    .flush = file_flush,
    .fini  = file_fini,
};

const logger_backend_vtable_t *logger_backend_get_vtable(void)
{
    return &vtable;
}

编译与运行:

# 编译主程序
gcc -Wall -fPIC -c logger.c -o logger.o
gcc -Wall logger.o -ldl -o logger_main main_user.c

# 编译后端 .so(每个后端一个 .so)
gcc -Wall -fPIC -shared console_backend.c -o libconsole_backend.so
gcc -Wall -fPIC -shared file_backend.c    -o libfile_backend.so

# 运行(用户在 main_user.c 中根据命令行参数选择后端)
./logger_main console
./logger_main file=/var/log/myapp.log

此案例展示了完整的库工程实践:清晰的接口定义、ABI 稳定的结构体(vtable)、运行时插件加载、配置参数传递、资源生命周期管理。

8.8 Linux ld.so 加载流程详解

当 Linux 用户执行 ./main 时,内核实际执行以下流程:

  1. execve 系统调用:内核读取 main 的 ELF 头,找到 PT_INTERP 段指向的动态链接器(如 /lib64/ld-linux-x86-64.so.2)。
  2. 加载动态链接器:内核把 ld.so 映射到进程地址空间,把控制权交给 ld.so 的入口点(_start)。
  3. ld.so 自举:初始化自身 GOT,定位自身的 .dynamic 段。
  4. 读取 main 的 .dynamic 段:找到 DT_NEEDED 项,递归加载所有依赖的 .so。
  5. 符号解析:根据 DT_SYMTAB、DT_STRTAB、DT_GNU_HASH 解析每个未定义符号,更新 GOT/PLT。
  6. 执行初始化代码:按依赖顺序调用每个 .so 的 DT_INIT_ARRAY,最后调用 main 的 DT_INIT_ARRAY。
  7. 调用 _start → __libc_start_main → main:CRT 启动例程设置参数后调用用户 main。
  8. 进程退出:exit 触发 DT_FINI_ARRAY 反向调用,最终调用 _exit。

关键环境变量:

变量作用
LD_LIBRARY_PATH临时追加库搜索路径(优先于系统默认)
LD_PRELOAD在所有 NEEDED 之前强制加载指定 .so,常用于 hook(如 libSegFault、libefence)
LD_BIND_NOW立即绑定所有符号,等价于 -z now
LD_DEBUG输出 ld.so 调试信息(libs、symbols、bindings、versions 等)
LD_TRACE_LOADED_OBJECTS等价于 ldd,列出依赖但不实际运行程序

示例:使用 LD_DEBUG 排查符号问题:

LD_DEBUG=symbols,bindings ./main 2>&1 | grep my_add
# 输出包含:symbol=my_add; lookup in file=./main
#          symbol=my_add; lookup in file=./libmymath.so.1
#          binding file ./main to ./libmymath.so.1: normal symbol my_add

附录 A:常用命令速查表

A.1 Linux 工具

# === 查看静态库 ===
ar t libfoo.a              # 列出归档成员
nm -s libfoo.a             # 查看符号索引
objdump -d libfoo.a        # 反汇编所有成员

# === 查看动态库 ===
nm -D libfoo.so            # 动态符号表
objdump -T libfoo.so       # 动态符号(详细)
objdump -R libfoo.so       # 重定位项
readelf -d libfoo.so       # 动态段
readelf -r libfoo.so       # 重定位表
readelf -h libfoo.so       # ELF 头
readelf -l libfoo.so       # 程序头表

# === 查看可执行文件 ===
ldd main                   # 列出动态依赖
readelf -d main | grep NEEDED  # 查看依赖项

# === 修改 ELF ===
patchelf --set-rpath /opt/lib main        # 修改 RPATH
patchelf --set-soname libfoo.so.1 libfoo.so  # 修改 SONAME
patchelf --set-interpreter /lib/ld-linux.so.2 main  # 修改解释器

# === 系统级 ===
ldconfig -v                # 重建 .so 缓存
ldconfig -p | grep foo     # 查询缓存中是否有 libfoo
/etc/ld.so.conf            # 默认搜索路径配置
/etc/ld.so.conf.d/*.conf   # 子配置目录

# === 调试 ===
LD_DEBUG=libs ./main       # 输出库加载过程
LD_DEBUG=symbols ./main    # 输出符号查找过程
LD_DEBUG=bindings ./main   # 输出符号绑定过程
LD_BIND_NOW=1 ./main       # 强制立即绑定(等价 -z now)
LD_PRELOAD=./libhook.so ./main  # 预加载 hook 库

A.2 Windows 工具

# MSVC 工具
dumpbin /exports foo.dll              # 列出导出符号
dumpbin /imports main.exe             # 列出导入符号
dumpbin /dependents main.exe          # 列出依赖的 DLL
dumpbin /headers main.exe             # 查看 PE 头

# PowerShell 检查 DLL
[System.Reflection.Assembly]::LoadFile("C:\path\to\foo.dll")

# 第三方 GUI 工具
Dependencies(开源,Dependency Walker 替代)
Process Monitor(监控 DLL 加载过程)

A.3 macOS 工具

otool -L libfoo.dylib      # 列出依赖
otool -D libfoo.dylib      # 查看 install name
otool -tv libfoo.dylib     # 反汇编
nm -m libfoo.dylib         # 符号表(含 Mach-O 标志)

install_name_tool -id @rpath/libfoo.dylib libfoo.dylib      # 修改 install name
install_name_tool -add_rpath @loader_path/../lib main       # 添加 rpath
install_name_tool -change old_path new_path main            # 修改依赖路径

# codesign 签名(macOS 11+ 强制)
codesign -s "Developer ID: Your Name" libfoo.dylib

附录 B:典型问题排查清单

症状可能原因排查命令
error while loading shared libraries: libfoo.so: cannot open shared object file库不在搜索路径ldd main、`ldconfig -p
undefined symbol: foo链接顺序错误、符号未导出`nm -D libfoo.so
version GLIBC_2.17’ not found`运行环境 glibc 过旧`objdump -T main
symbol lookup error: foo: undefined symbol: bar运行期符号缺失LD_DEBUG=symbols ./main
cannot open shared object file: Operation not permittedSELinux/AppArmor 拒绝audit2allow、strace -e openat
dll not found (Windows)DLL 不在搜索路径dumpbin /dependents main.exe
entry point not found (Windows)DLL 版本不匹配dumpbin /exports foo.dll
dyld: Library not loaded (macOS)install name 错误otool -L main、install_name_tool

静态库创建

基本写法:编译目标文件 gcc -c <源文件> -o <目标.o>

// 编译生成目标文件
gcc -c libfoo.c -o libfoo.o

基本写法:创建静态库 ar rcs <库文件.a> <目标文件>...

// 打包目标文件为静态库
ar rcs libfoo.a libfoo.o

基本写法:查看静态库内容 ar -t <库文件.a>

// 列出库中的目标文件
ar -t libfoo.a

基本写法:链接静态库 gcc <主文件> -L<路径> -l<库名> -o <输出>

// 链接当前目录的 libfoo.a
gcc main.c -L. -lfoo -o main

动态库创建

基本写法:编译位置无关代码 gcc -fPIC -c <源文件> -o <目标.o>

// 生成位置无关目标文件
gcc -fPIC -c libfoo.c -o libfoo.o

基本写法:创建动态库 gcc -shared -o <库文件.so> <目标文件>...

// 链接为动态共享库
gcc -shared -o libfoo.so libfoo.o

基本写法:指定版本 gcc -shared -Wl,-soname,<库名.so.1> -o <库.so.1.0> <目标>

// 设置共享库版本
gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0 libfoo.o

基本写法:链接动态库 gcc <主文件> -L<路径> -l<库名> -o <输出>

// 链接动态库 libfoo.so
gcc main.c -L. -lfoo -o main

基本写法:运行时指定库路径 LD_LIBRARY_PATH=<路径> ./<程序>

// 运行时设置库搜索路径
LD_LIBRARY_PATH=. ./main

基本写法:链接时指定运行时路径 gcc -Wl,-rpath,<路径> <其他参数>

// 内嵌运行时搜索路径
gcc main.c -L. -lfoo -Wl,-rpath,. -o main

运行时加载 dlopen

基本写法:打开动态库 dlopen(<库路径>, RTLD_LAZY);

// 运行时加载共享库
void* handle = dlopen("./libfoo.so", RTLD_LAZY);

基本写法:获取符号 dlsym(<handle>, <符号名>);

// 取得函数指针
typedef int (*func_t)(int);
func_t f = (func_t)dlsym(handle, "add");

基本写法:调用动态函数 <函数指针>(<参数>);

// 调用从动态库取得的函数
int r = f(42);

基本写法:关闭动态库 dlclose(<handle>);

// 卸载动态库
dlclose(handle);

基本写法:获取错误信息 dlerror();

// 查询最近一次错误
const char* err = dlerror();

基本写法:编译需链接 dl gcc <文件> -ldl -o <输出>

// 链接 dl 库使用 dlopen
gcc main.c -ldl -o main

库的导出符号

基本写法:默认导出 int <函数>(...) { }

// 默认所有全局符号导出
int add(int a, int b) { return a + b; }

基本写法:可见性控制 __attribute__((visibility("default"))) int <函数>();

// 显式声明导出
__attribute__((visibility("default"))) int add(int a, int b);

基本写法:隐藏符号 __attribute__((visibility("hidden"))) int <函数>();

// 隐藏不导出
__attribute__((visibility("hidden"))) static int helper();

基本写法:默认隐藏编译 gcc -fvisibility=hidden -shared ...

// 默认隐藏所有符号
gcc -fvisibility=hidden -shared -o libfoo.so libfoo.o

库查询工具

基本写法:查看依赖 ldd <程序>

// 查看程序依赖的动态库
ldd ./main

基本写法:列出符号 nm <库文件>

// 查看库中的符号表
nm libfoo.a

基本写法:查看动态符号 nm -D <库.so>

// 查看动态库导出符号
nm -D libfoo.so

基本写法:查看符号所属库 readelf -d <库.so>

// 查看动态库信息
readelf -d libfoo.so

头文件与库组织

基本写法:声明导出函数 <头文件.h> 中声明

// 头文件中声明供外部使用
#ifndef FOO_H
#define FOO_H
int add(int a, int b);
#endif

基本写法:使用库 #include <<头文件>> 编译链接

// 主程序使用库
#include "foo.h"
int r = add(1, 2);