Linux下的动态连接库及其实现机制

Linux与Windows的动态连接库概念相似,但是实现机制不同。它引入了GOT表和PLT表的概念,综合使用了多种重定位项,实现了"浮动代码",达到了更好的共享性能。本文对这些技术逐一进行了详细讨论。 

本文着重讨论x86体系结构,这是因为

(1)运行Linux的各种体系结构中,以x86最为普及;

(2)该体系结构上的Windows操作系统广为人知,由此可以较容易的理解Linux的类似概念; 

下表列出了Windows与Linux的近义词,文中将不加以区分: 

Windows
Linux

动态连接库(DLL) Shared Object 
目标文件(.obj) 文件名结尾常是
.o 
可执行文件(.exe) Executable(文件名无特定标志) 
连接器(link.exe) Linker
Editor (ld) 
加载器(exec/loader) Dynamic Linker
(ld-linux.so) 
段(segment)
节(section) 

一些关键字在本文中有特定含义,需要澄清: 

编译单元:一个C语言源文件,经过编译后将生成一个目标文件 
运行模块:一个动态连接库或者一个可执行文件。简称为模块 
自动变量、函数:C语言auto关键字修饰的对象 
静态变量、函数:C语言static关键字修饰的对象 
全局变量、函数:C语言extern关键字修饰的对象 

1
动态连接库的优点 

程序编制一般需经编辑、编译、连接、加载和运行几个步骤。由于一些公用代码需要反复使用,就把它们预先编译成目标文件并保存在"库"中。当它与用户程序的目标文件连接时,连接器得从库中选取用户程序需要的代码,然后复制到生成的可执行文件中。这种库称为静态库,其特点是可执行文件中包含了库代码的一份完整拷贝。显然,当静态库被多个程序使用时,磁盘上、内存中都是多份冗余拷贝。 

而使用动态连接库就克服了这个缺陷。当它与用户程序的目标文件连接时,连接器只是软件开发网
www.mscto.com
作上标记,说明程序需要该动态连接库,而不真的把库代码复制到可执行文件中;仅当可执行文件运行时,加载器根据这个标记,检查该库是否已经被其它可执行文件加载进内存。如果已存在于内存中,不用再从磁盘上加载,只要共享内存中已有的代码即可。这样磁盘、内存中始终只有一份代码,较静态库为优。 

2
Linux动态连接库的重要特点:浮动代码 

在Windows中,连接生成动态连接库时要指定一个首地址。应用程序运行时,加载器将尽可能把动态连接库装入到该地址;如果地址已被占用,该动态连接库只能被加载到其它地址空间内,这时就要对库中的代码和数据进行修补,或叫做重定位。如此一来,库的多个实例在内存中经过重定位后,彼此将不尽相同,自然不再能共享了。为了避免这个缺陷,Windows自带的库都指定了互不重叠的地址,尽管如此,其它软件厂商的产品仍然不可避免的使用重叠地址,由此部分丧失了使用动态连接库的好处。 

在Linux中,为了达到更好的共享性能,使用了与Windows不一样的策略:浮动代码(Position
Independent
Code,简称PIC)。具体说,使用的转移指令都是相对于当前程序计数器(IP)的偏移量;代码中引用变量、函数的地址都是相对于某个基地址的偏移量。总之,从不引用一个绝对地址。这样,动态连接库无论被加载到什么地址空间,不用修补代码就可以正常工作。既然只有一份代码,就容易实现共享了。 

 
值得指出,此处所指的共享,是指为了节省存储器,多个进程使用动态连接库代码段、只读数据段在内存中的唯一映像;另一种常用的共享定义,是指多个进程对同一段(可能是动态分配的)存储区进行读写,实现进程间通信(IPC)。后一种共享定义与本文无可执行文件运行时,加载器根据这个标记,检查该库是否已经被其它可执行文件加载进内存。如果已存在于内存中,不用再从磁盘上加载,只要共享内存中已有的代码即可。这样磁盘、内存中始终只有一份代码,较静态库为优。 

