中华鲟网箱养殖:Linux操作系统中库的版本控制机制

来源:百度文库 编辑:偶看新闻 时间:2024/04/30 09:44:15

1,库的名称

 

   Linux下的库文件文件名主要如下:

 

   lib*.so.x.y.z,其中x是主版本号,y是次版本号,z是发布版本号。

 

   x不同的库是不能互相兼容的。依赖于旧的库版本的程序,如果使用新的版本号的库,需要进行相应修改并且重新编译链接。

   y不同的库,主要是表示进行了增量升级,修改包括新增符号,并且保持原有符号不变。高的次版本号,向后兼容主版本号相同,但是次版本号较低的库文件。

 

   发布版本,主要是进行bug的修正和性能的改进等。由于不添加新的接口,因此主版本、次版本号相同但是发布版本不同的库完全兼容。

 

 

   有些linux下的库不遵守这种规则,例如C语言库的glibc的基本c语言库:libc-x.y.z.so,glibc中的动态链接器:ld-x.y.z.so;其中的数学库libm和运行时装载库libdl也是如此。

 

 2,SO-NAME

 

    SO-NAME是库的主版本号的标识。比如 lib*.so.x.y.z的SO-NAME是lib*.so.x。

 

    当然,C语言库的SO-NAME与一般的也不大相同。比如C语言基本库libc-2.6.1.so的SO-NAME是libc.so.6,动态链接器ld-2.6.1.so的SO-NAME则是ld-linux.so。

 

    Linux系统的库管理器,往往在每个库文件所在目录下,为库创建一个SO-NAME命名的软链接文件。比如存在库/lib/lib*.so.x.y.z,那么在/lib下会产生一个/lib/lib*.x的软链接,指向/lib/lib*.x.y.z。这样如果系统中/lib/lib*.x.y.z的y和z发生变化,SO-NAME指向的库会进行更新,指向最新版本。共享库的主版本不同,则会产生多个SO-NAME的软链接。

 

    ldconfig用于更新软链接,其遍历所有默认共享库目录,比如/lib,/usr/lib,以新建(如果没有)或者更新所有软链接,使其指向新的库。

 

 3,编译链接和符号机制

 

    默认情况下,编译器自动使用最新版本的库文件进行编译链接。

 

   Linux操作系统编译的可执行文件中,会记录依赖的库的SO-NAME。这样如果SO-NAME所标识的库不存在的时候会产生错误(无法找到库文件)。如果SO-NAME标识的库文件存在,会存在如下的问题: 编译的时候,使用的是高次版本的SO-NAME,运行时则只有低次版本的库文件存在。这样如果加载了低次版本的库文件,则有可能产生符号不存在(因为高版本进行了增量拓展)的链接错误。

 

    在早期操作系统处理方式是记录次版本号到编译出的可执行程序,在库装载(可执行文件运行)时查找的当前系统中的次版本号如果有高于记录在可执行文件中链接的时候的次版本号的库文件,则允许运行,否则进行警告或者压根不允许运行。

 

    但是问题是,也许尽管我们在编译的时候使用的是次版本号高的库文件,但是我们其实并不需要那些只有在高次版本库中存在的符号。也就是链接时候的是高次版本库,而真正程序中使用的则是低次版本库中有的东西。

 

    上述这种程序在低次版本情况下运行起来是没问题的,按照上述方法则会不允许其运行。

 

    为了解决上述问题,Linux采用了基于符号的版本机制。对于符号,程序员给它们指定版本标识。例如函数符号 add的版本标识是VERS_1.1,称为属于VERS_1.1集合。而对于符号del则属于VERS_1.2集合,并且add也属于VERS_1.2如下:

 

   VERS_1.1{

  add

  }

 

  VERS_1.2{

   del

  }VERS_1.1  (这个表示继承了或者说包含了VERS_1.1)。

 

  链接器使用这种集合关系就可以知道程序依赖的符号所属的集合,也就是至少的程序次版本。程序中记录的就不是编译使用的库次版本号,而是真正需要的库的次版本号了。

 

  这样,如果真正需要的程序次版本满足,则可允许其运行。

 

  那么程序运行时,链接器通过程序内记录的依赖的所有库的至少需要的符号次版本信息,查看是否满足运行需求。

 

 

  Linux正是使用SO-NAME和基于符号集合的版本信息来实现的这种版本控制。

 

 

