When an executable is linked with a shared object which has a DT_SONAME field, then when the executable
is run the dynamic linker(注:就是ld-linux-x86-64.so.2) will attempt to load the shared object specified
by the DT_SONAME field rather than the using the file name given to the linker(注:应该是指通过-lmystuff传给
linker的文件名,也就是libmystuff.so).
共享库简介
- Introduction of Shared Library
不同的名字 Names
一个共享库常常有多个名字,比如
像
libadd.so.1
这样的叫做soname
,通常是由“lib”
,紧接着库的名字,紧接着“.so”
,然后跟着一个版本号。版本号通常是递增的。像
libadd.so.1.0.1
这样的叫做real name
,通常是soname之后再加一个“小版本号(minor number)”和一个“发布版本号(release number)”。也可以不加“发布版本号”。像
libadd.so
这样的叫做linker name
。这通常是给链接器(linker)用的。比如,运行下面的命令,linker就会去找叫libadd.so
或者libadd.a
的库。通常
real name
对应的是真正的库文件,而soname
和linker name
对应的只是个符号链接(symbolic link)而以。soname
对应的 符号链接可以通过ldconfig
来创建。只要把共享库放在某个ldconfig
知道的目录下,比如/usr/local/lib
,然后再运行即可。
linker name
对应的符号链接不会被ldconfig
创建,需要自己手工创建一下。使用共享库
动态链接的可执行程序在运行时会先加载一个
dynamic loader
,一般叫ld-linux.so*
,然后这个loader再去加载这个可执行文件用到的 其它共享库。dynamic loader
的具体名字可以在可执行文件里找到,从上面的输出里可以看到,在我的环境里它叫
/lib64/ld-linux-x86-64.so.2
。ld-linux-x86-64.so.2
是如何找到一个可执行文件需要的其它共享库的呢?在它的man page(man ld-linux
或者man ld-so
)里大概 是这么说的,这里,暂时先不去关心
rpath
和LD_LIBRARY_PATH
。缓存文件/etc/ld.so.cache
是被ldconfig
程序管理的。系统配置文件/etc/ld.so.conf
里记录着共享库的搜索目录,比如/usr/local/lib
等。一个可执行文件里只记录着共享库的soname,并没有完整的路径,比如如果每次都要去
/usr/local/lib
、/lib
、/usr/lib
等目录里搜索会影响性能,所以把共享库的soname和完整的路径缓存在文件/etc/ld.so.cache
里以提高查找性能。通过ldconfig
来维护缓存的有效性。当新加、删除共享库或者共享库搜索目录变化时,记得运行ldconfig
来更新缓存信息。一些ldconfig
的简单用法,通常
/etc/ld.so.conf
指定的目录中会包含/usr/local/lib
目录,一般来说用户安装的共享库会放在这个目录里。/lib
通常用来 放置启动时需要的库,而/usr/lib
通常用来放置随Linux发行版自带的库。除了/usr/local/lib
,也可以在/etc/ld.so.conf
指定 新的目录来放置共享库。创建共享库
上述命令的一些解释:
-fPIC
选项用来生成位置无关代码(position-independent code)。有两个类似的选项,-fPIC
和-fpic
。两者的区别是-fpic
在 生成位置无关代码时会检查global offset table (GOT)有没有超过相关平台的限制,如果超过则报错;而-fPIC
会避免这些限制。据说,-fpic
生成的目标文件相比-fPIC
要小一些。反正用-fPIC
总是没错的。两个选项的具体描述见man gcc
。-Wl,-soname,libmystuff.so.1
,-Wl
指明后面跟着的选项-soname
是传给linker的。注意是用逗号来分隔,而不是空格;如果有空格, 记得要转义。-soname
选项后跟的共享库的soname。如果使用了这个选项,那么在创建共享库时,给定的soname会写到库文件中(ELF的DT_SONAME
字段)。如果一个可执行文件链接到设置了DT_SONAME
字段共享库,会(详见man ld
),-o libmystuff.so.1.0.1
指定了共享库的real name
,包含详细的版本信息。-lc
,如果这个共享库依赖别的库,那么用-l
选项指定。-lxxx
应该放在*.o
的后面,否则gcc会报undefined reference to
的错误消息。 这是因为如果链接器遇到-lxxx
时,如果发现那时没有任何.o文件用到这个库的符号(symbol),就不会把这个库链接进来。即使,-l
选项的后面有.o文件用到了那个库,链接器也不会再去尝试链接这个库。详见man ld
。另外,为了方便调试,最好不要剥离库的符号,也不要在编译时指定
-fomit-frame-pointer
选项。安装和使用共享库
下面的例子都是安装到
/usr/local/lib
目录下,实际上任何/etc/ld.so.conf
指定的目录以及默认目录/lib
和/usr/lib
都是可以的。如果要安装一个新的共享库,那么把库文件拷贝到
/usr/local/lib
,然后运行ldconfig
更新缓存并建立符号链接。如果要更新共享库,且共享库只是小版本升级。那么只需要更新一下符号链接就可以,不需要更新缓存。因为缓存只是记录着soname和它的全路径信息, 并没有任何小版本信息。
如果库是大版本升级,那么符号链接和缓存都需要更新。
ldconfig
是如何建立符号链接的呢?前面讲到,通过-soname
选项可以把库的soname存到库文件里,ldconfig
会读取这个字符串,以它 作为符号链接的名字。所以创建共享库时,记得指定-soname
选项并赋予正确的名字。最后,因为
ldconfig
只会创建从real name
到soname
的如何链接,为了让链接器可以找到共享库,还需要建立linker name
对应的 符号链接。使用共享库有两种使用场景:编译/链接时和运行可执行文件时。
编译/链接时(linker怎么找到共享库)
如果共享库已经放在了
/usr/local/lib
等“标准目录”下,那么直接通过-l
使用即可,如果由于某些原因共享库还没安装到“标准目录”下,比如库还处于开发中,那么可以通过
-L
指定库的搜索目录,man ld
中详细地描述了链接器是怎么搜索共享库的。运行可执行文件时(dynamic loader怎么找到共享库)
同样地,如果共享库已经放在了
/usr/local/lib
等“标准目录”下,那么无需额外的工作。如果由于某些原因共享库还没安装到“标准目录”下,比如库还处于开发中,那么需要告诉dynamic loader(
ld-linux-x86-64.so.2
) 去哪里找这个库。否则会有下面的错误使用
LD_LIBRARY_PATH
,可以在.bashrc
等文件里全局地设置LD_LIBRARY_PATH
变量或者在某个shell里单独地设置,LD_LIBRARY_PATH
最好只作为一个临时的手段,这篇文章里讲了为什么使用LD_LIBRARY_PATH
是不好的。使用
rpath
,rpath
会把库的搜索目录编译到可执行文件里。版本控制 Versioning
系统常常会存在着不同版本的共享库,不同的可执行文件可能使用不同版本的共享库,那么是如何做到不发生冲突的呢?
从上面的
readelf
的输出可以看到,可执行文件里存在它依赖的共享库的soname
,也就是共享库的“大版本信息”。soname
只是一个符号链接,它指向某个具体版本的库文件,比如libadd.so.1.0.1
。soname
指向的库文件可能会更新。为了使得 库文件的更新不影响可执行文件的运行,这就要求库的更新不会产生任何兼容性问题。如果存在兼容性问题,那么在生产库 的时候需要增加soname
里的版本号。这篇文章里讲了会引起兼容性问题的典型情形,比如改变接口行为,删除接口等。
其它
ldd
使用
ldd
可以查看一个可执行文件需要的共享库,比如这篇文章和
man ldd
都提到了ldd
可能会有安全问题。不要对不信任的程序执行ldd
命令。可以用下面的命令代替ldd
,TODOs
/etc/ld.so.preload
覆盖共享库的部分接口LD_PRELOAD
LD_DEBUG
如果你觉得这篇文章对你有用,可以微信扫一扫表示🙏 / If you find this post is useful to you, buy me 🍶 via Wechat