2
Linux动态连接库的重要特点:浮动代码 

在Windows中,连接生成动态连接库时要指定一个首地址。应用程序运行时,加载器将尽可能把动态连接库装入到该地址;如果地址已被占用,该动态连接库只能被加载到其它地址空间内,这时就要对库中的代码和数据进行修补,或叫做重定位。如此一来,库的多个实例在内存中经过重定位后,彼此将不尽相同,自然不再能共享了。为了避免这个缺陷,Windows自带的库都指定了互不重叠的地址,尽管如此,其它软件厂商的产品仍然不可避免的使用重叠地址,由此部分丧失了使用动态连接库的好处。 

在Linux中,为了达到更好的共享性能,使用了与Windows不一样的策略:浮动代码(Position
Independent
Code,简称PIC)。具体说,使用的转移指令都是相对于当前程序计数器(IP)的偏移量;代码中引用变量、函数的地址都是相对于某个基地址的偏移量。总之,从不引用一个绝对地址。这样,动态连接库无论被加载到什么地址空间,不用修补代码就可以正常工作。既然只有一份代码,就容易实现共享了。 

值得指出,此处所指的共享,是指为了节省存储器,多个进程使用动态连接库代码段、只读数据段在内存中的唯一映像;另一种常用的共享定义,是指多个进程对同一段(可能是动态分配的)存储区进行读写,实现进程间通信(IPC)。后一种共享定义与本文无关。 

3
Linux动态连接库的实现机制:重定位 

3.1
重定位概述 

浮动代码通过重定位操作得以实现。而重定位可以按多种标准进行分类: 

--
按发生的地点,可分成对代码段(.text)重定位和对数据段(.data)重定位。 

--
按发生的时间,可分成连接时重定位和加载时重定位(加载时重定位也称为动态重定位)。但这两步并不总是必不可少的。例如,要实现浮动代码就不能对代码段进行动态重定位,这时采取的办法是,把需要动态重定位的项搬到数据段中去,然后在代码段中引用这些项。 

--
按重定位项引用的对象,可分成数据引用和函数引用。如果引用的是静态数据或静态函数,连接器会优化生成的代码,去掉动态重定位项。 

--
从字面上讲,
x86体系结构上的Linux使用了多种重定位方式,名字前缀以"R_386_",后面分别接:32、GOT32、PLT32、COPY、GLOB_DAT、JMP_SLOT、RELATIVE、GOTOFF、GOTPC。每种方式都有特定的含义。 
以上几种分类中最重要的是按地点分类。而下文也将以它为主线,逐一介绍各种重定位项。首先,引入两个关键概念:GOT表和PLT表。 

3.2
GOT表 

GOT(Global Offset
Table)表中每一项都是本运行模块要引用的一个全局变量或函数的地址。可以用GOT表来间接引用全局变量、函数,也可以把GOT表的首地址作为一个基准,用相对于该基准的偏移量来引用静态变量、静态函数。由于加载器不会把运行模块加载到固定地址,在不同进程的地址空间中,各运行模块的绝对地址、相对位置都不同。这种不同反映到GOT表上,就是每个进程的每个运行模块都有独立的GOT表,所以进程间不能共享GOT表。

在x86体系结构上,本运行模块的GOT表首地址始终保存在離寄存器中。编译器在每个函数入口处都生成一小段代码,用来初始化離寄存器。这一步是必要的,否则,如果对该函数的调用来自另一运行模块,離中就是调用者模块的GOT表地址;不重新初始化離就用来引用全局变量和函数,当然出错。 

3.3
PLT表 

PLT(Procedure Linkage
Table)表每一项都是一小段代码,对应于本运行模块要引用的一个全局函数。以对函数fun的调用为例,PLT中代码片断如下: 

.PLTfun:
jmp *[email protected](離) 
pushl $offset 
jmp
[email protected] 

