Linux & UNIX 共享库 二

Linux & UNIX 共享库 二

版权声明:署名-非商业性使用-相同方式共享

@@ Tags: Linux;动态库
@@ Date: 2023-10-14 2126

动态加载共享库

当一个可执行文件开始运行之后,动态链接器会加载程序的动态依赖列表中的所有共享库,但有些时候延迟加载库是比较有用的,如只在需要的时候再加载一个插件。

这项功能由dlopen API实现,使得程序能够在运行时打开一个共享库,根据名字在库中搜索一个函数,然后调用这个函数。

  • void *dlopen(const char *filename, int flags);
    • 函数打开一个共享库,返回一个供后续调用使用的句柄。
    • flags 必须包含RTLD_LAZYRTLD_NOW之一,以及零个或任意个其他标志位的组合。
      • RTLD_LAZY Since glibc 2.1.1
        • 惰性绑定,表示只有在引用符号的代码被执行的时候才解析符号,这只发生在函数引用,针对变量的引用则总是在库加载时立即绑定的。
        • 如果LD_BIND_NOW环境变量存在时,则会忽略该标志位。
      • RTLD_NOW
        • 指定该标志或者LD_BIND_NOW环境变量存在时,共享库中所有未定义的符号都会在函数返回前被解析,如果解析失败则会返回一个错误。
      • RTLD_GLOBAL
        • 此共享对象定义的符号将用于随后加载的共享对象的符号解析
      • RTLD_LOCAL
        • RTLD_GLOBAL互斥,如果两个都指定,那么这个值是默认值。
        • 此共享对象定义的符号将不能用于随后加载的共享对象的符号解析
      • RTLD_NODELETE Since glibc 2.2
        • 表示dlclose()不会卸载共享对象,因此在后来执行dlopen()重新加载时,共享对象的静态对象与全局变量不会被重新初始化。
      • RTLD_NOLOAD Since glibc 2.2
        • 不要加载共享对象。这可以用来测试对象是否已经加载(若未加载,dlopen()返回NULL; 如果已经加载,则返回对象句柄)。
        • 此标志还可用于提升已加载的共享对象上的标志。例如,一个先前用RTLD_LOCAL加载的共享对象可以用RTLD_NOLOAD | RTLD_GLOBAL重新打开。
      • RTLD_DEEPBIND Since glibc 2.3.4
        • 将此共享对象中符号的查找范围置于全局作用域之前。这意味着自包含对象将优先使用自己的符号,而不是已经加载的对象中包含的具有相同名称的全局符号。
  • void *dlsym(void* handle, const char* symbol);
    • 函数在库中搜索一个符号(一个包含函数或变量的字符串)并返回其地址。
    • 特殊情况下符号的值实际上可以为NULL,因此dlsym()返回NULL不一定表示错误,要区分错误需要通过dlerror()检查是否设置了错误信息。
    • handle可以指定两个伪句柄:
      • RTLD_DEFAULT
        从主程序中开始查找符号,接着按序在所有已加载的共享库中查找,包括那些通过使用了RTLD_GLOBAL标记的dlopen()调用动态加载的库,这个标记对应于动态链接器所采用的默认搜索模型。
      • RTLD_NEXT
        在上一次dlsym()查询的基础上,查找下一个匹配的符号,这在定义函数包装器时比较常用。
  • int dlclose(void *handle);
    函数关闭之前由 dlopen()打开的库。
  • char *dlerror(void);
    • 函数返回一个错误消息字符串,在调用上述函数中的某个函数发生错误时可以使用这个函数来获取错误消息。
    • 返回NULL表示没有错误发生。
    • 调用此函数后错误指示器将被重置。
    • 线程安全。
  • int dladdr(const void *addr, Dl_info *info);
    • 返回一个包含地址 addr(通常通过前面的 dlsym()调用获得)的相关信息的结构。

控制共享库定义的符号的可见性

