深入理解计算机系统之七--链接

一、简介

1、链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做连接器(linker)的程序自动执行的。链接器在软件开发中扮演者一个关键的角色,因为它们使得分离编译(separate compilation)成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这块模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。链接通常是由链接器来默默地处理的。

(1)理解链接器将帮助你构造大型程序。构造大型程序的程序员经常会遇到由于缺少模块、缺少库或者不兼容的库版本引起的连接器错误。除非你理解链接器是如何解析引用、什么是库以及链接器是如何使用库来解析引用的,否则这类错误将令你感到迷惑和挫败。

(2)理解连接器将帮助你避免一些危险的编程错误。Linux连接器解析符号引用时所做的决定可以不动声色地影响你程序的正确性。在默认情况下,错误地定义多个全局变量的程序将通过连接器,而不是产生任何警告信息。由此得到的程序会产生令人迷惑的运行时行为,而是非常难以调试。我们将向你展示这是如何发生时,以及该如何避免它。

(3)理解链接将帮助你理解语言的作用域规则是如何实现的。例如,全局和局部变量之间的区别是什么?当你定义一个具有static属性的变量或者函数时,实际到底意味着什么?

(4)理解链接将帮助你理解其他重要的系统概念。链接器产生的可执行目标文件在重要的系统功能中扮演着关键角色,比如加载和运行程序、虚拟内存、分页、内存映射。

(5)理解链接将使你能够利用共享库。多年以来,链接都被认为是相当简单和无趣的。然而,随着共享库和动态链接在现代操作系统中重要性的日益加强,链接成为一个复杂的过程,为掌握它的程序员提供了强大的能力。比如,许多软件产品在运行时使用共享库来升级压缩包装的(shrink-wrapped)二进制程序。还有,大多数Web服务器都依赖于共享库的动态链接来提供动态内容。

 

二、详解

1、编译器驱动程序

(1)sum.c文件

//
int sum(int *a, int n)
{
	int i, s = 0;

	for (i = 0; i < n; i++) {
		s += a[j];
	}

	return s;

}
//

(2)main.c

//
int sum(int *a, int n);

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

int main()
{
	int val = sum(array, 2);
	return val;
}
//

大多数编译器系统提供编译器驱动程序,它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。

比如,要用GNU编译系统构造示例程序,通过shell输入下列命令来调用GCC编译器

# gcc -Og -o prog main.c sum.c   

下图概括了驱动程序将示例程序从ASCII码源文件翻译可执行目标文件时的行为。.o文件组合起来创建一个可执行文件prog

# ./prog 执行时,是shell调用操作系统中一个叫做加载器(loader)的函数,它将可执行文件prog中的代码和数据复制到内存,

然后将控制转移到这个程序的开头。

2、静态链接

像Linux LD程序这样的静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。

为了构造可执行文件,链接器必须完成两个主要任务:

(1)符号解析(symbol resolution)。目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量。符号解析的目的是将每个符号引用正好和一个符号定义关联

起来。

(2)重定位(relocation)。编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存关联起来,从而重定位这些节,然后修改

所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。

3、目标文件

目标文件有3种形式:

(1)可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。

(2)可执行目标文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。

(3)共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动地加载进内存并链接。

编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。从技术上说,一个目标模块就是一个字节序列,

而一个目标文件就是一个以文件形式存放在磁盘中的目标模块。

目标文件是按照特定的目标文件格式来组织的,各个系统的目标文件格式都不相同。

从贝尔实验室诞生的第一个Unix系统使用的是a.out格式。Windows使用可移植可执行(PE)文件。MacOS-X使用Mach-O格式。

现代X86-64 Linux 和 Unix 系统使用可执行可连接格式(ELF)。

4、可重定位目标文件

ELF头以一个16位节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。

.text: 已编译程序的机器代码。

.rodata: 只读数据,比如printf语句中的格式串和开关语句的跳转表。

.data: 已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。

.bss:未初始化的全局和静态C变量,基于所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是

一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率;在目标文件中,未初始化变量不需要占据任何实际的磁盘

空间。运行时,在内存中分配这些变量,初始化为0。

.symtab: 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。

实际上,每个可重定位目标文件在.symtab中都有一张符号表。然而,和编译器中的符号表不同,.symtab不包含局部变量的条目。

.rel.text: 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量

的指令都需要修改。另一方面,任何调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位

信息,因此通常省略,除非用户显式地指示链接器包含这些信息。

.rel.data: 被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量

地址或者外部定义函数的地址,都需要被修改。

.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项来

调用编译器驱动程序时,才会得到这张表。

.line:原始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会得到这张表。

.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是null结尾的字符串的序列。

5、

 

 

 

 致谢

1、《深入理解计算机系统》[第3版],作者 Randal E.Bryant, David R.O`Hallaron 译者 龚奕利 贺莲   

 

 

 

 

 

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读