其中引用的GOT表项被加载器初始化为下一条指令(pushl)的地址,那么该jmp指令相当于nop空指令。 

用户程序中对fun的直接调用经编译连接后生成一条call [email protected]指令,这是一条相对跳转指令(满足浮动代码的要求!),跳到.PLTfun。如果这是本运行模块中第一次调用该函数,此处的jmp等于一个空指令,继续往下执行,接着就跳到.PLT0。该PLT项保留给编译器生成的额外代码,会把程序流程引入到加载器中去。加载器计算fun的实际入口地址,填入[email protected]表项。图示如下: 

user
program 
-------------- 
call
[email protected] 


DLL PLT table
loader 
-------------- -------------- -----------------------

fun: <-- jmp*[email protected] --> change GOT entry
from 
| $loader to $fun, 
v then jump
to there 
GOT
table 
-------------- 
[email protected]:$loader 

第一次调用以后,GOT表项已指向函数的正确入口。以后再有对该函数的调用,跳到PLT
表后,不再进入加载器,直接跳进函数正确入口了。从性能上分析,只有第一次调用才要加载器作一些额外处理,这是完全可以容忍的。还可以看出,加载时不用对相对跳转的代码进行修补,所以整个代码段都能在进程间共享。 

熟悉Windows的程序员很容易注意到,GOT表、PLT表与Windows中的引入表(Import)有类似之处。其它对应关系还有:
Linux的version script与Windows的.DEF文件;Linux 的dynamic symbols
section与Windows的输出表(Export)。不再举更多例子了。 

3.4
代码段重定位 

需要说明,由浮动代码的要求,代码段内不应该存在重定位项。此处只是借用了"在代码段中"这个短语,实际的重定位项还是位于数据段的GOT表内。尽管如此,它与3.5节"数据段中的重定位"的区别是很明显的。 

a)
装载GOT表首地址 

使用GOT表当然事先要知道它的首地址,然而该首地址会随运行模块被加载的首地址不同而不同。Linux使用了一个技巧在运行时求出正确的GOT表首地址。代码片断如下,紧接其后列出的是对应的目标文件(.o)与动态连接库(.so)中的重定位项类型: 

call
L1 
L1: popl 離 
addl $GOT [.-.L1],
離 
.o: R_386_GOTPC 
.so:
NULL 

如前所述,该代码片断存在于每个函数的入口处。程序第一句把当前程序计数器(IP)值推进堆栈,第二句又把它从堆栈中弹出来,结果相当于movl
%eip,
離,只不过合法的x86指令集中不允许%eip作为操作数而已。然后第三句把離加上一个GOT表与IP值的差,这个差值是个与动态连接库加载首地址无关的常数,在连接时即可求出。整个过程用类C语言描述如下: 


= %eip; 
離 = ($GOT -
%eip) 

至此離等于GOT表首地址。 

上述过程是编译、连接相合作的结果。编译器生成目标文件时,因为此时还不存在GOT表(每个运行模块有一个GOT表,一个PLT表,由连接器生成),所以暂时不能计算GOT表与当前IP间的差值,仅在第三句处设上一个R_386_GOTPC重定位标记而已。然后进行连接。连接器注意到GOTPC重定位项,于是计算GOT与此处IP的差值,作为addl指令的立即寻址方式操作数。以后再也不需要重定位了。

b)
引用变量、函数地址 

当引用的是静态变量、静态函数或字符串常量时,使用R_386_GOTOFF重定位方式。它与GOTPC重定位方式很相似,同样首先由编译器在目标文件中设上重定位标记,然后连接器计算GOT表与被引用元素首地址的差值,作为leal指令的变址寻址方式操作数。代码片断如下: 

leal
[email protected](離), 陎 
.o:
R_386_GOTOFF 
.so:
NULL 