设计良好的共享库应该只公开那些构成其声明的应用程序二进制接口(ABI)的符号(函数和变量),其原因如下。

  • 用户可能会误用不属于提供者作为ABI的接口或者内部接口,导致升级共享库时会带来兼容性问题。
  • 在运行时符号解析阶段,由共享库导出的所有符号可能会优先于其他共享库提供的相关定义。
  • 导出非必需的符号会增加在运行时需加载的动态符号表的大小。

下列技术可以用来控制符号的导出。

  • 在 C 程序中可以使用 static 关键词使得一个符号私有于一个源代码模块,从而使得它无法被其他目标文件绑定。

  • GNU C 编译器 gcc 提供了一个特有的特性声明,它执行与 static 关键词类似的任务。

    void __attribute__ ((visibility("hidden"))) func(void) {
        // Code
    }
    
  • 版本脚本可以用来精确控制符号的可见性以及选择将一个引用绑定到符号的哪个版本。

    gcc -Wl,––version–script,vis.map ...

    cat vis.map
    VER_1 {
        global:     # 将下面的符号设为全局
            vis_f1;
            vis_f2;
        local:
            *;      # 隐藏其他符号
    };
    

    通配符语法 glob(7)

使用链接器脚本创建版本化的符号

符号版本化允许一个共享库提供同一个函数的多个版本,每个程序只会使用在链接共享库时的函数版本(来自 glibc 2.1)。

应用该技术可以对共享库进行不兼容的改动而无需提升库的主要版本号,从极端的角度来讲,符号版本化可以取代传统的共享库主要和次要版本化模型。

下面通过一个简单的例子来展示符号版本化的用途,首先是第一个版本的共享库。

版本1

sv_lib_v1.c

#include <stdio.h>

void xyz(void) { printf("v1 xyz()\n"); }

sv_v1.map

VER_1 { 
    global: xyz; 
    local:  *;    # 隐藏所有符号
};

版本2

sv_lib_v2.c

#include <stdio.h>

__asm__(".symver xyz_old,xyz@VER_1");   // 通过汇编指令绑定版本标签
__asm__(".symver xyz_new,xyz@@VER_2");  // 不是@, 表示xyz()的默认定义

void xyz_old(void) { printf("v1 xyz\n"); }

void xyz_new(void) { printf("v2 xyz\n"); }

void pqr(void) { printf("v2 pqr\n"); }

sv_v2.map

VER_1 { 
    global: xyz; 
    local:  *;    # 隐藏所有符号
};

VER_2 { 
    global: pqr;
} VER_1;          # 继承VER_1

测试程序

sv_prog.c

#include <stdlib.h>

int
main(int argc, char *argv[])
{
    void xyz(void);
    xyz();
    exit(EXIT_SUCCESS);
}

编译 & 测试

# 版本1
$ gcc -g -c -fPIC -Wall sv_lib_v1.c
$ gcc -g -shared -o libsv.so sv_lib_v1.o -Wl,--version-script,sv_v1.map
$ gcc -g -o p1 sv_prog.c libsv.so

$ LD_LIBRARY_PATH=. ./p1
v1 xyz() # 输出

$ objdump -t p1 | grep xyz
0000000000000000  F *UND*  0000000000000000  xyz@@VER_1 # 输出

$ objdump -t libsv.so | grep xyz
0000000000001119 g  F .text  0000000000000017  xyz # 输出

# 版本2
$ gcc -g -c -fPIC -Wall sv_lib_v2.c
$ gcc -g -shared -o libsv.so sv_lib_v2.o -Wl,--version-script,sv_v2.map
$ gcc -g -o p2 sv_prog.c libsv.so

$ LD_LIBRARY_PATH=. ./p2
v2 xyz # 输出
$ LD_LIBRARY_PATH=. ./p1
v1 xyz # 输出

$ objdump -t p1 | grep xyz
0000000000000000  F *UND*  0000000000000000  xyz@@VER_1 # 输出

