静态链接分析指引


目标文件想要变成可执行文件,还需要进行链接。本文会介绍链接的基础版:静态链接。

从例子说起

为了说明什么是链接,我们先准备好例子,通过例子分析。

文件a.c:

extern int shared;
int main()
{
    int a = 100;
    swap(&a, &shared);
}

文件b.c:

int shared = 1;

void swap(int* a, int* b){
    int tmp = *a;
    *a =  *b;
    *b = tmp;
}

上述代码中,a.c文件中引用了b.c中的shared变量和swap函数。对a.c文件进行编译,来看看在目标文件中,这两个符号如何表示。

执行命令 gcc -c a.c会得到a.o目标文件,使用readelf -s a.o查看一下目标文件中的符号表:

one

可以看到,这两个符号的Ndx属性是UND,就是undefined的。gcc虽然成功编译了a.c,但是它在目标文件中做了标记,告诉我们哪些符号是没有的。

现在我们来看看b.c的目标文件中的符号: 同样gcc -c b.creadelf -s b.o就可以看到:

two

可以看到,shared和swap是存在于b.o中的。

链接

既然a中的符号在b中,那把a和b合并成一个目标文件,不就解决问题了?

这正是链接的基本目的。

执行下面的命令:

ld a.o b.o -e main -o ab

-e main指定链接后程序的入口是main函数,-o ab表示链接后的文件名。

现在我们来看看文件ab中的符号:

readelf -s ab

static-three

可以看到,main、share、swap三个符号都有,并且不再是UND。

链接规则

链接器是如何合并a.o,b.o的呢?很简单,就是把相同的段进行了合并。

下面我们来验证一下,以text段为例。 a.o的text段:

static-four

该段的大小是2c。

再来看b.o的text段:

static_five

该段的大小是4a。

2c + 4a = 76

由此可以猜测,ab的text段大小是76。

static-six

果然如此,由此验证了链接的基本规则。

VMA和File off

现在我们有段合并的ab文件,ab已经是一个可执行文件。这里就要先澄清容易搞混的概念,就是表示段的地址的两个角度。后面很多涉及地址计算的地方,稍不留神,就容易搞混,引起理解上的困惑。

  • File off

第一个角度,就是各个段在目标文件中的偏移量,File off。

  • VMA(virtual memory address)

第二个角度,就是各个段加载到内存时的虚拟内存地址,即VMA。

现在,我们在ab文件中看一下这两个地址:

static-serven

从中可以看出text段在文件中的偏移和VMA的区别。

从中也可以了解到,链接时,不光是计算出了各个段的File off,同时也计算出了各个段在加载到内存时的VMA,为执行文件做好了准备。

符号的虚拟地址

链接完成后,所有符号的虚拟地址,其实就已经确定了。

因为链接前,各个符号在各自目标文件中的相应段内的偏移是固定的。以swap为例: readelf -s b.o:

eight

swap在b.o的text段的开始位置。

在链接时,a和b的text段会合并,a的text段会在b的text段之前,如下所示:

nine

a的text段大小是2c,如下图所示:

ten

合并后的text段的虚拟内存地址是:4000e8,如下图所示:

eleven

4000e8 + 2c = 4000114

4000114就是b的text段的位置,又因为swap在b的text中的偏移是0,所以可以知道:4000114就是swap的虚拟内存地址。

现在让我们来验证一下:

twelve

正确!其他的符号都是可以用同样的方法计算出来的。

重定位

链接后,我们知道了每个符号的虚拟内存地址。但是还有个问题没有解决。

我们先来看看a.o的指令段中是如何调用swap函数的:

thirteen

e8 就是call指令,它后面的4个字节,是要调用的函数指令相对call下一条指令的偏移量。

这里是00 00 00 00 ,显然这个值是无意义的,因为这是在链接前,还不知道swap的虚拟内存地址呢。

现在我们来看看ab文件中,上面的call指令中的地址部分。

fourteen

红色框中就是那条e8指令,后面的地址值变成了 00 00 00 07, 为啥不是07 00 00 00 ?因为这里是小端表示。

然后call指令下一条指令的地址就是蓝框中的 40010d。

40010d + 07 = 400114

这正好是swap的地址,如图中绿框所示。

这就是重定位。对指令中涉及的符号引用,在链接后,根据符号的虚拟地址,进行调整。

重定位段

那么,链接器是如何知道e8这个call指令中的地址是需要重定位的呢?

答案就是有一个重定位段,专门记录这些信息。我们来看看a.o中的重定位段:

fifteen

我们来看一下这个红框里面的内容。

红框右边的swap表示,swap这个符号在text段中引用了,需要进行重定位。

具体在哪里引用了呢?红框左边的21表示,这个位置在text段中偏移21字节处。

sixteen

从上面的图中可以看出,偏移21刚好是e8 call指令后面的字节,正是swap的地址处。

21这里也叫做重定位入口

在重定位段中,swap的TYPE属性是R_X86_64_PC32,简单理解,这个表示的是,不要在重定位入口处直接填写swap的虚拟内存地址,而要填swap相对call指令下一条指令的偏移量。

R_X86_64_PC32这里也叫做重定位入口类型

可以把重定位入口类型看做函数f,输入是符号的虚拟内存地址,输出是要填到text中符号引用位置的值。

总结

以上,就是静态链接的整个过程。总体看,静态链接,其实就做了两件事。

1、将多个目标文件中的相同段进行合并,为新的段分配虚拟内存地址,记录新的段在新文件中的偏移量,即VMA和File off。同时,计算并记录所有的符号的虚拟内存地址。

2、根据各个目标文件中的重定位段信息,对合并后的text段中涉及到符号引用的位置进行重定位。


原创文章,转载请注明出处,否则拒绝转载!
本文链接:抬头看浏览器地址栏

上篇: ELF文件分析(一)
下篇: IRT模型中的项目信息函数解读