第42章 共享库高级特性

学习内容:

  1. 动态的加载共享库
  2. 控制共享库定义符号的可见性
  3. 使用连接器脚本创建版本化的符号;
  4. 使用初始化和终止函数在加载和卸载库时自动的执行地代码
  5. 共享库预加载
  6. 使用LD_DEBUG来监控动态链接器的操作

42.1 动态加载库

当一个可执行文件开始运行之后,动态链接器会加载程序的动态依赖列表中所有的共享库,但是有时候延时加载库是比较有用的,如只在需要的时候再加载一个插件。动态链接器的这项功能是通过一组API来实现的,这组API是通常被称为dlopen API,他源自 Solaris,现在其中大部分都在SUSv3中进行了规定。

dlopen API 使得程序能够完全在运行时打开一个共享库,根据名字在库中搜索一个函数,然后调用这个函数,在运行时采用这种方式加载的共享库通常被称为动态加载的库,他的创建方式与其他库的创建方式完全一样。

核心dlopen API由下列函数构成

  • dlopen()函数打开一个共享库,返回一个供后续调用的句柄
  • dlsysm()函数在库中搜索一个符号(一个包含函数或变量的字符串)并返回其地址
  • dlclose 关闭之前由dlopen打开的库
  • dlerror 函数返回一个错误消息字符串,在调用上述函数中的某个函数发生错误时可以使用这个函数来获取错误信息

要在Linux 使用dlopen AP构建程序必须要指定-ldl选项以便与libdl库链接起来。

42.1.1 打开共享库:dlopen()

dlopen() 函数将名为libfilename的共享库加载进调用进程的虚拟地址空间并增加该库的打开引用计数

#include <dlfcn.h>
void *dlopen(const char * libfilename,int flags);
                Returns library handle on success,or NULL on error

如果libname 包含了一个斜线(/),那么dlopen()会将其解释成一个绝对或者相对路径,否则动态链接器会使用41.11节中介绍的规则来搜索共享库。

