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可以是静态或者共享的。
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 和 LIB和ORIGIN 可以出现在这些搜索目录中。他们会被全路径所覆盖。
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
指定这些库的存放路径。