动态库与静态库
库的创建、链接机制、符号解析、加载策略与跨平台实践
前置知识
- 函数详解:建议先完成前一篇的学习
学习目标
- 掌握「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)机制,目标是:
- 减小可执行文件体积:多个进程共享一份
.so文件,磁盘与内存占用均降低。 - 便于升级:替换
.so文件即可升级,无需重新编译所有依赖程序。 - 支持插件:程序可在运行时按需加载库,实现可扩展架构。
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 C | 1978 | 首次定义标准库函数(printf、malloc 等),但未规定 ABI。 |
| C89/C90 | 1989/1990 | ISO/IEC 9899:1990 正式标准化 <stdio.h>、<stdlib.h> 等头文件,库函数集合稳定。 |
| C99 | 1999 | 引入 <stdbool.h>、<complex.h>、<inttypes.h>、<tgmath.h> 等新头文件;inline 语义复杂化,影响头文件库设计。 |
| C11 | 2011 | 引入 <stdatomic.h>、<threads.h>、<stdalign.h>、<stdnoreturn.h>,并新增可选边界检查库 Annex K(<stdio.h> 中的 *_s 函数)。 |
| C17/C18 | 2018 | 主要为缺陷修复,未引入新库;明确 stdatomic.h 不再可选。 |
| C23 | 2024 | 引入 <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 静态库的形式化定义
设 为一组目标文件(object file)的集合,每个 包含:
- 符号定义集
- 符号引用集
- 节(section)集合
静态库 是一个归档文件:
其中 是符号到目标文件的映射,用于加速链接器查找。链接器对静态库的解析过程可形式化为:
该过程重复直至 不再变化(不动点),即所有可解析符号都被纳入。
2.2 动态库的形式化定义
动态库 是一个可被多个进程共享装载的目标文件,包含:
其中:
- :动态符号表(
.dynsym),仅包含导出与导入的符号,体积小于完整符号表.symtab。 - :重定位表(
.rela.plt、.rela.dyn),描述运行期需要修复的地址。 - :GNU 符号版本机制(
.gnu.version、.gnu.version_d、.gnu.version_r),允许同一符号存在多个版本。
加载时刻由动态链接器(dynamic linker / interpreter,PT_INTERP 段指向的程序,Linux 上通常为 /lib64/ld-linux-x86-64.so.2)执行符号绑定:
2.3 链接时机的三分类
3. 理论推导与证明
3.1 静态库解析的”按需拷贝”性质
命题:静态库链接满足”惰性拷贝”性质,即未引用的目标文件不会进入最终可执行文件。
证明:设可执行文件初始未定义符号集为 (来自用户代码与启动例程 crt0)。链接器遍历静态库,若 ,则将 加入可执行文件,更新 ;否则 被跳过。由于目标文件 加入与否仅依赖于 是否非空,未引用的 不会被加入。
推论:静态库不会导致”过度膨胀”,但同一符号若在多个静态库中均有定义,则会出现”重复定义”错误。
3.2 动态库的”位置无关”性质
命题:PIC(Position Independent Code)代码在被映射到任意虚拟地址时,无需修改代码段即可正确执行。
证明思路:PIC 代码对所有全局符号的引用通过 GOT(Global Offset Table)间接完成,对所有函数调用通过 PLT(Procedure Linkage Table)间接完成。GOT 与 PLT 位于数据段,可在加载时由动态链接器填充实际地址。代码段只包含相对偏移指令(如 mov rax, [rip + offset]),不包含绝对地址,因此与加载地址无关。
推论:PIC 允许同一份 .so 文件被多个进程映射到不同的虚拟地址,但物理内存只需一份(通过 mmap 共享),这是动态库节省内存的理论基础。
3.3 符号版本与兼容性
设 表示符号 的版本为 。GNU 符号版本机制允许同一 .so 同时导出多个版本的 printf@@GLIBC_2.2.5 与 printf@@GLIBC_2.17。运行时,动态链接器根据可执行文件 .gnu.version_r 中记录的需求版本选择对应实现。
兼容性定理:若库 的版本 同时保留了 的所有符号版本,则依赖 的可执行文件在 上仍可运行(向后兼容)。
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 稳定性设计
- PIMPL 模式(Pointer to IMPLementation):在头文件中仅暴露不透明指针,实现细节放在
.cpp中,修改实现不破坏 ABI。 - 虚函数表稳定:发布的类一旦有虚函数,不能新增虚函数(会改变 vtable 布局);新增功能通过新增非虚函数或子类完成。
- 避免内联:导出的函数不要在头文件中内联,否则客户端可能编译期绑定旧实现。
- 数据成员对齐:发布的结构体不要随意增删字段,如需扩展使用”预留字段”或”扩展结构体”模式。
7.2 版本号管理(SemVer + SONAME)
| 语义化版本变化 | ABI 影响 | SONAME 变化 |
|---|---|---|
MAJOR(不兼容) | 破坏 ABI | libfoo.so.2 |
MINOR(向后兼容) | 新增符号,不破坏旧 ABI | libfoo.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.so | Linux | 列出动态符号表 |
objdump -T libfoo.so | Linux | 列出动态符号(更详细) |
readelf -d libfoo.so | Linux | 查看动态段(NEEDED、SONAME、RPATH) |
ldd main | Linux | 查看可执行文件的动态依赖 |
ldconfig -p | Linux | 列出系统缓存的所有 .so |
patchelf | Linux | 修改 ELF 的 RPATH、SONAME、解释器 |
dumpbin /exports foo.dll | Windows | 列出 DLL 导出符号 |
dumpbin /dependents main.exe | Windows | 列出 EXE 依赖的 DLL |
Dependency Walker / Dependencies | Windows | GUI 工具,递归显示依赖树 |
otool -L libfoo.dylib | macOS | 列出动态依赖 |
install_name_tool | macOS | 修改 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 时,内核实际执行以下流程:
execve系统调用:内核读取main的 ELF 头,找到PT_INTERP段指向的动态链接器(如/lib64/ld-linux-x86-64.so.2)。- 加载动态链接器:内核把
ld.so映射到进程地址空间,把控制权交给ld.so的入口点(_start)。 ld.so自举:初始化自身 GOT,定位自身的.dynamic段。- 读取
main的.dynamic段:找到DT_NEEDED项,递归加载所有依赖的.so。 - 符号解析:根据
DT_SYMTAB、DT_STRTAB、DT_GNU_HASH解析每个未定义符号,更新 GOT/PLT。 - 执行初始化代码:按依赖顺序调用每个
.so的DT_INIT_ARRAY,最后调用main的DT_INIT_ARRAY。 - 调用
_start→__libc_start_main→main:CRT 启动例程设置参数后调用用户main。 - 进程退出:
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 permitted | SELinux/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);