Linux共享库

版权声明:本文为博主原创文章,未经博主允许不得转载。转载请注明出处:http://blog.csdn.net/neuq_jtxw007 https://blog.csdn.net/neuq_jtxw007/article/details/79009351

Linux共享库

8.0 背景

  由于动态链接库有很多优点,大量的程序开始使用动态链接机制,导致系统里面存在大量的共享对象。如果没有很好的方法将这些共享对象组织起来,系统中的共享对象文件会给长期维护、升级造成很大的问题。所以操作系统会对共享对象的目录组织和使用方法有一定的规则。

8.1 共享库版本

8.1.1 共享库兼容性

  共享库的开发者会不停地更新共享库版本,以修正原有的Bug、增加新的功能或改进性能等。其更新可以被分为两类:

  • 兼容更新
      所有的更新只是在原有的基础上添加一些内容,所有原有的接口都保持不变。

  • 不兼容更新
      共享库更新改变了原有的接口,使用该共享库原有接口的程序可能不能运行或运行不正常。

      注意:要破坏一个共享库的ABI十分容易,要保持ABI的兼容缺十分困难。很多因素会导致ABI的不兼容,比如不同版本的编译器或系统库可能会导致结构体的成员对齐方式不一致,从而导致了ABI的变化。

8.1.2 共享库版本命名

  Linux有一套规则来命名系统中的每一个共享库,它规定共享库的文件命名规则必须如下:
libname.so.x.y.z
  lib为前缀、中间是库名和后缀”.so”,最后面跟着的是三个数字组成的版本号。”x”表示主版本号,”y”表示此版本号,”z”表示发布版本号。
  主版本号表示库的重大升级,不同主版本号的库之间不兼容。
  次版本号表示库的增量升级,即增加一些新的接口符号,且保持原来的符号不变。主版本号相同的情况下,高的次版本号的库向后兼容低的次版本号的库。
  发布版本号表示库的一些错误的修正、性能改进等,并不添加任何新的接口,也不对接口进行修改。相同主版本号、次版本号的共享库,不同的发布版本号之间完全兼容。

8.1.3 SO-NAME

  每个共享库都有一个对应的”SO-NAME”,这个SO-NAME即共享库的文件名去掉次版本号和发布版本号,保留主版本号。比如一个共享库叫做libQt5Widgets.so.5.2.1,那么它的SO-NAME即libQt5Widgets.so.5,”SO-NAME”的两个相同共享库,次版本号大的兼容次版本号小的。Linux系统会为每个共享库在它所在的目录创建一个跟”SO-NAME”相同的并且指向它的软链接。

/usr/lib/x86_64-linux-gnu$ ll libQt5Widgets.so.5
lrwxrwxrwx 1 root root 22  92  2016 libQt5Widgets.so.5 -> libQt5Widgets.so.5.2.1

  这个软链接会指向目录中主版本号相同,次版本号和发布版本号最新的共享库。比如目录中有两个共享库版本分别为:libQt5Widgets.so.5.2.1和libQt5Widgets.so.5.8.0,那么软链接libQt5Widgets.so.5会指向libQt5Widgets.so.5.8.0。这样保证以”SO-NAME”为名的软链接指向的是系统中最新版本的共享库。
  Linux提供了一个工具”ldconfig”,当系统中安装或更新一个共享库时,就需要运行这个工具,他会遍历所有的默认共享目录,比如/lib、/usr/lib等,然后更新所有的软链接,使它们指向最新版本的共享库;如果安装了新的共享库,那么ldconfig会为其创建相应的软链接。

链接名
  当我们在编译中需要用到共享库的时候,比如需要链接一个libXXX.so.4.8.1的共享库,只需要在编译时制定-lXXX即可,可省略其他部分。编译器会根据当前环境,在系统中的相关路径(由-L参数指定)查找最新版本的”XXX”库。这个”XXX”又被称为共享库的链接名。

8.2 符号版本

  动态链接器在进行动态链接时,只进行主版本号的判断,即只判断SO-NAME,如果被依赖的共享库SO-NAME与系统中存在的实际共享库SO-NAME一致,那么系统就认为接口兼容,而不再进行兼容性检查。这样就会出现一个问题,程序如果用到了高版本号中新添加的接口而且目前系统中的低版本号中的共享库中不存在,那么就会发生重定位错误。因为次版本号只保证向后兼容,并不保证向前兼容,新版的次版本号的共享库可能添加了一些旧版本号没有的符号。这种问题叫做次版本号交会问题
  对于这个问题,现代系统通过符号版本机制来解决。
  Linux下的Glibc从版本2.1之后开始支持一种叫做基于符号的版本机制方案。这个方案的基本思路是让每一个导出和导入的符号都有一个相关联的版本号,即在新版本中添加的那些全局符号打上一个标记,比如”GLIBC_2.14”。那么如果共享库的每一次次版本号升级,我们都给那些在新的次版本号中添加的全局符号打上相应的标记。Linux下的符号版本机制允许同一个名称的符号存在多个版本
  但是Linux系统下共享库的符号版本机制并没有被广泛应用,主要使用共享库符号版本机制的是Glibc软件包中所提供的20多个共享库。
  下面用 readelf -s /lib/x86_64-linux-gnu/libc-2.19.so 查看C语言运行库的符号表:

Symbol table '.dynsym' contains 2225 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 000000000001f520     0 SECTION LOCAL  DEFAULT   12
     2: 00000000003ba730     0 SECTION LOCAL  DEFAULT   21
     3: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND _rtld_global@GLIBC_PRIVATE (24)
     4: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND __libc_enable_secure@GLIBC_PRIVATE (24)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __tls_get_addr@GLIBC_2.3 (25)
     6: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND _rtld_global_ro@GLIBC_PRIVATE (24)
     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _dl_find_dso_for_object@GLIBC_PRIVATE (24)
     8: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _dl_starting_up
     9: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND _dl_argv@GLIBC_PRIVATE (24)
    10: 0000000000078110   292 FUNC    GLOBAL DEFAULT   12 putwchar@@GLIBC_2.2.5
    11: 0000000000096a70    32 FUNC    GLOBAL DEFAULT   12 __strspn_c1@@GLIBC_2.2.5
    12: 000000000010a2b0    16 FUNC    GLOBAL DEFAULT   12 __gethostname_chk@@GLIBC_2.4
    13: 0000000000096a90    26 FUNC    GLOBAL DEFAULT   12 __strspn_c2@@GLIBC_2.2.5
    14: 0000000000110570   165 FUNC    GLOBAL DEFAULT   12 setrpcent@@GLIBC_2.2.5
    15: 00000000000a7ba0    10 FUNC    GLOBAL DEFAULT   12 __wcstod_l@@GLIBC_2.2.5
    16: 0000000000096ab0    34 FUNC    GLOBAL DEFAULT   12 __strspn_c3@@GLIBC_2.2.5
    17: 00000000000fa950    33 FUNC    GLOBAL DEFAULT   12 epoll_create@@GLIBC_2.3.2
    18: 00000000000cb300    33 FUNC    WEAK   DEFAULT   12 sched_get_priority_min@@GLIBC_2.2.5
    19: 000000000010a2c0    16 FUNC    GLOBAL DEFAULT   12 __getdomainname_chk@@GLIBC_2.4
    .
    .
    .

  可以看到Glibc中有类似”GLIBC_PRIVATE”这样的符号版本,这样的符号版本标记用于GLIBC内部,它提醒共享库使用者,最好不要使用这些符号,因为它不是对外公开的,有可能随着共享库版本的演化被删除或者改变。

8.3 共享库系统路径

  目前大多数包括Linux在内的开源操作系统都遵守一个叫做FHS(英文:Filesystem Hierarchy Standard 中文:文件系统层次结构标准),这个标准规定了一个系统中的系统文件应该如何存放,包括各个目录的结构、组织和作用,这有利于促进各个开源操作系统之间的兼容性。FHS规定,一个系统中主要有三个存放共享库的位置:

  • /lib,系统最关键和基础的共享库,比如动态链接器 ld-2.19.so、C语言运行库 libc-2.19.so、数学库 libm-2.19.so等,这些库主要是/bin和/sbin下的程序要用到的库,还有系统启动时需要的库。
  • /usr/lib,这个目录下主要保存的是一些非系统运行时所须要的关键性的共享库,主要是开发时用到的共享库。
  • /usr/local/lib,这个目录用来放置一些与操作系统本身并不十分相关的库,主要是一些第三方应用程序的库。

      总体来看,/lib和/usr/lib是一些很常用的、成熟的,一般是系统本身所需要的库;而/usr/local/lib是非系统所需的第三方程序的共享库。

8.4 共享库查找过程

  在Linux系统中,动态链接器是/lib/ld-linux.so.X(X是版本号),程序所依赖的共享对象全部由动态连接器负责装载和初始化。动态链接器会在/lib、/usr/lib和由/etc/ld.so.conf配置文件指定的目录中查找共享库。
  ldconfig是一个文本配置文件,它包含其他的配置文件,这些配置文件中存放着目录信息。由ld.so.conf指定的目录是:

