用示例来帮助我们更好的理解linux的二次依赖问题

linux libraries

本文只是The Linux Documentation Project library howto的简要总结。
linux linker 和 loader的man pages 也是很好的信息源。
在linux中,有三种类型的库文件:static,shared, dynamically loaded(DL)。
动态加载库有一些特定的应用场景,例如插件等。本文将专注与static和 shared 库。

Static libraries

static library 是一些目标文件的集合或者归档,命名规则是前缀 lib,后缀.a。
例如: libfoobar.a
static library 使用ar 来创建

ar rcs libfoobar.a foo.o bar.o
  • r: insert member into the archive,会删除重复的member,使用最新的member
  • s: add index to the the archive
  • c: modifier, create the archive

程序链接静态库是很简单的,只需要在命令行中添加静态库的全路径:

gcc -o app main.c /path/to/foobar/libfoobar.a

或者 使用 -L/l选项来间接指定:

gcc -o app main.c -lfoobar -L/path/to/foobar

Shared libraries

动态共享库是ELF格式的目标文件,当程序运行时,会被加载到内存中。
动态共享库的后缀是.so,例如:libfoobar.so
动态共享库目标文件需要通过gcc编译得到,编译的时候需要添加 -fPIC选项以产生位置无关代码,方便代码在内存中被重定位。

gcc -fPIC -c foo.c
gcc -fPIC -c bar.c

使用gcc创建一个动态共享库就像创建一个可执行文件一样,只是需要添加-shared 选项

gcc -shared -o libfoobar.so foo.o bar.o

程序链接动态共享库的方法和静态库是一样的。

动态共享库和未定义符号

ELF 对象维护着一张它使用到的符号表,包括一些属于其他ELF对象的符号,这种情况下,这些对象被标记为未定义。

在编译期,ld 连接器尝试解决未定义的符号,通过把静态库中的代码链接到输出ELF目标文件 或者 动态的链接由动态共享库提供的代码。

如果在共享库中发现一个未定义的符号,一个 DT_NEEDED条目将在输出ELF目标文件中被创建。

DT_NEEDED 字段的含义依据于链接命令:
- 如果该库以绝对路径链接,那么存储全路径
- 否则存储 库名称(或者soname,如果soname被设置)

你可以使用readelf命令来检查一个ELF目标文件的依赖关系

readelf -d main

or

readelf -d libbar.so

当产生可执行文件时,如果仍然有符号是未定义,那么连接器会报错:
all dependencies must therefore be available to the linker in order to produce the output binary

由于历史原因,当编译动态共享库时,该行为是被失能的,你需要指定--no-undefined(or -z defs) 标志显式的指定有未定义符号时,需要报错。

gcc -Wl,--no-undefined -shared -o libbar.so -fPIC bar.c

or

gcc -Wl,-zdefs -shared -o libbar.so -fPIC bar.c

注意:当产生静态库时,实际上,没有链接这一操作,未定义的符号将保持不变。

合理的处理二次依赖问题

问题: 共享库的依赖是它的目标文件。

让我们想象这样一个情形:main 程序依赖 libbar库,libbar库依赖共享库libfoo.so。
libbar可以是静态或者共享的。

main
libbar.a/libbar.so
libfoo.so
foo.c

int foo(){
	return 42;
}
bar.c 

int foo();

int bar(){
	return foo();
}
main.c

int bar();

int main(int argc,char* argv[]){
	return bar():
}

创建libfoo.so

libfoo.so只依赖libc:

gcc -shared -o libfoo.so -fPIC foo.c

创建libbar.a

gcc -c bar.c
ar rcs libbar.a bar.o

正如之前所说,静态库只是目标文件的集合,无法体现外部依赖关系。
在这个场景下,libbar.a 和libfoo.so之间没有显式的联系

创建libbar.so

合理的方式是创建动态共享库,可以显式的指定它依赖于libfoo

gcc -shared -o libbar2.so -fPIC bar.c -lfoo -L$(pwd)

上述命令将为libbar.so创建一个DT_NEEDED项,值是libfoo.so

$ readelf -d libbar.so
Dynamic section at offset 0xe08 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libfoo.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
...

然而,当编译动态库时,未定义符号没有被默认的解决,所以,我们创建dumb库来模拟,该库没有DT_NEEDED项:

gcc -shared -o libbar_dump.so -fPIC bar.c

链接libbar.a

正如之前所说,当链接可执行文件时,连接器必须解决所有的未定义符号。

只链接libbar.a会产生一个错误,因为它有未定义的符号,连接器无法找到这些符号在何处定义:

$ gcc -o app_s main.c libbar.a
libbar.a(bar.o): In function `bar':
bar.c:(.text+0xa): undefined reference to `foo'
collect2: error: ld returned 1 exit status

在链接命令中添加libfoo.so,将解决这个问题:

gcc -o app main.c libbar.a -L$(pwd) -lfoo

此时,你可以发现,app显式的依赖于libfoo.so

$ readelf -d app
Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libfoo.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
...

在运行时,动态链接器ld.so 将寻找libfoo.so,除非你安装它到标准目录(/lib, /usr/lib),否则你需要告诉ld.so到哪里寻找libfoo.so:

LD_LIBRARY_PATH=$(pwd) ./app

总结: 当可执行程序链接静态库时,你需要显式的指定该静态库的所有依赖。

