Blog

LD_LIBRARY_PATH和-library-path

Content #

在Linux系统中,LD_LIBRARY_PATH是一个由若干个路径组成的环境变量,每个路径之间由冒号隔开。默认情况下,LD_LIBRARY_PATH为空。

如果为某个进程设置了LD_LIBRARY_PATH,那么进程在启动时,动态链接器在查找共享库时,会首先查找由LD_LIBRARY_PATH指定的目录。

这个环境变量可以很方便地让测试新的共享库或使用非标准的共享库。比如希望使用修改过的libc.so.6,可以将这个新版的libc放到的目录 /home/user中,然后指定LD_LIBRARY_PATH:

$ LD_LIBRARY_PATH=/home/user /bin/ls

Linux中还有一种方法可以实现与LD_LIBRARY_PATH类似的功能,那就是直接运行动态链接器来启动程序,比如:

$/lib/ld-linux.so.2 –library-path /home/user /bin/ls

就可以达到跟前面一样的效果。

From #

程序员的自我修养

ldconfig

Content #

如果动态链接器在每次查找共享库时都去遍历 FHS规定的共享库的位置,那将会非常耗费时间。所以Linux系统中都有一个叫做ldconfig的程序,这个程序的作用是为共享库目录下的各个共享库创建、删除或更新相应的 SO-NAME(即相应的符号链接),这样每个共享库的SO-NAME就能够指向正确的共享库文件;并且这个程序还会将这些SO-NAME收集起来,集中存放到/etc/ld.so.cache文件里面,并建立一个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。

From #

程序员的自我修养

动态链接器与.dynamic段

Content #

在Linux系统中,动态链接器是/lib/ld-linux.so.X(X是版本号),程序所依赖的共享对象全部由动态链接器负责装载和初始化。

任何一个动态链接的模块所依赖的模块路径保存在“.dynamic”段里面,由 DT_NEED类型的项表示。动态链接器对于模块的查找有一定的规则:

  1. 如果DT_NEED里面保存的是绝对路径,那么动态链接器就按照这个路径去查找;
  2. 如果DT_NEED里面保存的是相对路径,那么动态链接器会在/lib、/usr/lib和由

/etc/ld.so.conf配置文件指定的目录中查找共享库。

为了程序的可移植性和兼容性,共享库的路径往往是相对的。

From #

程序员的自我修养

FHS规定的共享库的位置

Content #

  1. /lib 这个位置主要存放系统最关键和基础的共享库,比如动态链接器、C语言运行库、数学库等,这些库主要是那些/bin和/sbin下的程序所需要用到的库,还有系统启动时需要的库。
  2. /usr/lib 这个目录下主要保存的是一些非系统运行时所需要的关键性的共享库,主要是一些开发时用到的共享库,这些共享库一般不会被用户的程序或shell脚本直接用到。这个目录下面还包含了开发时可能会用到的静态库、目标文件等。
  3. /usr/local/lib 这个目录用来放置一些跟操作系统本身并不十分相关的库,主要是一些第三方的应用程序的库。比如我们在系统中安装了python语言的解释器,那么与它相关的共享库可能会被放到/usr/local/lib/python,而它的可执行文件可能被放到/usr/local/bin下。GNU的标准推荐第三方的程序应该默认将库安装到/usr/local/lib下。

From #

程序员的自我修养

次版本号交会问题(Minor-revision Rendezvous Problem)

Content #

动态链接器在查找共享库过程中,可采用两种策略:

  1. 警告的策略

如果找到的共享库次版本号低于所需要的版本, SunOS 4.x系统的策略是向用户发出一个警告信息,表示系统中仅有低次版本号的共享库,但运行程序还是继续运行。

  1. 保守策略

有些采取更加保守策略的系统中,对于这种系统中没有足够高的次版本号满足依赖关系的情况,程序将会被禁止运行,以防止出现意外情况。

实际上很多应用程序在高次版本的系统中都有构建,但实际上它只用到了低次版本的那部分接口,在采取第二种策略的系统中,如果系统中只有低次版本号的共享库,那么这些程序就不能运行。我们可以把这个问题叫做次版本号交会问题(Minor-revision Rendezvous Problem)。