include /etc/ld.so.conf.d/*.conf

  ldconfig程序的作用是为共享目录下的各个共享库的创建、删除或者更新相应的SO-NAME,这样每个共享库的SO-NAME就能够指向正确的共享库文件;并且这个程序会将这些SO-NAME收集起来,集中存放到/etc/ld.so.cache文件里面。当动态链接器要查找共享库时,它可以直接从/etc/ld.so.cache里面查找。
  如果动态链接器在/etc/ld.so.cache里面没有找到所须要的共享库,那么它还会遍历/lib和/usr/lib这两个目录。
  如果我们在系统指定的共享目录下添加、删除或更新任何一个共享库,或者我们更改了/etc/ld.so.conf的配置,都应该运行一下ldconfig这个程序,以便调整SO-NAME和/etc/ld.so.cache。
  ldconfig做的这些都与运行程序时有关,跟编译时一点关系都没有。编译的时候还是该加-L就得加,不要混淆了。总之,就是不管做了什么关于library的变动后,最好都ldconfig一下,不然会出现一些意想不到的结果。

8.5 环境变量

LD_LIBRARY_PATH

  在Linux系统中,LD_LIBRARY_PATH是一个由若干个路径组成的环境变量,每个路径之间由冒号隔开。默认情况下,LD_LIBRARY_PATH为空。如果我们为某个进程设置了LD_LIBRARY_PATH,那么进程在启动时,动态连接器在查找共享库时,会首先查找由LD_LIBRARY_PATH指定的目录。LD_LIBRARY_PATH还会影响GCC编译时查找库的路径,它里面包含的目录相当于链接时GCC的”-L”参数。
  具体说来,动态链接器按照下面的顺序来搜索需要的动态共享库:

  1. ELF可执行文件中动态段中RPATH所指定的路径。这实际上是通过一种不算很常用,却比较实用的方法所设置的:编译目标代码时,可以对gcc加入链接参数“-Wl,-rpath”指定动态库搜索路径;
  2. 环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径;
  3. /etc/ld.so.cache中所缓存的动态库路径(如果支持ld.so.cache的话)。这可以通过修改配置文件/etc/ld.so.conf中指定的动态库搜索路径来改变;
  4. 默认的动态库搜索路径/usr/lib;
  5. 默认的动态库搜索路径/lib

LD_PRELOAD
  LD_PRELOAD里面指定的文件会在动态连接器按照固定规则搜索共享库之前装载,无论程序是否依赖它们,LD_PRELOAD 里面指定的共享库都会被装载。
  系统配置文件中有一个文件是/etc/ld.so.preload,它的作用与LD_PRELOAD一样,会被提前装载。

LD_DEBUG
  这个环境变量可以打开动态链接器的调试功能,当我们设置这个变量时,动态连接器会在运行时打印出各种有用的信息。

$ LD_DEBUG=help ls
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

  下面将LD_DEBUG设置成”files”,并且运行一个简单动态链接的helloworld:

$ LD_DEBUG=files ./helloworld
     20737:
     20737: file=libc.so.6 [0];  needed by ./helloworld [0]
     20737: file=libc.so.6 [0];  generating link map
     20737:   dynamic: 0x00007f81035d2ba0  base: 0x00007f8103215000   size: 0x00000000003c42c0
     20737:     entry: 0x00007f8103237050  phdr: 0x00007f8103215040  phnum:                 10
     20737:
     20737:
     20737: calling init: /lib/x86_64-linux-gnu/libc.so.6
     20737:
     20737:
     20737: initialize program: ./helloworld
     20737:
     20737:
     20737: transferring control: ./helloworld
     20737:
hello world
     20737:
     20737: calling fini: ./helloworld [0]
     20737:

  动态链接器打印出了整个装载过程,显示程序依赖哪个共享库并且按照什么步骤装载和初始化,共享库装载时的地址等。

  将LD_DEBUG设置成”libs”,并且运行一个简单动态链接的helloworld:

$ LD_DEBUG=libs ./helloworld
     19625: find library=libc.so.6 [0]; searching
     19625:  search cache=/etc/ld.so.cache
     19625:   trying file=/lib/x86_64-linux-gnu/libc.so.6
     19625
     19625:
     19625: calling init: /lib/x86_64-linux-gnu/libc.so.6
     19625:
     19625:
     19625: initialize program: ./helloworld
     19625:
     19625:
     19625: transferring control: ./helloworld
     19625:
hello world
     19625:
     19625: calling fini: ./helloworld [0]
     19625:

  可以看出须要的共享库libc.so是通过/etc/ld.so.cache找到的。

8.6 共享库的创建和安装

8.6.1 共享库的创建

  创建共享库的过程跟创建一般的共享对象过程基本一致,最关键的是使用GCC的两个参数,即”-shared”和”-fPIC”。”-share”表示输出结果是共享库类型的;”-fPIC”表示使用地址无关代码技术来生产输出文件。还有一个参数是”-Wl”,可以将指定参数传递给链接器。
  我们可以使用如下命令来生成一个共享库:

$gcc -shared -Wl,-soname,my_soname -o library_name source_files library_files

  如果我们不使用-soname来指定共享库的SO_NAME,那么该共享库默认就没有SO_NAME,即使用ldconfig更新SO_NAME的软链接时,对该共享库也没有效果。
  比如我们有一个b1.c源代码文件,希望产生一个libprintfA1.so.1.0.0的共享库,这个共享库依赖于当前目录的liba1.so共享库,我们可以使用如下命令:

gcc -shared -fPIC -Wl,-soname,libprintfA1.so.1 -o libprintfA1.so.1.0.0 b1.c -L./ -la1

  不过这种不加liba1.so共享库的查找路径,加载这个共享库时会找不到liba1.so:

$ ldd libprintfA1.so.1.0.0
    linux-vdso.so.1 =>  (0x00007fffe7e9c000)
    liba1.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8c1ea97000)
    /lib64/ld-linux-x86-64.so.2 (0x0000564b44014000)

  并且通过 readelf -d 查看其动态段表,可以发现有SONAME,并且可以看到这个库依赖的库:

$ readelf -d libprintfA1.so.1.0.0

Dynamic section at offset 0xdf8 contains 26 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [liba1.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libprintfA1.so.1]
 0x000000000000000c (INIT)               0x588
 0x000000000000000d (FINI)               0x6e8
 0x0000000000000019 (INIT_ARRAY)         0x200de0
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x200de8
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x1f0
 0x0000000000000005 (STRTAB)             0x380
 0x0000000000000006 (SYMTAB)             0x230
 0x000000000000000a (STRSZ)              192 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000003 (PLTGOT)             0x201000
 0x0000000000000002 (PLTRELSZ)           72 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x540
 0x0000000000000007 (RELA)               0x480
 0x0000000000000008 (RELASZ)             192 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x460
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x440
 0x000000006ffffff9 (RELACOUNT)          3
 0x0000000000000000 (NULL)               0x0

  所以我们需要在创建共享库时,指定共享库的查找路径,可以用链接器的”-rpath”选项或者GCC的-Wl,-rpath选项

gcc -shared -fPIC -Wl,-soname,libprintfA1.so.1 -o libprintfA1.so.1.0.0 b1.c -Wl,-rpath ./ -L./ -la1

  我们再用 ldd 查看libprintfA1.so.1.0.0的依赖,可以发现liba1.so没有再出现not found:

$ ldd libprintfA1.so.1.0.0
    linux-vdso.so.1 =>  (0x00007ffe4d1c0000)
    liba1.so => ./liba1.so (0x00007faf52455000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007faf52074000)
    /lib64/ld-linux-x86-64.so.2 (0x000056102e81d000)

  再用 readelf -d 查看其动态段表,发现多了一个RPATH,即共享库liba1.so的查找路径:

$ readelf -d libprintfA1.so.1.0.0

Dynamic section at offset 0xde8 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [liba1.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libprintfA1.so.1]
 0x000000000000000f (RPATH)              Library rpath: [./]
 0x000000000000000c (INIT)               0x588
 0x000000000000000d (FINI)               0x6e8
 0x0000000000000019 (INIT_ARRAY)         0x200dd0
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x200dd8
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x1f0
 0x0000000000000005 (STRTAB)             0x380
 0x0000000000000006 (SYMTAB)             0x230
 0x000000000000000a (STRSZ)              195 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000003 (PLTGOT)             0x201000
 0x0000000000000002 (PLTRELSZ)           72 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x540
 0x0000000000000007 (RELA)               0x480
 0x0000000000000008 (RELASZ)             192 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x460
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x444
 0x000000006ffffff9 (RELACOUNT)          3
 0x0000000000000000 (NULL)               0x0

8.6.2 共享库的安装

  创建共享库以后我们须将它安装在系统中,以便于各种程序都可以共享它。最简单的方法就是将共享库复制到某个标准的共享目录,如/lib、/usr/lib等,然后运行ldconfig即可。
  上述方法需要系统root权限,如果没有root权限可以用下面这种方法。共享库的安装核心是:建立相应的SO-NAME软链接、告诉编译器和程序如何查找共享库等

  1. 使用ldconfig建立SO-NAME,需要指定共享库所在目录:ldconfig -n shared_library_directory
  2. 在编译程序时,指定共享库的位置,GCC提供了两个参数”-L”和”-l”,分别用于指定共享库搜索目录和共享库的路径,也可以用链接器的”-rpath”参数或者环境变量 LD_LIBRARY_PATH。

猜你喜欢

转载自blog.csdn.net/neuq_jtxw007/article/details/79009351