系列文章
【链接、装载与库】程序的编译过程
【链接、装载与库】动态链接相关结构
【链接、装载与库】动态链接初体验
前言
动态链接是在运行时将各个模块的代码进行整合,静态链接是在编译时对各个模块的代码进行整合。
而运行时整合使得动态链接相对消耗了一些性能,为了减小这点带来的影响,动态链接中引入了延迟绑定(PLT)
,其基本思想是当函数第一次被用到时才进行绑定(符号查找、重定位等),如果没有用到则不进行绑定,接下来通过一个示例对延迟绑定进行分析。
正文
首先创建5个文件liba.c、liba.h、libb.c、libb.h、main.c。
内容分别为:
liba.c
int funca(int i)
{
return i + 1;
}
liba.h
#ifndef LIBA_H
#define LIBA_H
int funca(int i);
#endif
libb.c
int funcb(int i)
{
return i + 2;
}
libb.h
#ifndef LIBB_H
#define LIBB_H
int funcb(int i);
#endif
main.c
#include <stdio.h>
#include "liba.h"
#include "libb.h"
int main()
{
int a;
int b;
a = funca(1);
b = funcb(1);
printf("a is %d, b is %d\n", a, b);
}
输入如下指令分别生成liba.so、libb.so两个动态共享对象:
gcc -fPIC -shared -o liba.so liba.c
gcc -fPIC -shared -o libb.so libb.c
然后输入如下指令对main.c进行编译输出可执行文件main:
gcc -o main main.c ./liba.so ./libb.so
此时文件夹中的内容如下:
运行可执行文件main得到如下结果:
接下来对可执行文件main进行分析。
输入如下指令得到main的反汇编代码:
objdump -S main
其中main函数部分内容如下:
可以看到
调用funca函数部分代码为callq 660 <funca@plt>
调用funcb函数部分代码为callq 680 <funcb@plt>
调用printf函数部分代码为callq 670 <printf@plt>
找到反汇编代码中0x660
、0x670
、0x680
部分的代码,内容如下:
以funcb为例,可以看到调转到0x680
地址后执行的代码为去0x200fd0
地址取出一个数值,将其赋值给PC指针继续执行。
要想查看0x200fd0
地址处的内容,输入如下指令,得到main的二进制形式:
objdump -s main
找到我们想要的内容如下:
这部分叫做全局偏移表(GOT)
可以看到0x200fd0
地址处的内容为86060000 00000000
进行大小端转换之后就是0x686
,而0x686
处的代码则是:
也就是压栈一个数值2,然后执行0x650
处的代码,0x650
处代码如下:
可以看到这部分执行的代码为压栈GOT表第二项数据,跳转到GOT表第三项存储的函数地址开始执行。
GOT表的内容如下:
序号 | 地址 | 含义 |
---|---|---|
第一项 | 0x200fa8 | .dynamic段地址 |
第二项 | 0x200fb0 | Module ID (模块编号) |
第三项 | 0x200fb8 | _dl_runtime_resolve() |
第四项 | 0x200fc0 | funca函数 |
第五项 | 0x200fc8 | printf函数 |
第六项 | 0x200fd0 | funcb函数 |
调用_dl_runtime_resolve
函数用于把funcb的真实地址填写到GOT中,整个过程如下:
这是第一次调用funcb函数的时候,之后再调用funcb函数时,由于GOT表已经写入了funcb函数的真实地址,所以执行过程如下:
这样就实现了第一次调用funcb函数时完成了动态链接,之后就可以直接调用的效果。
在调用_dl_runtime_resolve
时要压入funcb函数的符号下标。
所谓的符号下标是.rela.plt
段中的下标,输入如下指令:
readelf -a main
找到.rela.plt
段的内容如下:
第一列表示函数在GOT表中的地址,最后一列表示函数的符号名。
_dl_runtime_resolve
获取符号下标之后,就可以通过符号名去别的模块查找函数地址,找到之后将函数地址填写到第一列存储的GOT表地址中,也就完成了动态链接。