0.序
本文承接上一篇动态链接4,这应该是关于动态链接笔记的最后一篇了。略去了书上关于符号版本的讨论。
10. linux下的动态库管理
通常linux下动态库遵循命名规则libname.so.x.y.z
,该动态库对应的SONAME
是libname.so.x
,链接时使用-lname
, 比如我机器上的/usr/lib/libpcap.so.1.10.1
,它的SONAME
则是libpcap.so.1
。链接时使用参数-lpcap
。因此根据上述规则,主版本号不同的动态库对应的 SONAME
也不同。这主要是因为主版本号不相同的动态库的ABI一般不互相兼容,因此用SONAME
做一个区分。
根据之前的讨论可以想到,如果使用上述规则,那么要正常使用一个动态库,应该至少存在两个符号链接,libname.so
和libname.so.x
。这是因为,使用-lname
链接时,link editor会去寻找动态库libname.so
,在DT_NEEDED
中记录的则是libname.so.x
,因此dynamic linker会去寻找libname.so.x
这个库。
这样曲折的管理方式主要便于动态库的更新和升级,设想我把libpcap.so.1.10.1
升级为libpcap.so.1.11.1
后,只需要修改libpcap.so.1
指向新版的动态库,而以前所有依赖libpcap
的可执行程序不需要做任何改动就可以直接使用新版的libpcap
。而libpcap.so
的存在则帮助我们忽略版本号。
glibc
中有一个动态库版本管理工具ldconfig
,可以帮助我们管理动态库的符号链接并创建/etc/ld.so.cache
。
下面通过一个例子演示一下:
// lostsymlib.c
// gcc -Wl,-soname=liblost.so.2 -fPIC -shared -o liblost.so.2.1.1 lostsymlib.c
// gcc -Wl,-soname=liblost.so.1 -fPIC -shared -o liblost.so.1.2.3 lostsymlib.c
// 由此得到两个版本的动态库
#include <stdio.h>
void print_hello(){
printf("hello, i am .so.2 lib\n");
}
现在我们有动态库liblost.so.2.1.1
, liblost.so.1.2.3
。接下来使用ldconfig(man ldconfig
可以查到相应参数的含义)。
$ ldconfig -v -n .
.:
liblost.so.2 -> liblost.so.2.1.1 (changed)
liblost.so.1 -> liblost.so.1.2.3 (changed)
可以看到,当前目录下增加了两个符号链接,liblost.so.1
, liblost.so.2
,此即这两个库的SONAME
。
然后我们用之前的指令再增加一个新的动态库,liblost.1.3.5
。
$ ldconfig -v -n .
.:
liblost.so.2 -> liblost.so.2.1.1
liblost.so.1 -> liblost.so.1.3.5 (changed)
ldconfig
修改了liblost.so.1
的符号链接指向,使其指向了版本更新的liblost.so.1.3.5
。
不过让人疑惑的是,ldconfig
并不会自动生成或者修改符号链接liblost.so
。这意味着,如果此前的liblost.so
指向liblost.so.1.2.3
,后来新增了liblost.2.1.1
,那么必须要手动修改liblost.so
的指向,否则-llost
仍会链接到低版本的动态库。这个行为有点迷惑,stackoverflow上也有相关讨论Using ldconfig on Linux。
最后说一下/etc/ld.so.cache
和/etc/ld.so.conf
这两个文件和ldconfig
的关系,默认情况下ldconfig
扫描的是/etc/ld.so.conf
下记录的目录,更新这些目录下的共享库文件的符号链接,并且把相应的链接关系记录到/etc/ld.so.cache
中,这样便于dynamic linker快速搜索。
11. 关于动态库的entry point
Why and how are some shared libraries runnable, as though they are executables?
这里讨论了如何生成一个可以直接运行的动态库。在举一个实际的例子前,不妨从一个较高层次上思考动态库可不可以有entry point。相比于可执行文件,动态库除了具有位置无关这一特性外,还有什么其它的分别吗?
如果我们把动态库看作一堆函数的集合,选择其中某个函数作为可执行程序的入口点,如果直接执行则从该函数开始。若是作为动态库加载那就正常加载。这样一想其实动态库完全可以也有entry point。
看下面的例子
// entry.c
// gcc -fPIC -pie -o entry.so entry.c
#include <stdio.h>
#include <stdlib.h>
void print_hello(char* s){
printf("hello %s!\n", s);
}
int main(int argc, char **argv)
{
print_hello(argv[0]);
return 0;
}
// havea.c
// gcc -o havea havea.c ./entry.so
#include <stdio.h>
void print_hello(char*);
int main(int argc, char* argv[]){
print_hello(argv[0]);
return 0;
}
运行结果如下
$ ./entry.so
hello ./entry.so!
$ ./havea
hello ./havea!
根据全局符号介入的规则,entry.so
作为共享库时,_start
,main
函数等被覆盖了。
如果结合到ASLR,默认情况下gcc
生成的可执行文件都应该是地址无关的,因此理论上来讲都可以作为动态库使用。
$ readelf -h havea
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x730
Start of program headers: 64 (bytes into file)
Start of section headers: 6384 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
从Type
属性可以看到,其实编译出的havea
其实就可以看作是一个动态库(我们知道,四种目标文件类型ET_REL
, ET_EXEC
, ET_DYN
, ET_CORE
,分别对应.o
, .exe
, .so
, core dump
文件)。
那怎样得到一个EXEC
type文件呢?我们可以用如下的方法编译
$ gcc -o havea -no-pie havea.c ./entry.so
$ readelf -h havea
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400600
Start of program headers: 64 (bytes into file)
Start of section headers: 6312 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
可以看到,这下生成的文件类型变为了EXEC
, 并且entry point
固定了,即该执行文件不满足ASLR
的要求。
OK,与动态库相关的东西差不多到此结束。关于动态库的初始化,等看到书的第四部分再继续写吧。