链接libbar.so

正如连接器文档中所述,当连接器ld遇到动态库作为输入时,它将处理动态库的所有DT_NEEDED 项,作为它的二次依赖:
- 如果连接器输出是一个动态库,(-copy-dt-needed-entries选项被设置,注意该选项是历史遗留)它将添加输入动态库的所有DT_NEEDED 项到输出目标中。
- 如果连接器输出是动态库,-no-copy-dt-needed-entries 被设置(默认行为),连接器将简单的忽略输入动态库的DT_NEEDED 项。
- 如果连接器输出是一个非共享,非重定向的(例如可执行文件),它将自动的添加输入动态库的DT_NEEDED项中所列举的库到命令行,如果它不能找到他们,就报错。

让我们看下当处理2个动态共享库时,将会发生什么事情。

链接dump库

当只链接dump库到app中时,会发生未定义符号的问题,因为它缺少DT_NEEDED libfoo.so项

$ gcc -o app main.c -L$(pwd) -lbar_dumb
libbar_dumb.so: undefined reference to `foo'
collect2: error: ld returned 1 exit status
怎样解决呢

当链接时,显示的添加-lfoo到命令行,指明依赖关系

 gcc -o app main.c -L$(pwd) -lbar_dumb -lfoo


$ readelf -d app
Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libbar_dumb.so]
 0x0000000000000001 (NEEDED)             Shared library: [libfoo.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
...

其实注意到app中有libfoo的显式依赖是不太正确的,因为app并未直接用到libfoo.so中的符号。我们称这种情况是 overlinking

让我们想像一种情形,在将来的某一天,我们决定提供一个新版的libbar.so,该libbar.so有相同的ABI,但是它基于一个新的不同与之前ABI接口的libfoo.so版本:理论上,我们不用重新编译app,就能直接使用新版的libbar.so。但是实际会发生的情况是,动态链接器将加载2个版本的libfoo.so到内存,会导致意外的结果。那么即使app没有使用libfoo.so中的符号,那么也需要重新编译app。

忽略libfoo.so依赖

处理dump库的另一种方式是:告诉链接器忽略未定义的符号:

 gcc -o app main.c -L$(pwd) -lbar_dumb -Wl,--allow-shlib-undefined


$ readelf -d app
Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libbar_dumb.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
...

其实这个app是不能正常运行的,因为ld.so无法处理app的依赖:

./app: symbol lookup error: ./libbar_dumb.so: undefined symbol: foo

你只能手动加载libfoo.so到内存中:

$ LD_PRELOAD=$(pwd)/libfoo.so LD_LIBRARY_PATH=$(pwd) ./app
正确的链接libbar.so库

正如上面提到的,当链接器争正确的链接动态库时,它会产生一个libfoo.so DT_NEEDED条目,添加它到链接命令,在指定路径下查找到该库,然后正确决议未定义的符号,至少这是我所希望看到的:
但是当我这样编译的时候,发生错误了:

$ gcc -o app main.c -L$(pwd) -lbar
/usr/bin/ld: warning: libfoo.so, needed by libbar.so, not found (try using -rpath or -rpath-link)
/home/diec7483/dev/linker-example/libbar.so: undefined reference to `foo'
collect2: error: ld returned 1 exit status

为什么产生错误呢?

让我们一起看下 ld 手册,找到 -rpath-link选项

当使用ELF时,一个动态库可能会依赖另一个。当ld -shared命令中 一个动态库作为输入时,将会产生。
当ld处理一个非动态,非重定向的连接时,会产生一个依赖需求,ld会自动定位被依赖的库,如果它没有显式包含在链接中,则把它包含在本次链接中。在这种情况下,-rpath-link选项指定了搜索目录集合。它可以指定一系列由冒号(:)分割的目录名称,或者该选项可以出现多次。
L I B 和 LIB 和 LIBORIGIN 可以出现在这些搜索目录中。他们会被全路径所覆盖。
ld 使用以下的路径用于搜索被依赖的库:
1. 由 -rpath-link指定的目录
2. 由-rpath指定的目录。
3. 由环境变量 LD_RUN_PATH指定的目录
4. 对于本机链接器,使用环境变量LD_LIBRARY_PATH 指定的目录
5. 对于本机链接器,使用DT_RUNPATH DT_RPATH
6. 默认目录:/lib and /usr/lib
7. 对于本机链接器,如果/etc/ld.so.conf存在,搜索该文件中列举的目录
如果以上目录还没有搜索到被依赖的库,ld会发出警告,继续链接。

实际上说,当为二次依赖的库指定路径时,不应该使用-L,而是使用rpath-link

$ gcc -o app main.c -L$(pwd) -lbar -Wl,-rpath-link=$(pwd)


$ readelf -d app
Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libbar.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
...

这就是最终的解决方法

你可能会使用 -rpath 来替代-rpath-link,但是这时,指定的路径将被存储到可执行文件中,如果你计划改变可执行文件的路径,那么会带来问题。像cmake工具,是在编译阶段使用-rpath,但是在安装阶段make install 会把指定的路径从可执行文件中删除。

总结

当链接库到可执行文件时,有2中情况:

  • 链接静态库,你只需要手动指定所有的依赖
  • 动态库,你不必指定所有的二次依赖,但是需要使用-rpath/-rpath-link指定这些库的存放路径。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值