ELF--动态链接

对前面add.c稍作修改,

#include <stdio.h>
int add_count = 0;

extern int sum_count;
extern void print_log(const char *ident, int line);
int add2(int num1, int num2)
{
  int result= 0;
  result = num1 + num2;

     add_count ++;
  return result;
}
int sum(int num1, int num2, int num3)
{

     sum_count++;

  int sum = add2(num1, num2);
  return add2(sum, num3);
}

编译成so gcc -shared -fPIC add.c -o libadd.so。查看ELF program header.

和可执行文件类似,只是LOAD的virtAddr起始地址是0x0.说明so在load到内存中时地址是不确定的。

当系统在装载某个包含so的进程时,也需要装载so。装载器会更具当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给so。

当进程在调用so中的函数和数据时,需要知道这些函数的绝对地址.我们知道函数sum的相对地址0x568,那如何知道他的绝对地址呢。

假设so被装载的目标地址为0x10000000,那么sum的绝对地址为0x10000568.

 

为使so能够被多个进程link,并节省内存。so需要分离出地址无关代码。主要是希望将so指令部分不要因为装载地址的改变而改变。所以基本想法是将指令中需要被修改的部分分理出来,更数据放在一起。而将指令中不变的部分保持不变,各个进程共享。各个进程各自拥有so数据的副本。

 

对于add.c文件中内部的函数调用和数据访问,是地址无关的。

对于add.c中文件间函数调用和数据访问,是通过GOT来实现的。当程序要访问变量sum_count或调用print_log是,首先找到GOT,根据GOT中对应项找到地址。在so装载时,会查找sum_count和print_log的地址,并填充到GOT中,每一项对应4字节地址。具体sum_count,printf_log对应于GOT的哪一项,可以通过重定位表来查看。

GOT是放在数据段,所以它是可以在so装载时被修改的,并且每个进程都拥有副本。

查看libadd.so的section header可以看到.got的起始地址0x1fe8:

查看libadd.so的重定位表:readelf -r libadd.so. (rel.dyn 和.rel.plt分别相当于.rel.data和.rel.plt. .rel.dyn是对数据引用的修正,修正的位置位于.got。.rel.plt是对函数引用的修正,修正的位置位于.got.plt)

引用的外部变量sum_count正好在.got中。

而print_log则在.got.plt中。这是由于编译时采用了延迟绑定。

为什么要使用延迟绑定呢?在动态链接下,模块间有大量函数调用,在程序开始执行时,动态链接器会耗费大量时间去解决函数符号的查找和重定位。但是实际上程序运行时可能只有少数函数才回用到,所以会delay程序启动时间。因此ELF采用延迟绑定,只有函数第一次用到才会绑定。

 采用延迟绑定后,当调用外部函数时,并不直接通过.got跳转,而是通过.got.plt跳转。每个外部函数都在.got.plt中有一项。比如print_log在.got.plt是printf_log@plt.

.got.plt的前三项是.dynamic地址,本模块的ID,_dl_runtime_resolve的地址。_dl_runtime_resolve是动态连接器函数,用来完成符号解析和重定位,将函数的真正地址放到.got表对用的项中,如print_log@GOT.

反汇编objdump -d libadd.so

在调用print_log时,跳转到print_log@plt

 

在.got.plt中前三项保留,jmp到表中第4项0x0c,push 0x0,0x0代表print_log在rel.plt中的下标,然后跳转到450位置。

在450位置,push .got.plt第二项模块ID,然后跳转到第三项_dl_runtime_resolve完成符号解析和重定位。

 

posted @ 2016-12-11 16:50  fellow_jing  阅读(628)  评论(0编辑  收藏  举报