当引用的是全局变量、全局函数时,编译器会在目标文件中设上一个R_386_GOT32重定位标记。连接器会在GOT表中保留一项,注上R_386_GLOB_DAT重定位标记,用于加载器填写被引用元素的实际地址。连接器还要计算该保留项在GOT表中的偏移,作为movl指令的变址寻址方式操作数。代码片断如下: 

movl
[email protected](離), 陎 
.o: R_386_GOT32 
.so:
R_386_GLOB_DAT 

需要指出,引用全局函数时,由GOT表读出不是全局函数的实际入口地址,而是该函数在PLT表中的入口.PLTfun(参见3.3节)。这样,无论直接调用,还是先取得函数地址再间接调用,程序流程都会转入PLT表,进而把控制权转移给加载器。加载器就是利用这个机会进行动态连接的。 

c)
直接调用函数 

如前所述,浮动代码中的函数调用语句会编译成相对跳转指令。首先编译器会在目标文件中设上一个R_386_PLT32重定位标记,然后视静态函数、全局函数不同而连接过程也有所不同。 

如果是静态函数,调用一定来自同一运行模块,调用点相对于函数入口点的偏移量在连接时就可计算出来,作为call指令的相对当前IP偏移跳转操作数,由此直接进入函数入口,不用加载器操心。相关代码片断如下: 

call
[email protected] 
.o: R_386_PLT32 
.so:
NULL 
http://blog.csdn.net/chaolumon/article/details/2992158

如果是全局函数,连接器将生成到.PLTfun的相对跳转指令,之后就如3.3节所述,对全局函数的第一次调用会把程序流程转到加载器中去,然后计算函数的入口地址,填充[email protected]表项。这称为R_386_JMP_SLOT重定位方式。相关代码片断如下: 

call
[email protected] 
.o: R_386_PLT32 
.so:
R_386_JMP_SLOT 

如此一来,一个全局函数可能有多至两个重定位项。一个是必需的JMP_SLOT重定位项,加载器把它指向真正的函数入口;另一个是GLOB_DAT重定位项,加载器把它指向PLT表中的代码片断。取函数地址时,取得的总是GLOB_DAT重定位项的值,也就是指向.PLTfun,而不是真正的函数入口。 

进一步考虑这样一个问题:两个动态连接库,取同一个全局函数的地址,两个结果进行比较。由前面的讨论可知,两个结果都没有指向函数的真正入口,而是分别指向两个不同的PLT表。简单进行比较,会得出"不相等"的结论,显然不正确,所以要特殊处理。 

3.5
数据段重定位 

在数据段中的重定位是指对指针类型的静态变量、全局变量进行初始化。它与代码段中的重定位比较起来至少有以下明显不同:一、在用户程序获得控制权(main函数开始执行)之前就要全部完成;二、不经过GOT表间接寻址,这是因为此时離中还没有正确的GOT表首地址;三、直接修改数据段,而代码段重定位时不能修改代码段。 

如果引用的是静态变量、函数、串常量,编译器会在目标文件中设上R_386_32重定位标记,并计算被引用变量、函数相对于所在段首地址的偏移量。连接器把它改成R_386_RELATIVE重定位标记,计算它相对于动态连接库首地址(通常为零)的偏移。加载器会把运行模块真正的首地址(不为零)与该偏移量相加,结果用来初始化指针变量。代码片断如下: 

.section
.rodata 
.LC0: .string
"Ok/n" 
.data 
p: .long
.LC0 
.o: R_386_32 w/ section 
.so:
R_386_RELATIVE 

如果引用的是全局变量、函数,编译器同样设上R_386_32重定位标记,并且记录引用的符号名字。连接器不必动作。最后加载器查找被引用符号,结果用来初始化指针变量。

对于全局函数,查找的结果仍然是函数在PLT表中的代码片断,而不是实际入口。这与前面引用全局函数的讨论相同。代码片断如下: 

.data 
p:
.long printf 
.o: R_386_32 w/
symbol 
.so: R_386_32 w/
symbol 

