动态链接,动态库是linux的一个重要组成部分,动态库机制允许可执行程序在运行时可以动态的访问外部函数,这样就不需要把所有的函数都编译进每一个可执行程序中,而且可以把动态库加载到任何地址,大大减少了内存中相同代码的内存占用以及使用的方便性,比如libc这种基础函数库。
今天就来探讨一下linux下的这种动态运行机制。
想要了解动态运行机制首先要了解位置无关代码。
何为位置无关代码?-->-fPIC干的事
我们来看个例子:
extern int test();
extern int a;
int fun()
{
a = 10;
test();
return a;
}
将这个例子分别用-fPIC选项和不用该选项编译成.o,如下:
zoronoa@:~/test/dynlink$ gcc-c 1.c -o nopic.o
zoronoa@:~/test/dynlink$ gcc-c -fPIC 1.c -o pic.o
接下来我们来分别看下这两个.o的重定位:
根据上图,我们可以看到采用-fPIC编译的.o中test,a的重定位是不一样的,采用R_386_PLT32和R_386_GOT32来进行重定位;而不采用-fPIC的.o,采用R_386_PC32和R_386_32来进行重定位。位置无关代码就是通过PLT和GOT来实现的,而这两者也是动态链接采用的运行机制,下文将会有介绍。
介绍动态库运行机制:
先来看一个程序,这个程序由两个.c组成,其中一个.c编译成可执行程序,另一个则编成动态库的形式:
main.c
#include<stdio.h>
extern int lib_a;
extern int lib_fun1();
int m_a = 1;
int main()
{
lib_fun1();
printf("from main:%d %d\n",lib_a,m_a);
return 0;
}
lib.c
extern int m_a;
int lib_a = 2;
int lib_fun1()
{
printf("from lib:%d\n",m_a);
return 0;
}
编译:
gcc -fPIC -shared lib.c -o libpic.so
gcc main.c -L. -lpic
运行:
LD_LIBRARY_PATH=. ./a.out
from lib:1
from main:2 1
LD_LIBRARY_PATH用来指定动态库路径,这里不再赘述
在介绍运行机制之前首先需要了解几个概念:
1.plt – procedure linkage table(过程链接表)
2.got –global offset table(全局偏移表)
每一个外部符号都在got中有相应的条目,如果该符号是函数则在plt中也有相应的条目,并且plt中的条目与got中的条目一一对应,但是got中由于有全局变量的存在,一般都比plt表中多一些项,这些项用来记录变量的地址。
plt机制介绍如下:
plt表的形式如下:
0x08048480 <+0>: jmp *0x804a010
0x08048486 <+6>: push $0x8
0x0804848b <+11>: jmp 0x8048460
当一个函数第一次被调用时,0x804a010地址处存储的是其下一条指令的地址,0x804a010这个地址就是got表对应的plt表的项,用来存放函数地址,也就是上述例子中的0x08048486,所以在第一次调用函数时,该条指令实际没有起到任何作用;第二条指令push是将该plt在本模块中的标号压栈,用于后续的地址回写。然后跳转到0x8048460的地方,该地方的代码一般形式如下:
8048460: ff 35 04 a0 04 08 pushl 0x804a004
8048466: ff 25 08 a0 04 08 jmp *0x804a008
804846c: 00 00 add %al,(%eax)
第一条指令是将本模块的ID压栈,方便后续地址回写,第二条jmp指令,跳转到0x804a008地址中存放的对应的内容中去,该内容中存放的是_dl_runtime_resolve函数的地址,所以这条jmp指令的意思就是跳转到_dl_runtime_resolve函数,而这个函数就是解析函数地址的地方,他会进而调用fixup函数,进而解析到你想要跳转的函数的地址,执行完该函数之后,并把这个函数的地址填写到0x804a010中,还记得这个地址吗??对,就是上文讲到的无用的那个地址,将目的函数的地址填入该地址之后,jmp* 0x804a010指令就能够直接跳到目标函数,这样当该函数被再次调用时,就可以通过jmp*0x804a010指令跳转过去,所以只有第一次调用到某个函数时,需要使用_dl_runtime_resolve函数解析函数地址,后续调用都可以直接跳转。
so,为什么要这么做呢??
linux下有无数的动态库在运行,所以这些动态库加载的地址是无法事先规定的,因此动态库的加载地址是不确定的,这也就是为什么要使用位置无关代码的原因。因为加载地址不确定,因此如果使用位置相关代码,这就与加载地址不固定相违背了。因为加载地址不确定,所以第一次调用某个函数时,需要通过plt机制来解析某个函数的地址,然后填入相应的got表中,后续都从got表中直接解析,这样就完美解决了加载地址不固定的问题。
下面通过调试具体介绍下动态库运行机制:
1.函数调用
动态库中外部函数调用并不是直接调用,而是先调用到function@plt中,
首先使用gdb调试,打断点在main函数
我们可以看到并不是直接调用lib_fun1,而是先调用lib_fun1@plt,接下来看下lib_fun1@plt的代码:
接下来我们看下0x804a008里存的是什么
里面存的是_dl_runtime_resolve函数的地址。通过该函数解析到lib_fun1函数的地址。继续运行,运行完这些函数之后,我们再来看下0x804a010中存放的内容:
已经是lib_fun1函数的地址了。
2.数据访问
全局变量的地址,在动态库被加载之后,会将相应的全局变量地址填入.got表中。
现在一般把整个got表分为两部分:.got和.got.plt
.got表中记录的是全局变量的地址
.got.plt中记录的是函数的地址,但是.got.plt的前面3项记录的是特殊的值
.got.plt[0]-->.dynamic断的地址
.got.plt[1]-->动态链接器的标识
.got.plt[2]-->_dl_runtime_resolve函数的地址