4,使用上述技术的库

 

   Linux的版本控制并没有得到很大范围应用,主要是Glibc软件包的20多个共享库使用了这种技术。并且使用了一些范围机制来屏蔽不希望暴露给库外部的人员使用的,库内部的符号。

 

5,GCC的拓展

 

   GCC对于上述Linux下的机制也有所拓展。除了程序员使用脚本来指定符号的集合,gcc允许使用汇编宏指令指定符号的集合(版本)。该指令为.symver可以直接嵌入到C/C++中。例如:

 

  asm(".symver add ,add@VERS_1.1");

 

  int add(int a,int b){

          return a+b;

  }

 

这样就表明add属于VERS_1.1这个版本集合。

 

另外,允许符号在多个库版本中存在。也就是在链接层面提供了符号重载机制。

 

 asm (".symver old_printf,printf@VERS_1.1");

 asm (".symver new_printf,printf@VERS_1.2");

 

 int old_printf(){

     .......

  }

 int new_printf(){

     .......

  }

 

 

这样,链接器对于使用1.1的printf链接到old_printf,对于1.2的,链接到new_printf。

 

 

6,共享库机制的应用

 

 

  GCC可以使用-Xlinker将--version-script参数的值传递给ld链接器。

 

  例如对于lib.c,使用的符号版本脚本为 lib.ver:

 

  gcc -shared -fPIC lib.c -Xlinker --version-script lib.ver -o lib.so。

 

 

  假设函数foo,脚本里如下定义:

 

  VERS_1.2{

 

   global:

         foo;

   local:

         *;

   };

 

  假设main使用了lib.so:

  链接main:  gcc main.c ./lib.so -o main

 

  则该程序放到只有不包含VERS_1.2的库的系统上运行的话,就会报错:

 

  ./main

 

  ./main: ./lib.so: version 'VERS_1.2' not found (requered by ./main)。

 

 

 

7,SO-NAME和路径记载在库文件/可执行文件的哪里

 

   库文件/可执行文件的.dynamic中记载依赖的文件所在目录。如果路径是相对的,则ld会在/lib和/usr/lib和由/etc/ld.so.conf中配置的目录中查找。

 

   ldconfig程序可以建立软链接,并且使得它指向正确的实际库。并将SO-NAME和路径的对应关系建立缓存并放入/etc/ld.so.cache文件。这样,ld实际从/etc/ld.so.cache文件中查找对应路径。/etc/ld.so.cache是专门设计的,方面查找。

 

   如果/etc/ld.so.cache中没有找到,则遍历/lib和/usr/lib,依然没有找到则报错。

 

   不同系统的/etc/ld.so.cache可能不大一样,比如FreeBSD是/var/run/ld-elf.so.hints,在ldconfig的手册页可以查到。

 

   另外,使用LD_LIBRARY_PATH也可以改变查找路径,用于临时改变一个应用程序的搜索路径。

 

 

   其实可以使用链接器带上选项来启动程序,进行替代:

 

    /lib/ld-linux.so.2 -library-path /home/usr/bin/ls 就会使用搜索/home/usr/bin下的库ls。

 

   这样动态链接器查找库的顺序变成了:

 

   LD_LIBRARY_PATH指定的路径,/etc/ld.so.cache指定路径,/usr/lib最后是/lib。

 

 

   另外,对于GCC编译的时候查找库的路径,LD_LIBRARY_PATH对它的影响相当于-L参数。

 

   另外,LD_PRELOAD指定的目录更具有优先装载权,并且无论程序是否依赖,ld都会装载它们。由于全局符号介入机制使得LD_PRELOAD里指定的库或者目标文件全局符号会覆盖之后加载的同名全局符号,使得我们可以方便的做到改写标准C库函数的某个而不影响其他。对于程序调试或者测试很有用。对应配置文件是/etc/ld.so.preload。

 

 

   还有一个是LD_DEBUG,其打开动态链接器的调试功能。这样动态链接器在运行中打印很多有用信息。对于开发调试共享库很有用。设置LD_DEBUG为 file ,则可以打印装载时候的搜索路径等:

 

   $LD_DEBUG=files ./hello  开启hello程序并使用LD_DEBUG。

 

   其他的诸如:

 

     =bindings 显示符号绑定过程, libs显示查找过程,versions显示符号版本依赖关系,reloc显示重定位过程,symbols显示符号表查找过程,statistics显示各种统计信息,all显示所有。

 

 

