linux动态链接程序运行机制

动态链接,动态库是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编译的.otesta的重定位是不一样的,采用R_386_PLT32R_386_GOT32来进行重定位;而不采用-fPIC.o,采用R_386_PC32R_386_32来进行重定位。位置无关代码就是通过PLTGOT来实现的,而这两者也是动态链接采用的运行机制,下文将会有介绍。


介绍动态库运行机制:

先来看一个程序,这个程序由两个.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函数的地址


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值