3.6
总结 

下表给出了前面讨论得到的全部结果: 

.o
.so 
------------------------------------------------------------ 
|装载GOT表首地址
R_386_GOTPC
NULL 
代码段|----------------------------------------------------- 
重定位|引用变量函数地址
静态 R_386_GOTOFF NULL 
| 全局 R_386_GOT32
R_386_GLOB_DAT 
|----------------------------------------------------- 
|直接调用函数
静态 R_386_PLT32 NULL 
| 全局 R_386_PLT32
R_386_JMP_SLOT 
------|----------------------------------------------------- 
数据段|引用变量函数地址
静态 R_386_32 w/sec R_386_RELATIVE 
重定位| 全局 R_386_32 w/sym
R_386_32
w/sym 
------------------------------------------------------------ 

4
结束语 

Windows使用PE文件格式,Linux使用ELF文件格式,这是两种动态连接库不同的根源。本文从ELF规范出发,深入讨论了Linux动态连接库的具体实现,目的在于进一步推广Linux的研究与应用。 

5
附录:Linux汇编程序语法 

x86体系结构上的Linux汇编器兼容于AT&T System
V/386汇编器的语法,与常见的Intel 语法颇有不同,如下表: 

AT&T
Intel 
常数 前缀$:pushl $4 push 4 
寄存器
前缀%:離 ebx 
跳转指令(绝对地址) 前缀*:jmp
*fun 
跳转指令(相对偏移) 无标记:jmp
fun 
目的、源操作数的顺序 源在前:movl $4,陎 目的在前:mov
eax,4 
操作数尺寸 后缀b、w、l:movl 修饰符byte
ptr等等 
变址寻址 [base disp]
disp(base) 

参考文献 

Executable
and Linking Format Spec v1.2, TIS Committee,
1995 
http://x86.ddj.com/ftp/manuals/tools/elf.pdf 
GNU
Project (gcc, libc, binutils), Free Software Foundation, Inc.,
1999 
http://www.gnu.org/software/ 
Solaris
2.5 Linker and Libraries Guide, Sun Microsystems Inc.,
1999 
http://docs.sun.com/ 
ftp://192.18.99.138/802-1955/802-1955.pdf 
SVR4
ABI x86 Supplement, The Santa Cruz Operation, Inc.,
1999 
http://www.sco.com/developer/devspecs/abx86-4.pdf

ELF: From The Programmer‘s Perspective, H J Lu,
1995 
http://metalab.unc.edu/pub/Linux/GCC/elf.ps.gz 
[6]
Using ld: The GNU linker, S Chamberlain, Cygnus Support,
1994 
http://www.gnu.org/manual/ld-2.9.1/ps/ld.ps.gz

Linux下的动态连接库及其实现机制,布布扣,bubuko.com

时间: 2024-10-14 05:29:23

Linux下的动态连接库及其实现机制的相关文章

linux下的静态连接库和动态链接库

对linux的静态连接库和动态链接库分不清楚,在看了一篇博文后,现在想做个自己的总结,以加深印象: 1.库的基本概念: 库是可执行代码的二进制形式,其可以被调入操作系统调入内存进行执行. 在window和linux系统,都存在各自的库,但是两种系统的库并不能兼容,因为它们的编译器,连接器,汇编器都是不相同的. 在windows下,静态连接库的后缀是.lib;动态链接库的后缀是.dll 在linux系统下,静态链接库的后缀是.a;动态链接库的后缀是.so 2.静态连接库和动态链接库的命名: 静态连

Linux 静态链接库和动态连接库

(0)文件夹 VMware 下安装Ubuntu的吐血经历 零基础学习Shell编程 Linux下的makefile的妙用 Linux调试神器 -- gdb 十分钟学会Python的基本类型 Linux 静态链接库和动态连接库 一:静态链接库的应用  三步走~~~ ##g++ -c StaticMath.cpp ##ar -crv libstaticmath.a StaticMath.o ##g++ -o run test_a.cpp -L. -lstaticmath #[@sjs_37_33 l

