第 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 function malloc() 调真 malloc()

      • 符号可见性:四道防线——C static/hidden 属性(gcc attributevisibility("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_DEBUGLD_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(运行时装载)

      dlopen(name, RTLD_LAZY/NOW [+GLOBAL]) 装载 + 引用计数;dlsym(h, sym) 找符号;dlclose(h) 卸载;dlerror() 错误字符串;dladdr(addr, info) 反查符号

      §42.1;编译 -ldl;dlsym 返回 void ,赋给函数指针用 (void **)(&fp) = dlsym(…​)RTLD_NOLOAD 只查已装载的库

      符号可见性(隐藏内部)

      C static(文件局部)+ gcc visibility("hidden") 属性(库内部);--version-script 精确控制;RTLD_GLOBAL 控制 dlopen 符号范围;--export-dynamic 让主程序符号对 dlopen 库可见

      §42.2;最小化导出 = 减少 interposition + 减少动态符号表 + ABI 稳定

      Version script 与 symbol versioning

      .map 文件用 global: f1; local: *; 定义可见符号;version tag 形成版本链(VER_2 继承 VER_1);同函数多版本用 asm(".symver xyz_old,xyz@VER_1")xyz_new@@VER_2;glibc 在 libc.so.6 装下所有老版本

      §42.3;编译 -Wl,--version-script,myscriptfile.map;default 版本用 @@

      Constructor / destructor

      void attributeconstructor init_fn(void) 在 dlopen 时自动执行;attributedestructor fini_fn 在 dlclose 时执行;旧的 _init()/_fini() 不推荐

      §42.4;多个 ctor 时按优先级(C++:构造函数);NPTL posix_init 等都靠 ctor

      LD_PRELOAD + /etc/ld.so.preload

      LD_PRELOAD=libfoo.so prog 让 libfoo 先于其他库加载——选择性 override 符号;/etc/ld.so.preload 系统级(要先 set-UID 不被禁);两者 set-UID 忽略

      §42.5;调试 malloc/free、追踪 socket 调用、做系统级 hook 都靠这个

      LD_DEBUG 追踪

      LD_DEBUG=help 列出可选:libs(搜索路径)、reloc(重定位)、files(输入文件)、symbolsbindings(绑定)、versionsallstatisticsunusedhelpLD_DEBUG_OUTPUT=file 输出到文件

      §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

      1. C static:文件局部,但同时禁止符号被 interposition。

      2. gcc visibility hidden

        void __attribute__((visibility("hidden"))) helper(void);
        /* 编译 -fvisibility=hidden 让所有符号默认 hidden */
      3. version script:见 §42.3。

      4. RTLD_GLOBAL:让 dlopen 出来的符号对后续 dlopen 可见。

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

      1. 控制可见性(§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
      2. 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 */
      3. glibc 使用GLIBC_2.0GLIBC_2.1 等。

      4. 匿名 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

      WhatLD_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)
      描述

      RTLD_LAZY

      函数符号按需解析(默认)

      RTLD_NOW

      立即解析所有符号

      RTLD_GLOBAL

      本库符号对后续 dlopen 可见

      RTLD_LOCAL

      本库符号私有(默认)

      RTLD_NODELETE

      dlclose 不真正卸载

      RTLD_NOLOAD

      不实际加载,只查已加载

      RTLD_DEEPBIND

      本库优先用自己符号,不被覆盖

      RTLD_DEFAULT

      dlsym 伪句柄:主程序 → 全局库

      RTLD_NEXT

      dlsym 伪句柄:本库之后的同名符号

      LD_PRELOAD=lib.so prog

      选择性 override 函数(set-UID 忽略)

      /etc/ld.so.preload

      系统级 LD_PRELOAD

      LD_BIND_NOW=1

      RTLD_LAZY 模式下也立即解析

      LD_DEBUG=libs

      看每条依赖的解析路径

      LD_DEBUG=bindings

      看每个符号的实际绑定位置

      LD_DEBUG=all

      上述全部

      LD_DEBUG_OUTPUT=file

      输出到 file.PID 文件

      visibility="hidden"

      gcc 属性,标记符号库内私有

      -fvisibility=hidden

      默认所有符号 hidden

      --export-dynamic

      主程序符号对 dlopen 库可见

      -Bsymbolic / -Bsymbolic-functions

      库内符号优先绑到本库

      asm(".symver OLD,NEW@VER")

      标注 OLD 是 NEW 的版本 VER

      asm(".symver OLD,NEW@@VER")

      标注 OLD 是 NEW 的默认版本

      --version-script=f.map

      链接时用 f.map 控制导出

      attributeconstructor

      dlopen 时自动执行的函数

      attributedestructor

      dlclose 时自动执行的函数

      符号隐藏优先级对照表
      机制 范围 备注

      C static

      翻译单元内

      同时防止 interposition

      gcc visibility("hidden")

      库内全局可见,库外不可见

      同时防止 interposition

      --version-script local: *;

      linker 决定;可跨越多文件

      控制导出表

      dlsym RTLD_LOCAL

      dlopen 出来的库

      默认

      dlsym RTLD_GLOBAL

      dlopen 出来的库

      让其他库也能用

      dlsym RTLD_DEEPBIND

      dlopen 出来的库

      优先本库定义

      -Bsymbolic(链接选项)

      库内符号优先绑自己

      启动快且行为更确定

      --export-dynamic(链接选项)

      主程序符号对 dlopen 库可见

      callback 用

      四、思维导图

      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

      五、重点与易错点

      1. dlopen 引用计数——多次 dlopen 同一文件只装载一次,但引用计数增加;必须 dlclose 等量次数才真正卸载。

      2. dlsym 不能直接赋函数指针——C99 禁止;用 (void *)(&fp) = dlsym(…​)

      3. dlsym NULL 歧义——「真的符号值是 NULL」和「找不到符号」都返回 NULL;调用 dlsym 前用 dlerror() 清缓存,调用后查 dlerror()

      4. dlopen + RTLD_LAZY——函数符号立即解析,变量符号总是立即解析,函数引用按需;想让函数也立即解析用 RTLD_NOW。

      5. dlclose 卸载是递归的——如果库的 DT_NEEDED 链上的库没人再引用,也会被卸载。

      6. LD_PRELOAD 在 set-UID 程序上忽略——安全,防止 set-UID root 程序被恶意库劫持。

      7. constructor 在 dlopen 时自动调用——和 C++ 全局对象的构造时机一致;对库的隐式初始化很方便。

      8. destructor 在 dlclose 时调用——但 _exit() 不会调用;在写退出处理时小心(必要时用 atexit())。

      9. version script 用 C 风格的通配——* 通配任意字符、? 通配单字符;用 glob(7) 规则。

      10. symbol versioning 的 @ vs @@@ 是非默认版本;@@ 是默认版本(同一函数只能一个 @@)。

      11. 匿名 version tag——只能一个;用真实版本名时多个节点可形成依赖链。

      12. glibc 用 GLIBC_2.x 形式命名 version——推荐实践:<package>_<major>.<minor>

      13. --enable-new-dtags 切到 DT_RUNPATH——DT_RUNPATH 优先级低于 LD_LIBRARY_PATH;DT_RPATH 高于 LD_LIBRARY_PATH。

      14. RTLD_DEEPBIND 是 Linux-specific——Solaris 也有;其他 UNIX 没有;可移植代码慎用。

      15. dlmopen(LM_ID_NEWLM, …​)——把库装入独立 link-map namespace,避免符号污染;很少用。

      16. 跨章衔接:第 27 章 exec;第 41 章共享库基础;第 38 章 set-UID 安全(LD_PRELOAD);第 28 章 ELF。

      Asciidoc lint check

      asciidoctor: 无警告。