第 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) 用 $ORIGIN 让应用自带库。读者应掌握三个名字和 ldconfig 流程——这是看懂 rpm -V / ldd 输出、理解 ABI 兼容性的基础。

      一、核心概念

      本章围绕 6 个核心概念展开:从 object library 起,到 ELF 动态链接、命名约定、缓存管理、运行时符号解析、$ORIGIN 自带库。

      概念 定义 + 重要性 实现提示

      Object library (静态 vs 共享)

      静态库 (.a) 拷进二进制;共享库 (.so) 在运行时加载、跨进程共享代码——磁盘/内存省,但首次加载慢、依赖 PIC

      §41.1-41.3;共享库代价 = PIC 性能开销 + 启动时链接符号解析;ELF 取代 a.out/COFF

      PIC + soname 创建库

      编译 gcc -c -fPIC mod.c;链接 gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0.1 mod.o;运行时找 libfoo.so.1 符号链接指向 real name

      §41.4;`nm -D mod.o

      grep GLOBAL_OFFSET_TABLE` 验 PIC;`objdump -p libfoo.so

      grep SONAME` 看 soname

      ELF DT_NEEDED + ld-linux

      静态链接嵌入 DT_NEEDED 记录依赖 soname;可执行运行时由 /lib/ld-linux.so.2 加载;DT_RPATH/DT_RUNPATH 提供自定义搜索路径

      §41.4.3;grep SONAME 验 DT_SONAME;`objdump -p prog

      grep NEEDED` 看 dep list;ldd prog 列表

      三个名字 + ldconfig

      real name libfoo.so.M.K(文件);soname libfoo.so.M(符号链接到 real,embed 进 executable);linker name libfoo.so(链接到 soname,给 -l 用);ldconfig 重建 cache + 自动维护 soname 链接

      §41.6, 41.7;`ldconfig -p

      grep libfoo` 看 cache 内容;新装/移除/改路径都要跑 ldconfig

      运行时符号解析

      主程序符号 > 共享库;共享库间按 -l 左→右顺序;-Bsymbolic 让库内符号优先绑到本库;RTLD_DEEPBIND 类似

      §41.12;RTLD_DEFAULT/RTLD_NEXT 提供 dynload 时的覆盖

      $ORIGIN rpath + LD_LIBRARY_PATH

      gcc -Wl,-rpath,/dir 把目录烧进 binary;$ORIGIN 表示「可执行所在目录」——可分发的「turn-key」应用;LD_LIBRARY_PATH 调试用、set-UID 不生效

      §41.10;`objdump -p prog

      grep -E 'RPATH

      RUNPATH'`;--enable-new-dtags 切到 DT_RUNPATH(低优先级)

      二、详细笔记

      41.1 Object Library 概述

      What:可复用的 object 集合;静态 (.a) / 共享 (.so) 两类;共享是主流。

      Why:可复用 = 不用每个程序重新编译;不复制 = 节省。

      How

      1. 静态库ar r libdemo.a mod1.o mod2.o(r=replace);ar t 看表;ar d 删。链接用 cc -o prog prog.o libdemo.a-Ldir -ldemo

      2. 共享库:后续小节。

      3. 调试信息:永远用 -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

      1. 节省:磁盘(不复制)、内存(text 段共享)。

      2. 代价:PIC 开销、首次启动找库 + 符号重定位。

      3. 额外优势: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

      1. ldd prog:列出 prog 的库依赖及其解析路径。

      2. nm -D lib.so | grep func:找符号定义。

      3. objdump -p lib.so | grep SONAME:看 soname。

      4. readelf -d lib.so | grep -E 'NEEDED|SONAME|RUNPATH':看动态段。

      5. 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

      Whatldconfig 重建 /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 兼容:

      1. public 函数签名不变(参数、返回类型)。

      2. public 函数语义不变(同样的输入给同样的输出)。

      3. public 结构不变(增字段可能破坏,但留 padding 还好)。

      4. 不删 public 函数/变量。

      5. 加新 public 函数/变量 OK。

      → 否则改 major,新建 libfoo.so.2

      When:每次发版前评估哪些是兼容变更。

      41.10 rpath + DT_RPATH/DT_RUNPATH

      Whatrpath 是「把库的搜索路径烧进 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:

      1. DT_RPATH(仅当无 DT_RUNPATH 时)

      2. LD_LIBRARY_PATH(set-UID 忽略)

      3. DT_RUNPATH

      4. /etc/ld.so.cache

      5. /lib /usr/lib

      Why:理解为什么 set-UID 程序不能用 LD_LIBRARY_PATH(安全);理解为什么 DT_RPATH 比 DT_RUNPATH 优先级高。

      How:调试时 LD_DEBUG=libs prog 看每条 DT_NEEDED 怎么解析。

      41.12 运行时符号解析

      What:符号查找顺序(来自 §41.12):

      1. main program 定义优先于 shared library

      2. shared libraries 按 DT_NEEDED / -l 左→右

      → 默认共享库不保证自己的符号绑到自己;用 -Bsymbolic 让库内符号先绑到本库。

      When:写「模块化」共享库(不希望被 main 符号覆盖);用 -Bsymbolic-functions 仅函数。

      41.13 静态 vs 共享链接的选择

      What:默认共享;要静态用 gcc -static 或命名 .a 路径。

      When:chroot、单文件部署、避免 ABI 变化时静态;否则共享。

      三、关键图表

      非可视化条目(库与变量一览)
      描述

      libfoo.so.MAJOR.MINOR

      real name,文件本身

      libfoo.so.MAJOR

      soname,符号链接指向 real name latest minor

      libfoo.so

      linker name,符号链接指向 soname,给 -lfoo

      DT_NEEDED

      ELF 段,列程序的依赖 soname

      DT_SONAME

      ELF 段,库自己的 soname

      DT_RPATH

      run-time 搜索路径(旧);高优先级(在 LD_LIBRARY_PATH 之前)

      DT_RUNPATH

      run-time 搜索路径(新);低优先级(LD_LIBRARY_PATH 之后)

      GLOBAL_OFFSET_TABLE

      PIC object 中的 GOT 符号

      TEXTREL

      仍有非 PIC 段(应避免)

      ldconfig

      重建 /etc/ld.so.cache + 维护 soname 符号链接

      ldd

      列程序共享库依赖及解析路径

      ar r/d/t

      archive 维护(replace/delete/table)

      $ORIGIN

      rpath 中的「可执行所在目录」

      LD_LIBRARY_PATH

      调试用,set-UID 忽略

      LD_DEBUG=libs

      看 ld-linux 详细搜索路径

      -Bsymbolic

      库内符号优先绑到本库

      -enable-new-dtags

      链接到 DT_RUNPATH 而非 DT_RPATH

      nm -D / objdump -p / readelf -d

      查看 SONAME/NEEDED 的工具

      兼容性 vs 不兼容性
      变更类型 是否兼容 soname 决策

      修 bug(不改语义)

      兼容

      不变

      性能优化

      兼容

      不变

      加新 public 函数/变量

      兼容

      不变

      增结构末尾字段(带 padding)

      兼容(谨慎)

      不变

      删 public 函数/变量

      不兼容

      改 major

      改函数签名(参数类型、返回类型)

      不兼容

      改 major

      改 public 全局变量的类型/语义

      不兼容

      改 major

      四、思维导图

      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

      五、重点与易错点

      1. 共享库在运行前没加载——首次 exec 时 ld-linux.so 找库;找不到 error in loading shared libraries

      2. DT_RPATH vs DT_RUNPATH:DT_RPATH 高优先级(绕过 LD_LIBRARY_PATH);DT_RUNPATH 低(LD_LIBRARY_PATH 优先);新版 glibc 默认 DT_RPATH。

      3. soname 必须有对应符号链接:只有 real name 时程序找不到库;ln -s realname soname 必须做。

      4. set-UID 程序忽略 LD_LIBRARY_PATH(§41.11 规则 2)——安全考虑,防止用私有库伪造库。

      5. PIC 不是可选优化而是强制:x86-32 不带 -fPIC 也能编译,但失去 text 段共享;其他架构会直接编译错。

      6. ABI 兼容性规则(§41.8):函数签名/语义/结构不变才「兼容」;加函数 OK;删/改 → 改 major。

      7. 符号解析默认主程序优先——同名符号 main 程序 > 所有共享库;这个语义有时出乎意料。

      8. 升 minor 不需要重链接:已运行的程序继续用旧 minor(加载时的副本);只有新 exec 才用新版。

      9. *升 major 需要重链接*或保留旧 soname 让旧二进制继续用——最好两个 real name/soname 都装(libfoo.so.1.0.2 + libfoo.so.2.0.0)。

      10. ldconfig 必须在装完库后跑一次:否则 ld.so.cache 还是旧的,运行时找不到。

      11. nm -D 只看动态符号(导出表);静态库用 nm mod.o;soname 不在符号表里,是 DT_SONAME 段。

      12. $ORIGIN 是 ld-linux.so 识别的特殊字符串——不是 shell 变量;只在 -Wl,-rpath,'$ORIGIN'/lib 这种字面写法里生效。

      13. -Bsymbolic 性能 + 正确性双重收益:减少启动时符号重定位 + 防止「同名符号覆盖」带来的怪异行为。

      14. 静态库的.object 用 ar r 加;共享库的 object 加后会被 stripped——所以共享库不能「加.o」。

      15. GLIBC versioning(预告 §42.3):glibc 用 symbol versioning 让 2.0 之后所有版本并存于 libc.so.6,无需改 major。

      16. 跨章衔接:第 27 章 exec 后动态链接器启动;第 38 章 set-UID 安全(LD_LIBRARY_PATH 忽略);第 42 章 高级特性(dlopen、symbol versioning);第 28 章 ELF 程序装载。

      Asciidoc lint check

      asciidoctor: 无警告。