次版本号交会问题并没有因为SO-NAME而解决。

From #

程序员的自我修养

SO-NAME

Content #

Solaris和Linux普遍采用一种叫做SO-NAME的命名机制来记录共享库的依赖关系。每个共享库都有一个对应的“SO-NAME”,这个SO-NAME即共享库的文件名去掉次版本号和发布版本号,保留主版本号。

比如一个共享库叫做libfoo.so.2.6.1,那么它的SO-NAME即libfoo.so.2。很明显,“SO-NAME”规定了共享库的接口,“SO-NAME”的两个相同共享库,次版本号大的兼容次版本号小的。在Linux系统中,系统会为每个共享库在它所在的目录创建一个跟“SO-NAME”相同的并且指向它的软链接(Symbol Link)。比如系统中有存在一个共享库“/lib/libfoo.so.2.6.1”,那么Linux中的共享库管理程序就会为它产生一个软链接“/lib/libfoo.so.2”指向它。

From #

程序员的自我修养

-export-dynamic

Content #

默认情况下,链接器在产生可执行文件时,只会将那些链接时被其他共享模块引用到的符号放到动态符号表,这样可以减少动态符号表的大小。也就是说,在共享模块中反向引用主模块中的符号时,只有那些在链接时被共享模块引用到的符号才会被导出。

有一种情况是,当程序使用dlopen()动态加载某个共享模块,而该共享模块须反向引用主模块的符号时,有可能主模块的某些符号因为在链接时没有被其他共享模块引用而没有被放到动态符号表里面,导致了反向引用失败。

ld链接器提供了一个“-export-dynamic”的参数,这个参数表示链接器在生产可执行文件时,将所有全局符号导出到动态符号表,以防止出现上述问题。我们也可以在GCC中使用“-Wl,-export-dynamic”将该参数传递给链接器。

From #

程序员的自我修养

不同ELF可执行文件的程序入口

Content #

对于不同链接形式的ELF可执行文件,这个程序的入口是有区别的。

  • 对于静态链接的可执行文件来说,程序的入口就是ELF文件头里面的e_entry指定的入口;
  • 对于动态链接的可执行文件,内核会分析它的动态链接器地址(在“.interp”段),将动态链接器映射至进程地址空间,然后把控制权交给动态链接器。

From #

程序员的自我修养

动态链接器的位置

Content #

动态链接器的位置既不是由系统配置指定,也不是由环境参数决定,而是由ELF 可执行文件决定。

在动态链接的ELF可执行文件中,有一个专门的段叫做“.interp”段(“interp”是“interpreter”(解释器)的缩写)。如果我们使用objdump工具来查看,可以看到“.interp”内容:

$ objdump -s a.out

a.out:     file format elf32-i386

Contents of section .interp:
 8048114 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so
 8048124 2e3200                                     .2.

“.interp”的内容很简单,里面保存的就是一个字符串,这个字符串就是可执行文件所需要的动态链接器的路径,在Linux下,可执行文件所需要的动态链接器的路径几乎都是“/lib/ld-linux.so.2”,其他的*nix操作系统可能会有不同的路径。在Linux的系统中,/lib/ld-linux.so.2通常是一个软链接,比如它可能指向/lib/ld-2.6.1.so,这个才是真正的动态链接器。

From #

程序员的自我修养

多进程或多线程访问共享模块中的全局变量

Content #

Q: 如果一个共享对象lib.so中定义了一个全局变量G,而进程A和进程B都使用了 lib.so,那么当进程A改变这个全局变量G的值时,进程B中的G会受到影响吗?

A: 不会。因为当lib.so被两个进程加载时,它的数据段部分在每个进程中都有独立的副本,从这个角度看,共享对象中的全局变量实际上和定义在程序内部的全局变量没什么区别。任何一个进程访问的只是自己的那个副本,而不会影响其他进程。那么,如果我们把这个问题的条件改成同一个进程中的线程A和线程B,它们是否看得到对方对lib.so中的全局变量G的修改呢?对于同一个进程的两个线程来说,它们访问的是同一个进程地址空间,也就是同一个lib.so的副本,所以它们对G的修改,对方都是看得到的。

From #

程序员的自我修养