linux下so动态库一些不为人知的秘密

linux 下有动态库和静态库,动态库以.so为扩展名,静态库以.a为扩展名.二者都使用广泛.本文主要讲动态库方面知识.    基本上每一个linux 程序都至少会有一个动态库,查看某个程序使用了那些动态库,使用ldd命令查看 # ldd /bin/ls linux-vdso.so.1 => (0x00007fff597ff000) libselinux.so.1 => /lib64/libselinux.so.1 (0x00000036c2e00000) librt.so.1 => /

linux下so动态库一些不为人知的秘密(转)

linux 下有动态库和静态库,动态库以.so为扩展名,静态库以.a为扩展名.二者都使用广泛.本文主要讲动态库方面知识.基本上每一个linux 程序都至少会有一个动态库,查看某个程序使用了那些动态库,使用ldd命令查看 # ldd /bin/ls 使用 ldd -u /bin/ls 查看不需要链接的so 大家知不知道linux从程序(program或对象)变成进程(process或进程),要经过哪些步骤呢,这里如果详细的说,估计要另开一篇文章.简单的说分三步:    1.fork进程,在内核创建

Linux下的动态库与静态库

2019-09-25 关键字:生成库.静态库引用.动态库引用 在 C 开发中,“库”是一个经常听到的名词. 所谓的库其实就是一个二进制文件.这个二进制文件的内容是可被其它C程序调用执行的函数.换句话说,库就是一组C代码的打包形式而已,打包是指将源代码以库的形式编译而生成的文件. 不过即使它只是源代码的打包,它也仍然是有系统专有性的,即不同系统下编译出来的库并不能互相兼容使用. C库可分为动态库与静态库两种.引用了外部库的应用程序在编译时会在”链接期“处理库与程序源码之间的关系. 1.静态库 静态

linux(debian) 安装软件,缺少动态连接库.so

有以下几种提示: 1.缺少动态连接库.so-cannot open shared object file:No such file or directory 2.缺少动态连接库.so.0-cannot open shared object file:No such file or directory 3.缺少动态连接库.so.1-cannot open shared object file:No such file or directory 可以直接: # sudo ldconfig 再编译,如

动态连接库 VS 静态连接库

一.静态库与动态库 通常情况下,对函数库的链接是放在编译时期(compile time)完成的.所有相关的对象文件(object file)与牵涉到的函数库(library)被链接合成一个可执行文件(executable file).程序在运行时,与函数库再无瓜葛,因为所有需要的函数已拷贝到自己门下.所以这些函数库被成为静态库(static libaray),通常文件名为"libxxx.a"的形式. 其实,我们也可以把对一些库函数的链接载入推迟到程序运行时期(runtime).这就是如

Qt动态连接库/静态连接库创建与使用,QLibrary动态加载库

版权声明:若无来源注明,Techie亮博客文章均为原创. 转载请以链接形式标明本文标题和地址: 本文标题:Qt动态连接库/静态连接库创建与使用,QLibrary动态加载库     本文地址:http://techieliang.com/2017/12/680/ 文章目录 1. 动态连接库创建与使用  1.1. 项目创建  1.2. 调用-使用.h文件 2. 静态库创建及使用  2.1. 创建  2.2. 使用 3. QLibrary动态加载动态库  3.1. 介绍  3.2. 范例  3.3.

linux下编译安装boost库

转载:http://www.cnblogs.com/oloroso/p/4632848.html linux下编译安装boost库 linux下编译安装boost库 1.下载并解压boost 1.58 源代码 下载 解压 2.运行bootstrap.sh 3.使用b2进行构建 构建成功的提示 4.安装boost库到指定目录 5.测试一下 代码 编译运行 先看一下系统环境 Linux o-pc 3.19.0-22-generic #22-Ubuntu SMP Tue Jun 16 17:15:15