8,编译时指定SO-NAME

 

  gcc -shared -fPIC -Wl ,-soname  myname -o libraryname  lib.c

 

 这样由于gcc将-soname传递给链接器,因此,指定库的软链接是 myname。如果不指定,则没有软链接,ldconfig就没有作用了。

 

 

 编译和链接可以分开完成:

 

 假设libfoo1.c和libfoo2.c编译成libfoo.so.1.0.0,并依赖于两个库libbar1.so和libbar2.so:

  gcc -shared -fPIC -Wl , -so-name  libfoo.so.1 -o libfoo.so.1.0.0 libfoo1.c libfoo2.c -lbar1 -lbar2

 

等效于:

 

  gcc  -c -g -Wall -o libfoo1.o libfoo1.c

  gcc  -c -g -Wall -o libfoo2.o libfoo2.c

 

ld -shared -so-name libfoo.so.1 -o libfoo.so.1.0.0 libfoo1.o libfoo2.o -lbar1 -lbar2

 

注意不要使用-fomit-frame-pointer ,其影响调试。

 

使用:

ld -rpath或者gcc -Wl ,-rpath  告知动态链接器装载的时候,首先查找的库路径。

 

-export-dynamic使得链接器在产生可执行文件时,将所有全局符号导出到动态符号表。默认是,只将有引用的符号导出到动态符号表。也就是说,只有被共享模块中引用到的主模块中的符号,才被导出到了全局符号表。当使用dlopen动态加载模块时,该模块反向引用的主模块的符号,就有可能因为在链接时没有共享模块引用而没有放到全局符号表。这将导致错误。链接器选项-export-dynamic可以防止这种错误。gcc -Wl , -export-dynamic传递给链接器。

 

 

9,符号和调试信息去除

 

使用strip程序去掉调试信息

 

  strip lib*.so

 

ld -s 不产生所有符号信息 -S 不产生调试符号信息

 

gcc中对应-Wl以进行参数传递。

 

 

10,使用ldconfig创建软链接

 

  ldconfig -n shared_library_directory

 

  -L指定搜索目录 -l指定路径,使用-rpath也可以。他们有细微差别,参照gcc手册。

 

11,共享库构造和析构

 

  共享库加载时进行一些文件打开、网络连接,可以使用共享库构造函数。只需要在函数前加上 __attribute__((constructor))即可。这种函数在共享库加载的时候执行,显然在main执行前。dlopen打开,则会在dlopen返回前被执行。

 

  析构函数 __attribute__((destructor))声明。main执行后被执行,或者在程序exit时。如果使用dlopen的,则在dlclose时,在其返回前被执行。

 

  上述属性定义在 函数返回值类型 和 函数名称中间,例如:

 

 void __attribute__((constructor)) init_function(void);

 

当然,这是gcc对C和C++的拓展语法。

 

使用构造和析构函数,必须使用系统默认的标准运行库和启动文件,因此不能使用gcc的-nostartfiles和-nostdlib参数。因为构造和析构是在系统默认的标准运行库或者启动文件里被执行的。

 

   默认构造和析构函数没有执行顺序限制,但是可以使用优先级来指定,例如:

 

 void __attribute__((constructor(5))) init_function1(void),其优先级是5。对于构造函数,数字越小优先级越高,析构函数则相反。所以一般使得构造和对应析构函数具有相同优先级,这样便符合资源申请和释放的一般规律:先申请后释放。

 

 

 

12,链接脚本文件

 

  使用链接脚本文件可以将共享库组合起来,从用户看,如同一个新的共享库。比如C运行库和数学库组成libnew.so,则可以如下:

 

GROUP(/lib/libc.so.6  /lib/libm.so.2)

 

 意味着将两个库文件进行格式转换形成新的库文件。

 

这种脚本称作动态链接脚本,因为这个链接过程是动态完成,也就是运行时完成的。