dlopen(0在成功时会返回一个句柄,在后续对 dlopen API中的函数的调用可以使用该句柄来引用这个库。如果发生了错误(如无法找到共享库),那么dlopen会返回NULL.

如果libfilename指定的共享库依赖其他共享库,那么dlopen会自动加载那些库,如果有必要的话这一过程会递归进行,这种被加载进来的库被称为这个库的依赖树。

在同一个库文件中可以多次调用dlopen(),但将库加载进内存的操作只会发生一次(第一次调用),所有的调用都返回同样的句柄值。但dlopen API 会为每个库句柄维护一个引用计数,每次调用dlopen都会增加引用计数,每次调用dlclose() 都会减少引用计数,只有当计数为0时dlclose()才会从内存中删除这个库

flags参数是一个位掩码,他的取值是RTLD_LAZY和RTLD_NOW中的一个这两个值含义分别如下。

  • RTLD_LAZY

只有当代码被执行的时候才解析库中未定义的函数符号。如果需要某个特定符号的代码没有被执行到,那么永远不会解析该符号。延迟解析只适用于函数引用,对变量的引用会被立即解析。指定RTLD_LAZY标记能够提供在与加载可执行文件的动态依赖列表中的共享动态库时动态链接器的常规操作对应的行为。

  • RTLD_NOW

在dlopen结束之前立即加载库中所有的未定义符号,不管是否需要用到这些符号,这种做法时打开库变得更慢了,但能够立即检测到任何潜在的未定义符号错误,而不是在后面某个时刻才检测到这种错误。在调试应用程序时这种做法是比较有用的,因为它能够确保应用程序在碰到未解析符号时立即发生错误,而不是执行了很长一段时间之后才发生错误。

通过将环境变量LD_BIND_NOW设置为一个非空字符串能够轻质动态链接器加载可执行文件的动态依赖列表中的共享库时立即解析所有符号(即类似于RTLD_NOW)。这个环境变量在gkibc2.2.1以及后续的版本中是有效的,设置LD_BIND_NOW会覆盖dlopen()RTLD_LAZY标

记的效果

flags也可以取其他的值,SUSv3规定了下列几种标记。

RTLD_GLOBAL

   这个库记忆来树中的符号在解析由这个进程加载的其他库中的引用和dlsym()查找时可用

RTLD_LOCAL

与RTLD_GLOBAL相反,如果不指定任何常量,那么就去这个默认值。它规定在解析后续加载的库中的引用时这个库及其依赖树中的符号不可用,

在不指定RTLD_GLOBAL和RTLD_LOCAL时,SUSv3并没有规定一个默认值。大多数UNIX事项与linux一样,将RTLD_LOCAL作为默认值,但一些实现将RTLD_GLOBAL作为默认值

linux还支持几个并没与在SUSv3规定的标记,如下所示

RTLD_NODELETE(自glibc2.2起)

在dlclose调用中不要卸载库,即使引用计数已经变成0了。这意味着在后面重新通过dlopen()加载库时不会重新初始化库中的静态变量。(对于动态链接器自动加载的库来讲,在创建库时通过指定gcc -Wl,-znodelete选项能够取得类似的效果)

RTLD_NOLOAD(自glibc2.2起)

不加载库。这个标记有两个目的。第一可以使用这个标记来检查某个特定的库是否已经被加载到了进程的地址空间中。如果已经加载了,那么dlopen()会返回库的句柄,如果没有加载,那么dlopen()就会返回NULL;第二可以使用这个标记来“提升”已加载的库的标记如在对之前使用RTLD_LOCAL打开的库调用dlopen()时可以在flags参数中指定RTLD_NOLOAD|RTLD_GLOBAL.

RTLD_DEEPBIND(自glibc2.3.4起)

在解析这个库的符号引用时线搜索库中的定义,然后再搜索已加载库中的定义,这个标记使得一个库能够实现自由包含,即优先使用自己的符号定义,而不是在已加载的其他库中定义的同名全局符号。(这与在41.12节中介绍的-Bsymbolic 连接器选项具有类似的效果。)

RTLD_NODELETE和RTLD_NOLOAD标记在Solaris  dlopen API中也进行了实现,但提供这两个标记的UNIX实现很少,RTLD_DEEPBIND时linux特有的。

当将libname指定为NULL时dlopen会返回主程序的句柄。(SUSv3将这种句柄成为全局符号对象的句柄)在后续对dlsym()的调用中使用这个句柄会导致首先在主程序中搜索符号,然后在程序启动时加载共享库进行搜索,最后在使用了RTLD_GLOBAL标记的动态加载的库进行搜索。

42.1.2 错误诊断

如果在dlopen()调用或dlopenAPI的其他调用得到了一个错误,那么可以使用dlerror来获取一个指向表明错误原因的字符串的指针。

#include <dlfcn.h>
const char *dlerror(void);
    Returns pointer to error-diagnostic string or NULL
   if no error has occurred since previous call to dlerror()

如果从上次调用dlerror()到现在没有发生错误,那么dlerror()函数返回NULL,读者在下一节中就会看到这这种处理方式的好处了。

42.1.3 获取符号的地址:dlsym()

dlsym()函数在handle指向的库以及该库的依赖树中的库名中搜索名为symbol的符号(函数或者变量)。

#include <dlfcn.h>
void *dlsym(void *handle,char *symbol);
    Returns address of symbol,or NULL if symbol is not found

如果找到了symbol,那么dlsym()会返回其地址,否则就返回NULL.handle参数通常是上一个dlopen调用返回的库句柄,或者它可以是下面所介绍的其中一个所谓的伪句柄。

dlopen()返回的符号值可能回事NULL,这一点与“找不到符号”的返回是无法区分的。为了弄清楚具体时哪一种情况就必须要先调用dlerror()(确保之前的错误字符串已经被清除了),如果调用dlsym()之后dlerror返回了一个非NULL值,那么就可以得出发生错误的结论了。

   如果symbol是一个变量的名称,那么可以将dlsym()的返回值复赋给一个合适的指针类型,并通过反引用该指针来得到变量的值。

int *ip;
ip = (int *)dlsym(symbol,"myval");
if(ip != NULL)
    printf("Value is *d\n",*ip);

如果symbol是一个函数的名称,那么可以使用dlsym()返回的指针来调用该函数。可以将dlsym()返回的值存储到一个合适的指针中如下所示

int ( *funcp)(int); 
//Pionter to a function taking an integer argument and returing an integer

但是不能简单的将dlsym()的结果复制给此类指针,如下面的例子所示。

funcp = dlsym(handlem,symbol);

其原因是C99标准禁止函数指针和void *之间的赋值操作。这个问题的解决方案是使用下面这样的类型转换。

*(void **)(&funcp) = dlsym(handle,symbol);

通过dlsym()得到了指向函数指针之后就能通过常规的C语法反向引用函数指针来调用这个函数了。

res =  (*funcp)(somearg);

读者将在dlsym()的返回值进行赋值时可能会使用下面这段指针看起来与上述代码等价的代码来取代上面的*(void**)语法。

(void*)funcp = dlsym(handle,symbol);
/*但是 gcc -pedantic在碰到上面这段代码时会发出 “ANSI C forbids the use of cast
 expressions as values”的警告信息。而是用*(void **)语言就不会出现这个警告信息,
因为是在向赋值语句的左值指向的地址赋值*/

在很多UNI实现中可以使用下面这样的类型转换来消除C编译器的警告。

funcp = (int)(*)(int)dlsym(handle,symbol);

  在dlysm()中使用库伪句柄

  dlsym()函数中的handle除了能够取自由dlopen()调用返回的句柄值之外,还可以取下面的伪句柄值.

RTLD_DEFAULT

 从主程序中开始查找symbol,接着按序所在所有已加载的共享库中查找,包括那些通过使用了RTLD_GLOBAL标记的dlopen()调用动态加载的库,这个表姐对应于动态加载器所采用的默认搜索程序。

RTLD_NEXT

  在调用dlsym()之后加载的共享库中搜索symbol,这个标记适用于需要创建与在其他地方定义的函数同名的包装函数的情况。如在主程序中可能会定义一个malloc(它可能完成内存分配的薄记工作),而这个函数在调用实际的malloc之前会首先调用通过func=dlsym(RTLD_NEXT,"malloc")来获取其地址。

SUSv3 并没有要求实现上述列出的伪句柄(甚至没有保留这两个值以供后续之用),并且在所有UNIX实现也没有定义上述伪句柄。为了从<dlfcn.h>中获取这些常量的定义必须要定义—_GNU_SOURCE特性测试宏。

示例程序

  程序清单42-1演示了dlopen API的使用。这个程序接收两个命令行参数:需加载的共享库名称和需执行的库中函数的名称。下面的例子演示了这个程序的使用

$ ./dynload ./libdemo.so.1 x1
 Called mod1-x1
$ LD_LIBRARY_PATH=. ./dynload libdemo.so.1 x1
Called mod-x1

在上述第一个命令中,dlopen()注意到库路径包含了一个斜线,因此将其解释称一个相对路径名(表示唯一当前工作目录的库)。在第二个命令中指定了库搜索路径LD_LIBRARY_PATH,动态链接器会根据正常的规则来解释这个库搜索路径(同样表示在当前工作目录中查找库)。

程序清单42-1:使用dlopen API


#include <dlfcn.h>
int main(int argc,char *argv[])
{
    void *libHandle;   //Handle for shared library
    void (*funcp)(void)  //pointer to function with no argument
    const char *err;
    if(argc !=3 || strcmp(argv[1],"--help")==0)
    {
        printf("%s lib-path func-name\n",argv[0]);
    }
    /*load the shared library and get a handle for later use*/
    libHandle = dlopen(argv[1],RTLD_LAZY);
    if(libHandle ==NULL)
    {
        printf("dlopen:%s",dlerror());
    }
    /*serch library for symbol named in argv[2]*/
    (void)dlerror();
    *(void **)(&funcp)=dlsym(libHandle,argv[2]);
    err = dlerror();
    if(err !=NULL)
    {
        printf("dlsym:%s",err);
    }
    /*if the address returned by dlsym() is non NULL,try calling it as a function that
     takes no arguments*/
     if(funcp ==NULL)
     {
        printf("%s is NULL\n",argv[2]);
     }else{
        (*funcp)();
     }
     dlclose(libHandle);  /*Close the library*/
     exit(0);
}

42.1.4 关闭共享库

dlclose()函数关闭一个共享库

#include <dlfcn.h>
int dlclose(void *handle);
                        Returns 0 on success,or -1 on error

    dlclose()函数会减小handle所引用的库打开引用的系统计数。如果这个引用计数变成了0并且其他库已经不需要用到该库中的符号了,那么就会卸载这个库。系统也会在这个库的依赖树中的库执行(递归的)同样的过程。当进程终止时会隐式地对所有库执行dlclose()。

42.1.5 获取与加载地符号相关地信息:dladdr()

dladdr()返回一个包含地址addr(通常通过前面地dlsym()调用获得)地相关信息地结构

#define _GNU_SOURCE
#include <dlfcn.h>
int dladdr(const void *addr,Dl_info *info);
    Returns nozero value if addr was found in a shared library,otherwise 0

info 参数指向一个由调用者分配地结构地指针,其结构形式如下。
 

typedef struct{
    const char *dli_name; //pathname of shared library containg 'addr'
    void       *dli_fbase; //Base address at which shared library is loaded
    const char *dli_sname; //Name of nearest run-time symbol with an address <= 'addr'
    void        *dli_saddr //Actual value of the symbol returned in 'dli_sname'
}

Dl_info 结构中地前两个字节指定了包含地址addr地共享库地路径名和运行时基地址。最后两个字段返回地址相关地信息。假设addr指向共享库中地一个符号地确切地址,那么dli_saddr返回地值与传入地addr一样。

42.1.6 在主程序中访问符号

  假设使用dlopen()动态加载了一个共享库,然后使用dlsym()获取了共享库中x()函数地地址,紧接着调用x(),如果在x()调用了函数y().那么通常会在程序加载地其中一个共享库中搜索y().

有时候需要让x()调用主程序中地y()实现(类似于回调机制)。为了达到这个目的就必须要使用主程序中地符号(全局作用域)对动态链接器可用,即在连接程序时使用 --export-dynamic 连接器选项。

$ gcc -Wl,--export-dynamic main.c
#或者编写下面这个等价地命令
$ gcc -export-dynamic main.c
#使用这些选项中地一个就能够允许动态加载地库访问主程序中地全局符号。

#gcc -rdynamic 选项和gcc -Wl,-E选项地含义,以及-Wl、--export-dynamic 时一样的。

42.2 控制符号的可见性

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

  • 如果共享库的设计人员不小心导出了未详细说明的接口,那么使用这个库的应用程序的作者可能会选择使用这些接口。这样在将来升级共享库时可能会带来兼容性问题。库的开发人员认为可以修改或删除哪些不属于文档中的ABI接口,而库的用户则希望继续使用名称与他们当前正在使用的接口名称是一样的的接口(同时语义保持不变)
  • 在运行时符号解析阶段,由共享库导出的所有符号可能会有限于其他共享库提供的相关定义
  • 导出非必需的符号会增加在运行时需加载的动态符号表的大小

当库的设计人员确保只导出那些库的声明的ABI所需的动态符号就能使上述问题发生的可能性降到最低或避免上述问题的发生。下列技术可以用来控制符号的导出。

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

除了使一个符号私有于源代码模块之外,static关键词还能达到一个相反的效果。如果一个符号被标记为static,那么在统一源文件中对该符号的所有引用会被绑定到该符号的定义上,其结果是这些引用在运行时不会被关联到其他共享库的相应定义上

  • GNU C编译器gcc提供了一个特有的特性声明,他执行于static关键词类似的任务。
void _addribute_((visibility("hidden")))
func(void)
{
    /*Code*/
}

static 关键字将一个符号的可见性限制在单个源码文件中,而hidden特性使得一个符号对构成共享库的所有源代码文件都可见,但对库外的文件不可见。

与static 关键词一样,hidden特性也能达到一个相反的效果,即防止在运行时发生符号插入。

  • 版本脚本(参见42.3节)可以用来精准控制符号的可见性以及选择将一个引用绑定到符号的哪个版本。
  • 当动态加载一个共享库时(参见42.1.1节),dlopen()接收的RTLD_GLOBAL标记可以yonglaizhiding-这个库中定义的符号应该用于后续加载的库中的绑定操作,--export-dynamic连接器选项(参见42.1.6节)可以用来使主程序的全局符号对动态加载的库的使用。

42.3 连接器版本脚本

  版本脚本是一个包含连接器ld执行的指令的文本文件。要使用版本脚本必须要指定--version-script连接器选项。

$ gcc -Wl, --version-script,myscriptfile.map ...

版本脚本的后缀通常(但不统一)是.map

下面几节将介绍版本脚本的几个用途。

42.3.1 使用版本脚本控制符号的可见性

  版本脚本的一个用途是控制那些可能会在无意中变成全局可见(即对与该库进行链接的应用程序可见)的符号的可见性。举一个简单的例子,假设需要从三个源文件 vis_comm.c vis_fl.c以及vis_f2.c中构建一个共享库,这三个源文件分别定义了函数 vis_comm()、vis_fl()、以及vis_f2()。VIS_comm函数由vis_f1()和vis_f2调用,但不想被与该库进行链接的应用程序直接使用。再假设使用常规的方式来构建共享库。 

$gcc -g -c -fPIC -Wall vis_comm.c vis_f1.c vis_f2.c
$gcc -g shared -o vis.so vis_comm.o vis_f1.o vis_f2.o

如果使用下面的readelf命令来列出该库到处动态符号,那么就会看到下面的输出。

$ readelf --syms --use-dynamic vis.so | frep vis_
  30 12:00000790   59 FUNC GLOBAL DEFAULT 10 vis_f1
  25 13:000007d0   73 FUNC GLOBAL DEFAULT 10 vis_f2
  27 16:00000770   20 FUNC GLOBAL DEFAULT 10 vis_comm

  这个共享库导出了三个符号:vis_comm()、vis_f1()以及vis_f2,但是这里需要确保这个库只导出vis_f1()和vis_f2符号,这种效果可以通过下面的版本脚本来实现。

 

$ cat vis.map
 VER_1{
    global:
          vis_f1;
          vis_f2;
    local:
          *;
};

  标识符VER_1是一种标签,在42.3.2节对符号版本化的讨论中将会看到一个版本脚本可以包含多个版本节点,每个版本节点以括号({})组织起来并且在括号前面设置一个唯一的版本标签。如果使用版本脚本只是为了控制符号的可见性,那么版本标签是多余的,但老版本的lld仍然需要用到这个标签,ld的现代版本允许省略版本标签,如果省略了版本标签的话就认为版本节点拥有一个匿名版本标签并且在这个脚本中不能存在其他版本节点。

  在版本节点中,关键词global 标记除了以分号分隔的对库之外的程序可见的符号列表的起始位置,关键词global标记出了以分号分隔的对库之外的程序隐藏的符号列表的起始位置。上面的星号(*)说明在符号规范中可以使用掩码模式,所使用的掩码字符与shell文件名匹配中使用的掩码字符是一样的---如*和?。在本例中local规范中的星号表示除了zaiglobal段中显示声明的符号之外的所有符号都对外隐藏。如果不这样声明,那么vis_comm()仍然是可见的。因为在默认情况下C全局符号对共享库之外的程序是可见的

  接着可以像下面这样使用版本脚本来构建共享库。

$ gcc -g -c -fPIC -Wall vis_comm.c vis_f1.c vis_f2.c
$ gcc -g -shared -o -vis.so vis_comm.o vis_f1.o vis_f2.o \
        -W1,--version-script,vis.map

再次使用redelf可以看出vis_comm()不再对外可见了。

$ readelf --syms --use-dynamic vis.so |grep vis_
  25  0: 00000730     73   FUNC  GLOBAL DEFAULT 11 vis_f2
  29  16:00000690     59   FUNC  GLOBAL DEFAULT 11 vis_f1

42.3.2 符号版本化

  符号版本化允许一个共享库提供同一个函数的多个版本。每个程序会使用它与共享库进行(静态)链接时函数的当前版本。这种处理方式的结果是可以对共享库进行不兼容的改动而无需提升库的主要版本号。从极端角度来说,符号版本化可以取代传统的共享库主要和西药版本化模型,glibc从2.1开始使用了这种符号版本化技术,因此glibc2.0以及之前所有版本都是通过单个主要版本库(libc.so.6)来支持的。

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

 

$ cat sv_lib_v1.c
#include <stdio.h>

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

$cat sv_v1.map
    VER_1{
            global:xyz;
            local:*;    #hide all other symbpls
};

$gcc -g -c -fPIC -Wall sv_lib_v1.c
$gcc -g shared -o libsv.so sv_lib_v1.o -Wl, --version-scri[t,sv_v1.map

   在这个阶段,版本脚本sv_v1.map只用来控制共享库的符号的可见性,即只导出xyz(),

同时隐藏其它所有符号(在这个简单的例子中也没有其它符号了)。接着创建一个程序pl来使用这个库。

$ cat sv_prog.c
 #include <stdlib.h>
  void xyz(void);    
int main(int argc,char *argv[])
{
  xyz();
  exit(0);
}
$gcc -g -o p1 sv_prog.c libsv.so

//运行程序之后就能看到预期的效果
$ LD_LIBRARY_PATH=. ./p1
v1 xyz

现在假设需要修改库中xyz()的定义,但同时仍然需要确保程序pl继续使用老版本的函数。为完成这个任务,必须要在库中定义两个版本的xyz()。

$ cat sv_lib_v2.c
#include <stdlib.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");}
void pqr(void){printf("v2 pqr\n");}

  这里两个版本的xyz()是通过函数xyz_old()和xyz_new()来实现的,xyz_old() 函数对应于原来函数xyz()定义,pl程序应该继续使用这个函数。xyz_new()函数提供了与库的新版本进行连接的程序所使用的xyz()的定义。

  修改过的版本脚本中的两个.symver汇编指令将这两个函数绑定到了两个不同的版本标签上,下面将使用这个脚本来创建共享库的新版本。第一个指令指示与斑斑标签VER_1进行链接的应用程序(即程序pl)所使用的xyz()的实现时xyz_old(),与版本标签VER_2进行链接的应用程序所使用的xyz()的实现时xyz_new().

  第二个.symver指令使用@@(不是@)来指示当前应用程序与这个共享库进行静态连接应用时应该使用的xyz()的默认定义。一个符号的.symver指令中应只有一个指令使用@@标记。

  下面时与修改过之后的库对应的版本脚本

$ cat sv_v2.map
 VER1{
        global: xyz;
        local:  *;   #hide all other symbols
};

VER_2{
        global:pqr;
}VER_1;

  这个版本脚本提供了一个新版本标签VER_2,它依赖于标签VER_1。这种依赖关系是通过下面这行进行标记的

 }VER_1;

        版本标记依赖表明了相邻两个库版本的之间的关系。从语义上来经,Linux上的版本标签依赖的唯一效果是版本节点可以从他所属的版本节点中继承global和local规范。 

        依赖可以串联起来,这样就可以定义另一个依赖于VER_2的版本节点VER_3并以此类推地定义其他版本节点。

        版本标签名本身是没有任何意义的,他们相互之间的关系是通过指定的版本依赖确定的,因此这里选择名称VER_1和VER_2仅仅是为了按时他们之间的关系。为了便于维护,建议在版本签名中包含包名和一个版本号。如glibc会使用名为GLIBC_2.0和GLIBC_2.1之类的版本标签名。

        VER_2 版本标签还指定了将库中的pqr()函数到处并绑定到VER_2版本标签。如果没有通过这种方式来声明pqr(),那么VER_2版本标签从VER_1版本标签继承而来的local规范将会使pqr()对外不可见。还需要注意的是如果省略了local规范,那么库中的xyz_old()和xyz_new()符号也会被导出(这通常时不期望发生的事情)。

        现在按照以往方式构建库的新版本。

$gcc -g -fPIC -Wall sv_lib_v2.c
$gcc -g -shared -o libsv.so sv_lib_v2.o -Wl, --version-script,sv_v2.map

//现在创建一个新程序p2,它使用了xyz()新定义,同时程序p1使用了旧版的xyz().

$ gcc -g -o p2 sv_prog.c libsv.so
$ LD_LIBRARY_PATH=. ./p2          //uses xyz@VER_2

v2 xyz

$LD_LIBRARY_PATH=. ./P1          //uses xyz@VER_1
v1 xyz

        可执行文件的版本标签依赖是在静态链接时进行记录的。使用objdump -t可以打印出每个可执行文件的符号表,从而显示出两个程序中不同的版本标签依赖。

$ objdump -t p1 | grep xyz
08048380        F *UND*        0000002e    xyz@@VER_1

$ objdump -t p2 | grep xyz
080483a0        F *UND*        0000002e    xyz@@VER_2

42.4 初始化和终止函数

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

        初始化函数和终止函数是使用gcc的constructor和destructor特性来定义的。在库被加载时需要执行的所有函数都应该定影成下面的形式。

void __attribute__((constructor))some_name_load(void)
{
    /*Initialization code*/
}
//类似的,卸载函数的形式如下
void __attribute__((destructor))some_name_unload(void)
{
    /*Finalization code*/
}

//读者可以根据需要使用其他名字替换函数名some_name_load()和some_name_unload()
使用gcc的constructor和destructor特性还能创建主程序的初始化函数和终止函数。

  _init()和_fini()函数 

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

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

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

42.5 预加载共享库

        处于测试的目的,有些时候可以有选择地覆盖一些正常情况下会被动态连接器按照41.11节中介绍的规则找出的函数(以及其他符号)。要完成这个任务可以定义一个环境变量LD_PRELOAD,其值由在加载其他共享库之前虚假在的共享库的名称构成,其中共享库之间用空格或者冒号分割。由于首先会加载这些共享库,因此可执行文件会自动的使用这些苦中定义的函数,从而覆盖那些动态链接器在其他情况下会搜索的同名函数。如假设有一个程序调用了函数x1()和x2(),并且这两个函数在libdemo库中进行了定义,这样当运行这个程序时会看到下面的输出。

$ ./prog
 called mod1-x1 DEMO
 called mod2-x2 DEMO

 (在本例中假设共享库位于其中一个标准目录中,因此无需使用LD_LIBRARY_PATH环境变量。)

接着需要覆盖函数x1(),这可以通过创建另一个包含了不同的x1()定义的共享库libalt.so来完成。在运行这个程序时预加载这个库会得到下面的输出。

$ LD_PRELOAD=libalt.so ./prog
 called mod1-x1 ALT
 called mod2-x2 DEMO

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

        处于安全考虑,set-user-ID和set-group-ID程序忽略了LD_PRELOAD。

42.6 监控动态连接器:LD_DEBUG

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

        如果将help付给LD_DEBUG,n那么动态链接器将会输出有关LD_DEBUG的帮助信息,而指定的命令不会被执行。

$ LD_DEBUG=help dateValid  
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
all              all    previous  options  combined 
statistics       display  relocation  statistics
unused          determine  unused  DSOs
help             display  this  help  message  and  exit

 当请求与跟踪库搜索相关的信息时会产生很多输出,下面的例子对输出进行了删减

$  LD_DEBUG=libs   date

10687:           find  library=librt.so.1  [o];    searching
10687:           search  cache=/etc/ld.so.cache
10687:           trying  file=/lib/librt.so.1
10687:           find  library=libc. so.6  [ o];    searching 
10687:           search  cache=/etc/ld.so.cache
10687:           trying  file=/lib/libc.so.6
10687:           find  library=libpthread.so.o  [ o];    searching
10687:           search   cache=/etc/ld.so.cache
10687:           trying  file=/lib/libpthread.so.o 
10687:           calling  init:  /lib/libpthread.so.o 
10687:           calling  init:  /lib/libc.so.6 
10687:           calling  init:  /lib/librt. so.1 
10687:           initialize  program:  date
10687:           transferring  control:  date Tue  Dec  28  17:26:56  CEST  2010
10687:           calling  fini:  date  [o]
10687:           calling  fini:  /lib/librt.so.1  [o] 
10687:           calling  fini:  /lib/libpthread.so.o  [ o] 
10687:           calling  fini:  /lib/libc.so.6  [o]

        没一行开头的10687是指所跟踪的进程的进程ID,当监控多个进程(如父进程和子进程)时会用到这个值

        在默认情况下,LD_DEBUG的输出会被写到标准错误上,当可以将一个路径名赋给环境变量LD_DEBUG_OUTPUT来输出重定向到其他地方。

        如果需要的话可以给LD_DEBUG赋多个选项,各个选项之间使用逗号分割(不能出现空格)。symbols选项(跟踪动态连接器的符号解析)的输出特别多。

        LD_DEBUG对于由动态连接器隐式加载的库和使用使用dlopen()动态加载的库都有效。

        出于安全的原因,在set-user-ID和set-group-ID程序会忽略LD_DEBUG

42.7 总结

        动态链接器提供了dlopen API ,它允许程序在运行时显示地加载其他共享库,这样程序就能够实现插件功能了。

        共享库设计的一个重要方面时控制符号的可见性,这样库就能够只导出那些与该库进行连接的程序需要用到的符号了。本章介绍了几项用来控制符号可见性的技术。在这些技术中,版本脚本对符号可见性控制的粒度最细。

        本章还介绍了如何使用版本来实现一个共享库导出同一符号的多个定义以供该库进行链接的不同应用程序使用的模型。(各个应用程序使用它与库进行链接时符号的当前定义),这些名称为传统的在共享库真实名称中使用主要版本和次要版本号来继续版本话管理的方式提供了一个替代方案。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值