$ objdump -t p2 | grep xyz
0000000000000000  F *UND*  0000000000000000  xyz@@VER_2 # 输出

$ objdump -t libsv.so | grep xyz
0000000000001130 l  F .text  0000000000000017  xyz_new # 输出
0000000000001119 l  F .text  0000000000000017  xyz_old
0000000000001130 g  F .text  0000000000000017  xyz@@VER_2
0000000000001119 g  F .text  0000000000000017  xyz@VER_1

使用初始化和终止函数在加载和卸载库时自动地执行代码

可以定义一个或多个在共享库被加载和卸载时自动执行的函数,这样在使用共享库时就能够完成一些初始化和终止工作了。不管库是自动被加载还是使用 dlopen 接口显式加载的,初始化函数和终止函数都会被执行。

void __attribute__ ((constructor)) some_name_load(void) {
    // 初始化代码
}

void __attribute__ ((destructor)) some_name_unload(void) {
    // 反初始化代码
}

_init()和_fini()函数

用来完成共享库的初始化和终止工作的一项较早的技术是在库中创建两个函数_init()_fini()。当库首次被进程加载时会执行 void _init(void)中的代码,当库被卸载时会执行 void _fini(void)函数中的代码。

如果创建了_init()_fini()函数,那么在构建共享库时必须要指定 gcc –nostartfiles 选项以防止链接器加入这些函数的默认实现。(如果需要的话可以使用–Wl,–init–Wl,–fini 链接器选项来指定函数的名称。)

有了 gcc 的 constructordestructor 特性之后已经不建议使用_init()_fini()函数了,因为gcc 的 constructordestructor 特性允许定义多个初始化和终止函数。

共享库预加载

通过一组共享库的名称(以空格或冒号分隔)定义一个环境变量LD_PRELOAD,这将使可执行文件优先加载这些共享库,因此可执行文件自动会使用这些库中定义的函数,从而覆盖那些动态链接器在其他情况下会搜索匹配的同名函数,达到劫持或拦截的目的。

LD_PRELOAD 环境变量控制着进程级别的预加载行为。或者可以使用/etc/ld.so.preload文件来在系统层面完成同样的任务,该文件列出了以空格分隔的库列表。(LD_PRELOAD 指定的库将在加载/etc/ld.so.preload 指定的库之前加载。)

出于安全原因,set-user-IDset-group-ID 程序忽略了 LD_PRELOAD

使用 LD_DEBUG 来监控动态链接器的操作

有些时候需要监控动态链接器的操作以弄清楚它在搜索哪些库,这可以通过 LD_DEBUG环境变量来完成。通过将这个变量设置为一个(或多个)标准关键词可以从动态链接器中得到各种跟踪信息。

出于安全的原因,在 set-user-ID 和 set-setgroup-ID 程序中将会忽略 LD_DEBUG(自 glibc 2.2.5 起)。

查看LD_DEBUG的帮助信息

LD_DEBUG=help date
Valid options for the LD_DEBUG environment variable are:

  libs        display library search paths
  reloc       display relocation processing
  files       display progress for input file
  symbols     display symbol table processing
  bindings    display information about symbol binding
  versions    display version dependencies
  scopes      display scope information
  all         all previous options combined
  statistics  display relocation statistics
  unused      determined unused DSOs
  help        display this help message and exit

查看库搜索路径(每一行的数字前缀是进程id)

$ LD_DEBUG=libs date
   1206266:     find library=libc.so.6 [0]; searching
   1206266:      search cache=/etc/ld.so.cache
   1206266:       trying file=/lib/x86_64-linux-gnu/libc.so.6
   1206266:
   1206266:
   1206266:     calling init: /lib/x86_64-linux-gnu/libc.so.6
   1206266:
   1206266:
   1206266:     initialize program: date
   1206266:
   1206266:
   1206266:     transferring control: date
   1206266:
Sat 14 Oct 2023 09:16:59 PM HKT