Linux内核源码阅读系列(4)-链接1

上周工作实在太忙,写blog的事情是一拖再拖……本来准备好继续写Linux内核构成的,但是因为看书看的比较快,刚好看到一个比较关键的地方——程序的链接和载入,而这个部分有牵扯代码生成,机器语言编程,程序实例化和虚拟存储器很多内容,可以说是从程序员的角度理解Linux内核如何执行用户应用程序行为的核心。况且,这个部分牵扯很多不容忽视的“细节”,我害怕如果日子久了,就再也不记得这些内容,于是算是插入一篇,给自己记录下来。

什么是链接

大概教科书上都念过这样的经——源代码变成可执行文件一般要经过 1.编译预处理;2.编译;3.汇编;4.链接 这些过程。从使用TurboC那天起,我就被告知程序是这样产生的,但是直到我大学毕业以后的几年里,才真正知道这些事情都是怎么做的。而且,还是从一本卡耐基梅隆大学的本科生前导课程中学到的,由此可见中国所谓的大学误人子弟的老师的确不在少数。除了继续被愚弄几年以外,上那个大学又有什么意思?先忘掉将中国国内的计算机教育搞到本末倒置的Windows吧,看看Linux中的典型编译过程是怎样完成的。每个操作系统都提供一种编译驱动程序(compile driver),最典型的例子就是gcc。gcc事实上不是一个单独的程序,而是一组程序的组合。Unix世界的逻辑就是将事情分解和简化然后分发给各个可以相互协作的部分完成,在这个设计思想下,Unix世界产成了很多专注做好一件事情的小程序,比如最简单的yes。然而,又为了解决各个小程序之间的协同工作,Unix工具集中又添加进很多tools driver,比如gcc,这种tools driver的设计思想有点像设计模式中提到的facade(这个词似乎是法语,注意发音),他把一些难以把握细节的小工具进行整合从而为用户提供一个简便的接口。gcc在编译程序的过程中实际上需要调用cpp,cc1,as和ld这些工具来帮助它完成工作,于是编译过程就是个RPG游戏,当然这里需要重点看看角色变换和他们的输入与产出。下面游戏开始:

故事背景

一个简单的Swap程序:

/* main.c */
void swap();

int buf[2] = {1,2};

int main()
{
	swap();
	return 0;
}
/* swap.c */
extern int buf[];

int *bufp0 = &buf[0];
int *bufp1;

void swap()
{
	int temp;

	bufp1 = &buf[1];
	temp = *bufp0;
	*bufp0 = *bufp1;
	*bufp1 = temp;
}

出场人物

  1. cpp,“预处理器”
  2. cc1,“c语言编译器”
  3. as,“汇编器”
  4. ld,本集主角,江湖人称“链接器”

这些程序有的是不能被直接调用的,但有的是可以的。为了将问题简化,还是给gcc添加不同的选项作为驱动来观察比他们之间都发生了什么吧。具体什么选项呢,man gcc看看吧。


gcc [-c|-S|-E] ... infile ...

gcc手册的第一行就告诉我们“他不是一个人”。这三个选项从后往前分别指明的就是 -E 预处理; -S 编译; -c 汇编;如果不加参数就直接将输入的源文件做到链接,默认情况是一条龙服务。下面的表格简单的列出了各个步骤的命令,输入和输出。

命令 角色 输出 注释
gcc -E x cpp main.i 这里产生一个经过预处理的中间文件
gcc -S x cc1 main.S 产生汇编语言文件
gcc -c x as main.o 产生可重定位的ObjectFile

此外,swap.c的代码也可以按照上面的步骤按部就班的生成一个swap.o。接下来的工作就是用ld对已经生成的.o文件进行“链接”产生可执行文件。由此可见,

链接就是将不同部分的代码和数据收集和组合成一个单一文件的过程。这个文件可以被加载(或者被拷贝)到存储器中执行。(来自《深入理解计算机系统》)

但如果事情做到这个步,算是可以告一个段落,因为最重要的内容之一“可重定位的目标文件”(relocatable object file)已经生成了,也就是这里出现的.o文件。从技术上说.o与普通的二进制文件相比并没有什么特别之处,它就是一个在磁盘文件中的“字节序列”。但是,这个文件的重要之处在于他是类UNIX操作系统的ABI(application binary interface)的核心。

ELF

ELF的名字不错,elf似乎是德国神话传说中的一种精灵,恰恰也说明了ELF文件在系统中的执行就像是变魔法。跑题了。其实他是Executable and Linkable Format的缩写形式,wikipedia上关于elf的解释包括英文在内都不甚详细,但是还是值得一读的。这种二进制文件格式广泛使用于各种计算机平台。最早的ObjectFile的格式是诞生于贝尔实验室的a.out,知道现在仍然有很多应用程序采用a.out形式运行。

这里插播一下历史消息。贝尔实验室的UNIX系统中使用a.out作为可执行文件的形式,而后在很多UNIX版本中这个二进制文件格式被大量的采用。UNIX系统发展的重要里程碑System V在诞生的时候采用COFF(common object file format)——微软这个偷学狂人在其Windows系统发展的过程中采用了COFF的一个变体作为自己的可执行文件的形式至今,称为PE(portable executable)——现代UNIX版本中大多采用了ELF代替此前比较原始的二进制形式。

伟大的系统在诞生和发展过程中总能产生一些伟大的部件,甚至有些系统本身已经不存在了,但它的思想或者某些精妙的实现却依然在其他系统中以某种形式存在。这个例子数不胜数,比如上面说到的ELF,再比如研发Plan 9操作系统(现在依然存在)的过程中诞生的unicode和procfs。

ObjectFile有三种形式,1.可重定位目标文件(relocatable object file); 2.可执行目标文件(executable object file); 3.共享目标文件(shared object file)。
其中可重定位目标文件就是指.o文件。下面这个图展示的是一个典型的.o的文件组成。

 +----------------------+
 |    ELF header        | <--帮助链接器解析ObjectFile的信息
 +----------------------+
 |       .text          | <--已编译程序的机器代码
 +----------------------+
 |       .rodata        | <--只读数据,比如pirntf的格式化字串等
 +----------------------+
 |       .data          | <--已经初始化全局变量
 +----------------------+
 |        .bss          | <--未初始化全局变量
 +----------------------+
 |        .symtab       | <--符号表,这个表是提供给链接器使用的,每个OjectFile
 +----------------------+
 |      .rel.text       | <--可重定位的代码
 +----------------------+
 |      .rel.data       | <--可重定位的数据
 +----------------------+
 |       .debug         | <--调试符号表
 +----------------------+
 |       .line          | <--.text节中机器指令于源程序行号之间的映射表
 +----------------------+
 |       .strtab        | <--字符串
 +----------------------+
 | section header table | <--节头表(section header table)
 +----------------------+

更加详细的图表可以在"Linkers and Loaders"一书中看到,点这里

这些分段被称为“节”(section),并且,在.o文件中为这些保留了一张表,称作“节头表”(section header table)。节头表描述了不同节的位置和大小,其作用有点像各个节的检索索引。这些节之中,.debug.line节包含的是调试信息,只有gcc在使用"-g"选项时才能得到。而.symtab这个符号表节是每一个ObjectFile都会包含的,一些程序员错误的认为只有在使用"-g"选项时才能在ObjectFile中得到符号表。而这个.symtab节正是链接操作的核心。

预知后事如何,且听下回分解吧。下午约了师兄去游泳,4点从图书馆出来背着两块砖头一样的书就去了,被水一泡想说的东西全忘了。

发表评论