Linux内核源码阅读系列(6)-链接3

上一篇举例的时候那个例子并不是很恰当,因为用局部变量的生存周期来解释的话,也是行的通的。那个例子只能说是又添加了一种新的解释而已。要找一个比较妖的还挺难,就直接抄书了:

/* foo5.c */
#include <stdio.h>
void f(void);

int x = 15213;
int y = 15212;

int main()
{
	f();
	printf("x = 0x%x y = 0x%x n",
		x, y);
	return 0;
}
/* bar5.c */
double x;

void f()
{
	x = -0.0;
	^^^---链接器在处理这个符号x的时候,选择了,foo5.c文件中定义的
	      “强符号”int型的x,也就是解释为foo5.c中的x的内存位置写入
	      在这里定义的double型的值。
}

在IA32/Linux机器上,double型是8个字节,而int型是4字节;因此,这里将用double型的”-0.0″覆盖foo5.c中的x和y的内存位置,于是理所当然的程序出了一个意想不到的意外,而且这类错误是不容易被发现。

静态链接库

静态链接库就是把一堆相关的.o文件使用ar工具打包。最著名的静态连接库恐怕就是libc.a了。这是C语言标准库的静态链接版本。程序跟静态连接库链接的时候一般采用如下形式的命令:

$ gcc -O2 -c main.c
$ gcc -static -o swap_sample main.o libswap.a

程序在跟静态库链接的时候,首先链接器会按照命令行输入的从前往后的方向对可重定位文件进行符号解析,找出在模块内部未定义的符号,并将在其后找到包含这个符号定义的那个模块的代码和数据拷贝进入将生成的可执行目标文件,并对其中的符号进行重定位,如果这些未定义的符号全部解决,则链接成功并输出可执行文件,否则链接器会报错。

重定位

重定位就是确定一个对象(包括代码和数据)在存储器中的位置的过程。关于每个需要重定位的符号,链接器有两个方面事情要做,1. 对模块中的符号的定义(definition)进行定位,这个工作主要是合并各个输入模块的代码和数据节,并给每个节和每个符号定义赋以新的存储器地址; 2. 将模块中的引用(reference)指向正确的符号定义位置,这个工作主要依靠“重定位表目”完成,也就是上一篇中提到的实例中的rel.textrel.data节的总览中提到的”R_386_PC32“和”R_386_32“等附带有重定位类型的表目。

重定位表目可以用下面的包括下面代码展示的内容:

typedef struct {
	int offset;	/* 需要被重定位的“引用”在所在节中的偏移量 */
	int symbol:24,	/* 这个引用应该指向的符号 */
	    type:8;	/* 重定位类型 */
}

最重要的两类重定位类型就是”R_386_PC32″和”R_386_32″。

R_386_PC32: 这个类型的重定位信息主要控制的是程序在执行是的跳转。重定位一个使用32位PC(program counter)相关的地址引用。当CPU执行使用PC相关寻址的指令时,它就将在代码中编码的32位值加上PC当前运行时的值,得到有效地址,而PC值通常默认是存储器中的下一条指令的地址。

R_386_32:重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32值作为有效地址。————《深入理解计算机系统》

重定位符号应用的算法伪代码如下:

foreach section s {
	foreach relocation entry r {
		refptr = s + r.offset; /* 指向需要被重定位的引用的指针 */

	/* relocate a PC-relative reference */
	if (r.type = R_386_PC32) {
		refaddr = ADDR(s) + r.offset; /* 引用的运行时地址 */
		*refptr = (unsigned) ((ADDR(r.symbol) + *refptr - refaddr);
	}

	/* relocate an absolute reference */
	if (r.type == R_386_32)
		*refptr = (unsigned) (ADDR(r.symbol) + *refptr);
	}
}

R_386_PC32

在没有跳转的情况下,众所周知程序是按照从上到下的顺序顺序执行的,而这个事实在机器语言级别的直接反应就是PC的值默认情况下都会指向(经过call指令的计算后)当前执行指令的邻近下一条指令的地址。IA32结构中,一条指令的大小是4字节,所以,call指令的默认参数总是”-4″(0xfffffc),以便操作数于PC值相加时,跳转到临近的下一条指令。也就是说上面伪代码中的refptr在PC相关的地址引用中,初始值是”-4″。那么,如果程序发生非顺序执行的跳转,其重点因素就是要给call等类似的指令一个正确的操作数。这个操作数与PC中的值进行计算之后可以跳转到相应的对象(代码)保证程序的正确执行。”R_386_PC32″这种类型的重定位过程就是给call或者类似指令计算一个正确的操作数的过程。上面展示的伪代码中的refptr就是这个操作数。因为在给符号定义(definition)定位的过程中,ADDR(r.symbol)是确定的,所以,refptr就是可以计算的。

R_386_32

这种情况就简单些,计算方法只是将可重定位引用所在节的首地址和其偏移量相加,这样就能确定符号在虚存中的位置。

未完待续。

——写完后偷偷修改的分割线——-
这两天些的东西非常tmd的艰深难懂,但是硬骨头还是要啃的。市面上有很多SourceReview的书,但是读完之后总是觉得只见树木不见森林。我想要一个从上到下看到通通投投的Linux内核“解析体验”,哈哈哈。理论是比较枯燥,细节是比较烦人,但是所有奇妙的计算效果就是用这些东西为基础的,没有办法。很多时候也许真的需要不求甚解,但是,我是个偏执狂,如果遇到自己感兴趣却没有弄通的东西总觉得如鲠在喉。最终结果是写这样的文章难为自己,看这个样的文章吓走朋友,哈哈哈

发表评论