动态库
1. 命名规则
lib + 库的名子 + .so
,例如:libtest.so
。
2. 制作过程
先来搞清楚一个概念:位置无关码。当跳转使用绝对地址的时候使用的就是位置有关码,而如果使用相对地址来访问(即当前指令的偏移)使用就是位置无关码。
其实动态库就是将一些生成的与位置无关的.o文件打包到一起得到一个.so文件,所以制作过程主要有三大步,我们举一个简单的例子来说明,比如我现在要实现一个算法库,该库只实现简单的加减乘除4个功能。
-
准备原材料:
首先需要编写好必要的源文件以及头文件,我们把头文件放到include目录,源文件放到src目录,即将生成的库文件放到lib目录中。
-
生成与位置无关的代码(生成与位置无关的.o):
使用参数-fPIC:gcc -fPIC -c *.c -I../include
。
-
将生成的.o打包成动态库:
使用参数-shared:gcc -shared -o libcal.so *.o -I../include
。
这样我们就制作好了一个动态库。
3. 发布与使用
- 发布动态库:
上面我们制作好的动态库需要发布给用户。 - 发布头文件:
因为库文件都是一些编译生成的目标文件的集合,用户是没有源码的,所以在头文件中需要体现库中的接口,包括使用一些注释对函数功能参数以及返回值做一些说明。 - 使用:
用户在使用的时候只需要包含提供的头文件,就可以调用相应的接口了。我们来看一下在编译的时候如何添加该库,下面有两种常用的方式:
方式一:gcc main.c ./lib/libcal.so -o app -Iinclude
方式二:gcc main.c -L lib -lcal -o myapp -Iinclude
从上面两种方式的输出结果可以看到方式一正常输出了而方式二产生了错误,我们可以查看一下这两种方式生成的可执行文件所依赖的动态库。 - 查看共享库
使用ldd命令:ldd + 可执行程序
可以查看可执行文件在执行的时候所依赖的动态库。
其中查看显示的最后一行:/lib64/ld-linux-x86-64.so.2
是动态连接器。应用程序在调用动态库的时候需要另外一个程序帮助完成,这个程序就是动态链接器,有了动态链接器会自动调用需要的动态库。
从上面的输出结果我们看出有些动态库链接成功,有些没有成功,没有成功说明违反了动态库的查找规则。 - 动态链接库搜索顺序:
- 首先查看程序文件的.dynamic 段是否包含了一个叫DT_RPATH的项(它是一个以冒号分隔的库文件搜索目录列表),编译时加入选项
-Wl,-rpath
即可,例如gcc main.c -L lib -l mycal -Wl,-rpath=./lib -o app -Iinclude
。 - 环境变量LD_LIBRARY_PATH 指定的动态库搜索路径(它是一个以冒号分隔的库文件搜索目录列表)。
- /etc/ld.so.conf,配置文件中包含库的路径。
- 默认的动态库搜索路径/lib。
- 默认的动态库搜索路径/usr/lib。
- 首先查看程序文件的.dynamic 段是否包含了一个叫DT_RPATH的项(它是一个以冒号分隔的库文件搜索目录列表),编译时加入选项
- 解决动态库链接失败:
- 临时设置:
export LD_LIBRARY_PATH=动态库路径
,LD_LIBRARY_PATH变量并不是默认的动态库的环境变量,但是如果设置这个变量,该路径会在默认的环境变量之前进行搜索。这种方式只能临时生效,适合开发过程中或测试阶段使用,当终端重启后,该环境变量的设置就会失效。
- 永久设置一:
在当前用户的bash配置文件中进行设置,可以达到目的但方法不够正规。- cd:进入家目录。
vi .bashrc
:打开配置文件。export LD_LIBRARY_PATH=绝对路径
:设置环境变量写入绝对路径。- 关掉终端重新打开才能生效,终端每次重启都会先读取.bashrc。
- 永久设置二:
使用该方法是比较正规的方法。cd /etc/ld.so.conf
:找到动态链接器的配置文件。- 动态库的绝对路径写到配置文件中。
sudo ldconfig -v
:更新
- 临时设置:
4. 深入理解动态链接
如果使用了动态库,库文件没有打包到可执行文件中,那么动态库是如何加载,应用程序是怎么确定所调用的库函数的地址呢?我们要先来了解一个概念叫做延迟重定位。
4.1 延迟重定位
ELF 文件对调用动态库中的函数采用延迟重定位的策略,所谓的延迟重定位就是在程序第一次调用外部函数的时候再进行重定位。试想如果程序调用了比较多的动态库函数时,如果在程序启动阶段进行重定位,将库函数地址填入到got表中,这必将影响系统的启动速度。利用ELF格式文件中的plt和got可以实现延迟重定位。
- plt(Procedure Link Table):程序链接表,属于代码段,类似数组。每一段plt中都有一小段代码,可以将这段代码看作中间函数,会去got表中读取相应的地址进行跳转。首次对动态库中的函数调用都会跳转到plt[0],这段代码可以访问动态链接器中的函数,通过压入栈中的参数来确定真正函数的地址,再将这个地址写入到对应的got中,下次再调用同一个函数时就可以直接跳转到真正的地址,不需要再调用动态连接器了。
- got(lobal Offset Table):全局偏移表,属于数据段,类似数组。该表中存放的都是符号的地址,对动态库的访问会到该表中取出相应的地址。got前三项为特殊项:GOT[0]对应本ELF动态段(.dynamic段)的装载地址,GOT[1]对应本ELF的link_map数据结构描述符地址,GOT[2]对应_dl_runtime_resolve动态链接器函数的地址。3个特殊项后面依次是每个动态库函数的GOT表项。
4.2 mmap映射
可执行程序启动对共享库的加载是使用内存映射的方式,使用strace命令跟踪一下系统调用过程:
从上面的输出结果可以看出程序运行时先调用open()打开了动态库,然后进行了mmap操作,所以共享库的加载是通过内存映射的方式加载到物理内存的。
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。注意:虚拟地址并非真正的物理内存地址,虽然创建虚拟区间并完成地址映射,但是并没有将任何文件数据拷贝至物理内存。真正的文件读取是当进程发起读或写操作访问到这段映射地址时,引发缺页异常,然后才会将磁盘的数据读取到物理内存中。
4.3 重定位过程分析
先看一下main()函数中输出的库函数add()的地址:
0x4005a0这个地址是不是库函数真正的地址,库函数的调用是如何实现重定位的,
我们先使用 objdump -d 输出一个反汇编,看一下这些地址的内容:
Disassembly of section .plt:
0000000000400590 <add@plt-0x10>:
400590: ff 35 72 0a 20 00 pushq 0x200a72(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400596: ff 25 74 0a 20 00 jmpq *0x200a74(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40059c: 0f 1f 40 00 nopl 0x0(%rax)
00000000004005a0 <add@plt>:
4005a0: ff 25 72 0a 20 00 jmpq *0x200a72(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
4005a6: 68 00 00 00 00 pushq $0x0
4005ab: e9 e0 ff ff ff jmpq 400590 <_init+0x28>
00000000004005b0 <printf@plt>:
4005b0: ff 25 6a 0a 20 00 jmpq *<