第 41 章 共享库基础 (Fundamentals of Shared Libraries)
核心结论
-
共享库 vs 静态库:静态库 (
lib*.a) 把 object 直接拷贝进可执行文件;共享库 (lib*.so) 在*运行时*由动态链接器 (ld-linux.so) 加载到内存,跨进程共享代码段——节省磁盘和内存,但增加首次加载时间和少量运行开销。 -
三个名字命名约定:real name(
libfoo.so.1.0.1)= 文件本身;soname(libfoo.so.1)= major 版本号,符号链接;linker name(libfoo.so)= 没版本号的符号链接,供-l选项。minor 升级改 real name、改 soname 链接指向即可——已运行的进程继续用旧版。 -
位置无关代码 (PIC) 与 -fPIC:共享库的 object 模块必须用
-fPIC编译;通过GLOBAL_OFFSET_TABLE_实现位置无关引用。Linux/x86-32 上-fPIC之外也行(但丢失跨进程共享 text 段)。 -
动态链接流程:静态链接时把
DT_NEEDED嵌入可执行;运行时ld-linux.so按 (1) DT_RPATH → (2) LD_LIBRARY_PATH → (3) DT_RUNPATH → (4) /etc/ld.so.cache → (5) /lib:/usr/lib 顺序搜索——set-user-ID 程序忽略 LD_LIBRARY_PATH。 -
符号解析规则:主程序定义优先于共享库;多个共享库间按
-l给定的左→右顺序找;用-Wl,-Bsymbolic让共享库内的全局符号优先绑定到该库。 -
现代工具链:
gcc -shared -Wl,-soname,libxx.so.N -o libxx.so.M.K …;ar r libfoo.a mod.o维护 archive;ldconfig(8)重建/etc/ld.so.cache+ 自动维护 soname 链接;ldd列依赖。
|
本章主旨
共享库是 UNIX 系统复用代码的标准机制——把库的代码段共享给所有使用它的进程,节省 RAM + 磁盘 + 支持运行时热升级(minor 升级不重链接)。本章系统地讲 (1) object library 基本概念(静态 vs 共享);(2) 创建共享库(-fPIC + -shared + soname);(3) ELF 动态链接器的工作(DT_NEEDED、DT_RPATH、DT_SONAME、DT_RUNPATH);(4) 版本命名约定(real/soname/linker);(5) ldconfig 缓存维护;(6) 运行时符号解析规则;(7) 用 |
一、核心概念
本章围绕 6 个核心概念展开:从 object library 起,到 ELF 动态链接、命名约定、缓存管理、运行时符号解析、$ORIGIN 自带库。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
Object library (静态 vs 共享) |
静态库 (.a) 拷进二进制;共享库 (.so) 在运行时加载、跨进程共享代码——磁盘/内存省,但首次加载慢、依赖 PIC |
§41.1-41.3;共享库代价 = PIC 性能开销 + 启动时链接符号解析;ELF 取代 a.out/COFF |
PIC + soname 创建库 |
编译 |
§41.4;`nm -D mod.o |
grep GLOBAL_OFFSET_TABLE` 验 PIC;`objdump -p libfoo.so |
grep SONAME` 看 soname |
ELF DT_NEEDED + ld-linux |
静态链接嵌入 |
§41.4.3; |
grep NEEDED` 看 dep list; |
三个名字 + ldconfig |
real name |
§41.6, 41.7;`ldconfig -p |
grep libfoo` 看 cache 内容;新装/移除/改路径都要跑 |
运行时符号解析 |
主程序符号 > 共享库;共享库间按 |
§41.12; |
$ORIGIN rpath + LD_LIBRARY_PATH |
|
§41.10;`objdump -p prog |
grep -E 'RPATH |
RUNPATH'`; |
二、详细笔记
41.1 Object Library 概述
What:可复用的 object 集合;静态 (.a) / 共享 (.so) 两类;共享是主流。
Why:可复用 = 不用每个程序重新编译;不复制 = 节省。
How:
-
静态库:
ar r libdemo.a mod1.o mod2.o(r=replace);ar t看表;ar d删。链接用cc -o prog prog.o libdemo.a或-Ldir -ldemo。 -
共享库:后续小节。
-
调试信息:永远用
-g;不要用strip(无 symbol);x86-32 不要用-fomit-frame-pointer。
When:写可复用代码 → 库(首选共享);一次性代码 → 直接编译进可执行。
Example:
// 摘自《The Linux Programming Interface》 第 41 章
$ cc -g -c mod1.c mod2.c mod3.c
$ ar r libdemo.a mod1.o mod2.o mod3.o // 静态库
$ rm mod1.o mod2.o mod3.o
$ cc -g -o prog prog.o libdemo.a // 链接
$ ./prog
Called mod1-x1
41.2 静态库
What:ar 创建的 archive,本质上是一堆 object 文件打包;链接时被选择性抽取。
Why:减少链接命令行;提供「按需抽取」机制。
How:
// 摘自《The Linux Programming Interface》 第 41 章
$ cc -g -c prog.c
$ cc -g -o prog prog.o -ldemo // 链接共享
$ cc -g -o prog prog.o -static -ldemo // 强制静态
$ cc -g -o prog prog.o -Wl,-Bstatic -ldemo -Wl,-Bdynamic // 混合
When:chroot jail、单文件部署、无依赖环境;否则优先共享。
41.3 共享库总览
What:单一副本跨进程共享;disk/RAM 省;可热升级 minor 版本。
Why:现代 Linux 的事实标准。
How:
-
节省:磁盘(不复制)、内存(text 段共享)。
-
代价:PIC 开销、首次启动找库 + 符号重定位。
-
额外优势:bug fix 升级时不用重链接已有可执行(minor 版本升级即可)。
When:所有现代应用都默认共享库。
41.4 创建 + 使用共享库——第一步
What:编译 -fPIC、链接 -shared -Wl,-soname,…;运行时 ld-linux.so 找 DT_NEEDED 指定的 soname。
Why:理解 soname 是理解版本兼容性的基础。
How——关键流程(来自 §41.4):
// 摘自《The Linux Programming Interface》 第 41 章
// 1) 编译
$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
// 2) 链接成共享库
$ gcc -g -shared -o libfoo.so mod1.o mod2.o mod3.o
// 3) 链接程序(运行时找 libfoo.so)
$ gcc -g -Wall -o prog prog.c libfoo.so
// 4) 运行(在共享库目录加 LD_LIBRARY_PATH=. )
$ LD_LIBRARY_PATH=. ./prog
DT_NEEDED:链接器把 libfoo.so(或 soname)嵌进可执行的 DT_NEEDED;运行时 ld-linux.so 据此找库。
When:创建任何共享库都按此流程。
Example(带 soname 的版本):
$ gcc -g -shared -Wl,-soname,libbar.so -o libfoo.so mod1.o mod2.o
$ ln -s libfoo.so libbar.so # soname 必须有 symbol link
$ gcc -o prog prog.c libfoo.so
$ LD_LIBRARY_PATH=. ./prog # 依赖 libbar.so 而非 libfoo.so
41.5 工具:nm、ldd、objdump、readelf
What:辅助分析共享库的工具集。
Why:调试「符号找不到」「库不存在」必用。
How:
-
ldd prog:列出 prog 的库依赖及其解析路径。
-
nm -D lib.so | grep func:找符号定义。
-
objdump -p lib.so | grep SONAME:看 soname。
-
readelf -d lib.so | grep -E 'NEEDED|SONAME|RUNPATH':看动态段。
-
readelf -V lib.so:看版本脚本定义的 version tag。
When:调试、验证 ABI、写 build 脚本时常用。
41.6 版本与命名约定
What:libfoo 的 real name = libfoo.so.MAJOR.MINOR;soname = libfoo.so.MAJOR;linker name = libfoo.so。Major 改 = ABI 不兼容;minor 改 = 兼容升级。
Why:允许多 major 版本并存 + 已运行进程用旧版。
How——标准流程(来自 §41.6):
# 1. 创建 real name + 嵌 soname
$ gcc -shared -Wl,-soname,libdemo.so.1 -o libdemo.so.1.0.1 mod.o mod2.o
# 2. 创建 soname 符号链接
$ ln -s libdemo.so.1.0.1 libdemo.so.1
# 3. 创建 linker name 符号链接
$ ln -s libdemo.so.1 libdemo.so
# 4. linker name 用于编译时
$ gcc -o prog prog.c -L. -ldemo
兼容 vs 不兼容变更(§41.8):不变 public 函数签名/语义/结构 → 改 minor;删/改 public 接口 → 改 major。
When:发新版本时按 ABI 决定改 major 还是 minor。
41.7 ldconfig 与 /etc/ld.so.cache
What:ldconfig 重建 /etc/ld.so.cache(ld-linux.so 用的查找缓存)+ 自动维护 /etc/ld.so.conf 列出的目录中的 soname 符号链接。
Why:动态链接器加速查找 + 自动维护 soname 链接。
How:
# /etc/ld.so.conf 例子
/usr/local/lib
/opt/X11/lib
# 重建 cache + soname 链接
$ ldconfig -v
# 仅看 cache
$ ldconfig -p
# 不重建 cache(仅维护 soname 链接)
$ ldconfig -n . # 处理当前目录的 .so
When:装新库、删旧库、修改 ld.so.conf 后都要跑一次 ldconfig;容器构建时也需要。
41.8 兼容 vs 不兼容变更
What:minor 改 = 同 ABI 兼容;major 改 = ABI 破了。
Why:决定 soname 改不改;改 soname 意味着旧可执行会找不到库。
How——保留 ABI 兼容:
-
public 函数签名不变(参数、返回类型)。
-
public 函数语义不变(同样的输入给同样的输出)。
-
public 结构不变(增字段可能破坏,但留 padding 还好)。
-
不删 public 函数/变量。
-
加新 public 函数/变量 OK。
→ 否则改 major,新建 libfoo.so.2。
When:每次发版前评估哪些是兼容变更。
41.10 rpath + DT_RPATH/DT_RUNPATH
What:rpath 是「把库的搜索路径烧进 ELF」的机制;DT_RPATH 高优先级、DT_RUNPATH 低优先级(会让 LD_LIBRARY_PATH 优先)。
Why:自带的「turn-key」应用需要这种机制——可执行自带 .so 在子目录、不依赖系统 LD_LIBRARY_PATH。
How:
# 烧一个目录进 prog 的 DT_RPATH
$ gcc -Wl,-rpath,/opt/myapp/lib -o prog prog.c -L/opt/myapp/lib -lmylib
# 多目录
$ gcc -Wl,-rpath,/d1 -Wl,-rpath,/d2 -o prog ...
# 使用 $ORIGIN(当前可执行所在目录)
$ gcc -Wl,-rpath,'$ORIGIN'/lib -o prog ...
# 切到 DT_RUNPATH(新版 glibc)—— LD_LIBRARY_PATH 优先于它
$ gcc -Wl,--enable-new-dtags -Wl,-rpath,/d1 ...
When:发布自带的二进制分发包;不污染系统库路径。
41.11 运行时库查找顺序
What:ld-linux.so 按以下顺序找 soname:
-
DT_RPATH(仅当无 DT_RUNPATH 时)
-
LD_LIBRARY_PATH(set-UID 忽略)
-
DT_RUNPATH
-
/etc/ld.so.cache
-
/lib /usr/lib
Why:理解为什么 set-UID 程序不能用 LD_LIBRARY_PATH(安全);理解为什么 DT_RPATH 比 DT_RUNPATH 优先级高。
How:调试时 LD_DEBUG=libs prog 看每条 DT_NEEDED 怎么解析。
三、关键图表
|
非可视化条目(库与变量一览)
|
|
兼容性 vs 不兼容性
|
四、思维导图
mindmap
root((第 41 章 共享库基础))
Object library
静态 .a
共享 .so
ELF 现代格式
创建共享库
-fPIC
-shared
-Wl,-soname
三名字命名
real libfoo.so.1.0.1
soname libfoo.so.1
linker libfoo.so
DT_NEEDED 嵌入
ld-linux.so
ldconfig cache
ldd 验证
查找顺序
DT_RPATH
LD_LIBRARY_PATH
DT_RUNPATH
ld.so.cache
lib 与 usr lib
ABI 兼容
签名不变
语义不变
结构不变
minor 升级
工具链
ar 维护
nm 查符号
objdump DT
readelf ELF
rpath 与 ORIGIN
DT_RPATH DT_RUNPATH
$ORIGIN turn-key
LD_LIBRARY_PATH
五、重点与易错点
-
共享库在运行前没加载——首次 exec 时 ld-linux.so 找库;找不到
error in loading shared libraries。 -
DT_RPATH vs DT_RUNPATH:DT_RPATH 高优先级(绕过 LD_LIBRARY_PATH);DT_RUNPATH 低(LD_LIBRARY_PATH 优先);新版 glibc 默认 DT_RPATH。
-
soname 必须有对应符号链接:只有 real name 时程序找不到库;
ln -s realname soname必须做。 -
set-UID 程序忽略 LD_LIBRARY_PATH(§41.11 规则 2)——安全考虑,防止用私有库伪造库。
-
PIC 不是可选优化而是强制:x86-32 不带 -fPIC 也能编译,但失去 text 段共享;其他架构会直接编译错。
-
ABI 兼容性规则(§41.8):函数签名/语义/结构不变才「兼容」;加函数 OK;删/改 → 改 major。
-
符号解析默认主程序优先——同名符号 main 程序 > 所有共享库;这个语义有时出乎意料。
-
升 minor 不需要重链接:已运行的程序继续用旧 minor(加载时的副本);只有新 exec 才用新版。
-
*升 major 需要重链接*或保留旧 soname 让旧二进制继续用——最好两个 real name/soname 都装(
libfoo.so.1.0.2+libfoo.so.2.0.0)。 -
ldconfig 必须在装完库后跑一次:否则 ld.so.cache 还是旧的,运行时找不到。
-
nm -D 只看动态符号(导出表);静态库用
nm mod.o;soname 不在符号表里,是 DT_SONAME 段。 -
$ORIGIN 是 ld-linux.so 识别的特殊字符串——不是 shell 变量;只在
-Wl,-rpath,'$ORIGIN'/lib这种字面写法里生效。 -
-Bsymbolic 性能 + 正确性双重收益:减少启动时符号重定位 + 防止「同名符号覆盖」带来的怪异行为。
-
静态库的.object 用
ar r加;共享库的 object 加后会被 stripped——所以共享库不能「加.o」。 -
GLIBC versioning(预告 §42.3):glibc 用 symbol versioning 让 2.0 之后所有版本并存于 libc.so.6,无需改 major。
-
跨章衔接:第 27 章 exec 后动态链接器启动;第 38 章 set-UID 安全(LD_LIBRARY_PATH 忽略);第 42 章 高级特性(dlopen、symbol versioning);第 28 章 ELF 程序装载。