第 42 章 共享库高级特性 (Advanced Features of Shared Libraries)
核心结论
-
dlopen API 运行时装载:
dlopen(name, flags)打开库(带引用计数);dlsym(handle, sym)找符号地址;dlclose(handle)减引用、到 0 时卸载;dlerror()返回错误字符串。flags 中RTLD_LAZY延迟解析函数引用、RTLD_NOW立即全部解析;RTLD_GLOBAL让本库符号对其他 dlopen 可见。 -
dlsym 伪句柄:
RTLD_DEFAULT从主程序开始顺序查;RTLD_NEXT找「调用 dlsym 库之后」加载的同名符号——用于 wrapper functionmalloc()调真malloc()。 -
符号可见性:四道防线——C
static/hidden属性(gccattributevisibility("hidden"))掩藏内部符号;RTLD_GLOBAL控制运行时全局可见;--export-dynamic让主程序符号对 dlopen 库可见;version script精确控制导出表。 -
Linker version script (--version-script):用
.map文件精确定义库导出的符号版本;global: <name>; local: *;的形式;多个VER_N节形成版本链;支持 symbol versioning(同一函数多个版本用@/@@)。 -
初始化与收尾函数:gcc
attributeconstructor在 dlopen 时自动执行;attributedestructor在 dlclose 时执行;旧的_init()/_fini()不推荐(已被取代)。 -
LD_PRELOAD + LD_DEBUG:
LD_PRELOAD让某库先于所有其它库加载——选择性 override 函数;/etc/ld.so.preload系统级;LD_DEBUG=help列可用关键字(libs/reloc/symbols/bindings 等);set-UID 两者都忽略。
|
本章主旨
本章是共享库的「高级刀」——动态装载、符号可见性、版本脚本、初始化收尾、preloading、debug。读完本章你应该能:(1) 用 dlopen 写一个 plug-in 系统;(2) 写一个不污染全局符号、严格 ABI 的共享库;(3) 用 version script 让一个库同时支持老、新两套 ABI(glibc 的 GLIBC_2.0 / GLIBC_2.x 模式);(4) 用 LD_PRELOAD 拦截 malloc/strcmp 做内存调试或字符串 hash;(5) 用 LD_DEBUG 解开「为什么我的库没被加载」的谜。Linux 上这套机制比静态库强大得多,是写「可演进的二方库」、写「模块化服务」、写「chroot sandbox」的关键工具。 |
一、核心概念
本章围绕 6 个核心概念展开:从 dlopen API、符号可见性、version script 与 symbol versioning、constructor/destructor、preload、debug 几个维度。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
dlopen API(运行时装载) |
|
§42.1;编译 |
符号可见性(隐藏内部) |
C |
§42.2;最小化导出 = 减少 interposition + 减少动态符号表 + ABI 稳定 |
Version script 与 symbol versioning |
|
§42.3;编译 |
Constructor / destructor |
|
§42.4;多个 ctor 时按优先级(C++:构造函数);NPTL posix_init 等都靠 ctor |
LD_PRELOAD + /etc/ld.so.preload |
|
§42.5;调试 malloc/free、追踪 socket 调用、做系统级 hook 都靠这个 |
LD_DEBUG 追踪 |
|
§42.6;set-UID 忽略(2.2.5 后);排查「为什么没找到库」 |
二、详细笔记
42.1 动态装载
What:用 dlopen API 在程序运行时加载/卸载共享库,实现 plug-in。
Why:程序启动后按需加载功能模块(编辑器加载 python 模块、服务器加载 filter、编译器加载后端);还能升级某模块不重启主程序。
How——核心四函数(来自 §42.1):
// 摘自《The Linux Programming Interface》 第 42 章
#include <dlfcn.h>
void *dlopen(const char *filename, int flags);
void *dlsym(void *handle, const char *symbol);
int dlclose(void *handle);
const char *dlerror(void);
int dladdr(const void *addr, Dl_info *info);
flags 必须包含 RTLD_LAZY/RTLD_NOW 之一:
/* 延后解析(默认行为) */
void *h = dlopen("./libdemo.so.1", RTLD_LAZY);
/* 立即解析所有符号——慢但立即报错 */
void *h = dlopen("./libdemo.so.1", RTLD_NOW);
/* 符号对其他 dlopen 库可见 */
void *h = dlopen("./libfilter.so", RTLD_LAZY | RTLD_GLOBAL);
/* 检查是否已加载(不实际加载) */
void *h = dlopen("./libdemo.so.1", RTLD_LAZY | RTLD_NOLOAD);
/* 让本库定义优先(防 interposition) */
void *h = dlopen("./libfoo.so", RTLD_LAZY | RTLD_DEEPBIND);
dlsym 关键模式——C99 禁止 void * ↔ 函数指针 直接赋,必须:
int (*funcp)(int); /* 函数指针类型 */
*(void **)(&funcp) = dlsym(h, "my_func");
if (dlerror() != NULL) fatal("dlsym");
res = (*funcp)(somearg);
伪句柄(dlsym 的特殊 handle):
/* 默认:从主程序开始顺序查 */
funcp = dlsym(RTLD_DEFAULT, "malloc");
/* 「我自己后面」——用于 wrapper */
real_malloc = dlsym(RTLD_NEXT, "malloc");
dlopen(NULL, …) 拿主程序的句柄。
When:写 plug-in 系统、模块化服务、动态后端。
Example:来自 §42.1 Listing 42-1 的 dynload.c——接受共享库路径和函数名作为 CLI 参数并执行。
42.2 符号可见性
What:控制库的导出符号表——只暴露 ABI 需要的,隐藏其他。
Why:(1) 防止用户依赖未声明的接口,导致 ABI 锁死;(2) 防止 interposition;(3) 减小动态符号表,加快启动。
How:
-
C static:文件局部,但同时禁止符号被 interposition。
-
gcc visibility hidden:
void __attribute__((visibility("hidden"))) helper(void); /* 编译 -fvisibility=hidden 让所有符号默认 hidden */ -
version script:见 §42.3。
-
RTLD_GLOBAL:让 dlopen 出来的符号对后续 dlopen 可见。
-
--export-dynamic:让主程序符号对 dlopen 库可见——回调机制必需。
When:写新共享库都该用 -fvisibility=hidden,再 version script 列出 public 函数。
42.3 Linker version script
What:.map 文件精确控制导出哪些符号;可定义 symbol versioning 让同一函数有多个版本。
Why:把「符号是否导出」从源码层(static/hidden)提到 linker 层,便于集中管理;symbol versioning 让一个 soname 容纳多个 ABI 版本(glibc 的 GLIBC_2.0/2.1…)。
How:
-
控制可见性(§42.3.1):
$ cat vis.map VER_1 { global: vis_f1; vis_f2; local: *; }; $ gcc -shared -Wl,--version-script,vis.map -o vis.so vis_*.o $ readelf --syms --use-dynamic vis.so | grep vis_ # 只有 vis_f1/vis_f2 -
symbol versioning(§42.3.2):
/* sv_lib_v2.c 中定义两个版本用 assembler directive */ #include <stdio.h> __asm__(".symver xyz_old,xyz@VER_1"); /* 老版 */ __asm__(".symver xyz_new,xyz@@VER_2"); /* 默认版 */ void xyz_old(void) { printf("v1 xyz\n"); } void xyz_new(void) { printf("v2 xyz\n"); } /* sv_v2.map */ VER_1 { global: xyz; local: *; }; VER_2 { global: pqr; } VER_1; /* VER_2 依赖 VER_1 */ /* 新程序链接后,xy z@@VER_2;老程序已经链接的用 xyz@VER_1 */ -
glibc 使用:
GLIBC_2.0、GLIBC_2.1等。 -
匿名 version tag:现代 ld 允许没有
VER_N名字的匿名 tag,但只能一个。
When:发新版本要保留旧程序兼容性时;写库需要严格 ABI 时。
42.4 constructor / destructor
What:库装载/卸载时自动执行的函数;用于初始化数据结构、注册回调。
Why:库作者可声明「一旦装载就完成 xxx 准备」;比依赖程序显式调用 lib_init() 更不易出错。
How——gcc 属性:
// 摘自《The Linux Programming Interface》 第 42 章
void __attribute__((constructor)) some_name_load(void) {
/* 初始化代码;dlopen() 时自动执行 */
}
void __attribute__((destructor)) some_name_unload(void) {
/* 收尾代码;dlclose() 时自动执行 */
}
旧的 _init()/_fini() 用 gcc -nostartfiles 让 ld 不加默认版本;用 -Wl,-init= / -Wl,-fini= 改名字。已不推荐——constructor 支持多个。
When:库的初始化用 constructor;C++ 静态对象构造也是 ctor。
42.5 LD_PRELOAD + /etc/ld.so.preload
What:LD_PRELOAD 指定一组共享库在所有其他库之前加载;其中定义的符号将掩盖其他库里同名符号。/etc/ld.so.preload 系统级等价。
Why:选择性 override 标准库函数(malloc/strcmp/open),不修改原二进制。
How:
# 选择性 override x1()
$ LD_PRELOAD=./libalt.so ./prog
Called mod1-x1 ALT
Called mod2-x2 DEMO # libalt.so 没定义的 x2 走原库
# 在 libalt 中 dlsym(RTLD_NEXT, "malloc") 拿真 malloc
/etc/ld.so.preload(系统级):
# /etc/ld.so.preload 内容(一行一个 .so 路径)
/opt/myapp/libmalloc.so
安全:LD_PRELOAD 和 /etc/ld.so.preload 都在 set-UID 程序上忽略——不能让用户用私有库伪装系统调用。
When:debug malloc (jemalloc/tcmalloc)、做文件 I/O 跟踪、做全系统 audit、做 fakeroot(bind mount 模拟 root)。
42.6 LD_DEBUG 追踪动态链接器
What:环境变量 LD_DEBUG 让 ld-linux.so 输出运行时诊断。
Why:调试「为什么 libfoo 没被加载」「为什么符号找不到」。
How:
$ LD_DEBUG=help date
Valid options for the LD_DEBUG environment variable are:
libs # 库搜索路径
reloc # 重定位处理
files # 进度
symbols # 符号表
bindings # 符号绑定
versions # 版本依赖
all # 上述全部
statistics # 重定位统计
unused # 未引用 DSO
help # 帮助
$ LD_DEBUG=libs date # 看每条 DT_NEEDED 怎么解析
$ LD_DEBUG=bindings prog # 详细看每个符号在哪家找的
$ LD_DEBUG_OUTPUT=trace LD_DEBUG=libs prog # 输出到 trace.PID
set-UID 程序忽略(glibc 2.2.5 后)。
When:动态库加载失败时第一道诊断;理解 ABI 兼容性。
三、关键图表
|
非可视化条目(dlopen flags 与 LD_DEBUG)
|
|
符号隐藏优先级对照表
|
四、思维导图
mindmap
root((第 42 章 共享库高级))
dlopen API
dlopen dlclose dlsym
RTLD LAZY NOW
RTLD DEFAULT NEXT
DL 信息 dladdr
符号可见性
static
visibility hidden
Bsymbolic
export dynamic
version script
symbol versioning
version tag VER N
symver OLD NEW VER
依赖链 VER 2 依赖 VER 1
glibc GLIBC 2.0..
constructor destructor
gcc attribute
dlopen 自动调用
替代旧 init fini
LD_PRELOAD
选择性 override
set UID 忽略
etc ld.so.preload
LD_DEBUG
libs bindings all
set UID 忽略
diagnostics
五、重点与易错点
-
dlopen 引用计数——多次 dlopen 同一文件只装载一次,但引用计数增加;必须 dlclose 等量次数才真正卸载。
-
dlsym 不能直接赋函数指针——C99 禁止;用
(void *)(&fp) = dlsym(…)。 -
dlsym NULL 歧义——「真的符号值是 NULL」和「找不到符号」都返回 NULL;调用 dlsym 前用
dlerror()清缓存,调用后查dlerror()。 -
dlopen + RTLD_LAZY——函数符号立即解析,变量符号总是立即解析,函数引用按需;想让函数也立即解析用 RTLD_NOW。
-
dlclose 卸载是递归的——如果库的 DT_NEEDED 链上的库没人再引用,也会被卸载。
-
LD_PRELOAD 在 set-UID 程序上忽略——安全,防止 set-UID root 程序被恶意库劫持。
-
constructor 在 dlopen 时自动调用——和 C++ 全局对象的构造时机一致;对库的隐式初始化很方便。
-
destructor 在 dlclose 时调用——但 _exit() 不会调用;在写退出处理时小心(必要时用
atexit())。 -
version script 用 C 风格的通配——
*通配任意字符、?通配单字符;用 glob(7) 规则。 -
symbol versioning 的 @ vs @@:
@是非默认版本;@@是默认版本(同一函数只能一个 @@)。 -
匿名 version tag——只能一个;用真实版本名时多个节点可形成依赖链。
-
glibc 用
GLIBC_2.x形式命名 version——推荐实践:<package>_<major>.<minor>。 -
--enable-new-dtags 切到 DT_RUNPATH——DT_RUNPATH 优先级低于 LD_LIBRARY_PATH;DT_RPATH 高于 LD_LIBRARY_PATH。
-
RTLD_DEEPBIND 是 Linux-specific——Solaris 也有;其他 UNIX 没有;可移植代码慎用。
-
dlmopen(LM_ID_NEWLM, …)——把库装入独立 link-map namespace,避免符号污染;很少用。
-
跨章衔接:第 27 章 exec;第 41 章共享库基础;第 38 章 set-UID 安全(LD_PRELOAD);第 28 章 ELF。