Chapter 7.1 Base Concepts

前六章我们一直在讨论程序的结构与执行的话题,即程序本身的表示、结构与运行过程。从本章起,我们将关注一个全新的话题:在系统上运行程序,即程序是如何在一个操作系统上运行的,不仅仅关注程序本身,更关注它与操作系统的交互。

这一章我们将聚焦一个极其容易被忽视而又十分关键的过程:链接(Linking)。我们将讨论:

  1. 为什么需要链接,链接带来了什么好处?
  2. 现有的计算机系统中是怎样链接的?
  3. 链接带来的一些有趣的技术。

Why Linking?

想象这样一个过程,这是个没有链接的世界,你写的每个代码都直接整体的编译成一个程序。

在这样一个世界,你是牛逼的开发 Windows 系统项目的管理人,吃着火锅唱着歌,老板来了个电话:程序出 bug 了!

你回到电脑前,经过漫长的调试,你发现是新来的小伙把 i==0 写成了 i=0 ,你一怒之下开除了他,改了这个小小的错误。然后面对几百万行代码,怎么办?重新编译!

你说编译就编译呗,不就点一下的事吗?几秒钟就搞定了。那是你平常写的玩具程序很短,几秒就完成了编译。But! 在前面优化第五章我们学过,编译过程编译器会做很多复杂的优化,需要消耗大量的资源去复杂的分析,就算降低优化强度极长的代码也需要漫长的时间翻译。几百万行代码往往需要以天、周甚至月为单位的时间进行编译。

为了重新编译这个有一丁点改动的代码,你们重新编译了这个庞大的代码,在一个月之后,终于完成了。你把修改了 bug 版本的程序打包发给客户,绝望的发现:用户早就开始用友商的程序了。这时你接到老板愤怒的电话:带着你的项目组滚蛋!

失业的你极其愤怒并且无事可干,你想到我就改了那么小一个部分的代码,却要把整个代码重新编译一遍,如果我可以只把那一小部分抽出来重新编译,再和其他部分链接在一起就好了!你在悲愤之中写出了人类历史上第一个链接器!你将被历史铭记!

好了,白日梦结束了,你既不是 Windows 的开发这并且人们早就发明了链接没机会给你永垂不朽了。

从上面的例子我们可以看出链接的重要性。在学校中大家往往写的都是写简单的玩具代码,最长也不过百来行,全部揉进一个文件中也不是不行,链接往往被忽略。

然而在实际的工程实践中,代码往往十几万行起步,百万行的程序更是比比皆是,并且涉及到多人合作、频繁修改等问题。这种时候如果在一个文件中这个工程就别维护了,项目组该滚了。那么在不同的文件中必然涉及到如何整合成一个程序

接下来我们将系统的阐述链接的一些思想方法与好处。

Modularity and EEficiency

链接是程序模块化思想的一部分,程序可以划分成一个一个模块,有不同的人分别编写调试完成再整合为一个完整的程序。并且可以调用整合一个个外部的库去简化编程提高代码复用率。

这样模块化+链接的模式带来开发效率上巨大的好处。

  1. 时间上,对于每个文件分别编译,当我改变其中某个文件是无需将整个文件编译,只需要重新编译那个更改过的较小模块,并重新链接,这个的时间代价远远小于完整编译。而且对于不同的模块我可以并行编译。

  2. 空间上,通过的思想,我们实现将常用的功能封装进库中,在需要的时候调用库而非再重新编写。通过动态库的技术我们甚至能够节省内存!

Procedures

在介绍完了链接的基本好处和思想后,我们来看该如何链接。链接有两个基本的步骤符号解析以及重定位

Symbol Resolution

// swap.c
void swap(){...} /* define symbol swap */

// main.c
int main()
{
    ...
    swap();          /* reference symbol swap */
    int *xp = &x;    /* define symbol xp, reference x */
    ...
}

在代码中我们会产生大量的符号(Symbol),包括函数、变量等等被命名的单位。符号会经过定义和引用的过程。定义时向编译器声明了这个符号是什么,引用时则是使用这个符号。

但是这两步骤往往可能并不在同一个文件中!在 swap.c 中定义了 swap()这个函数,但我却需要在main.c中的main()函数中调用。那么分别编译时编译器在编译 main.c 时就无法得知 swap()具体的定义,在链接的过程就需要将每个符号的引用匹配上它的定义。这个过程被称为符号解析(Symbol Resolution)

Relocation

这不同的编译好的文件合并为一个文件的过程中,势必要排布不同文件的位置。经过前几个章节的熏陶学习我们知道,程序在执行时被加载经内存中,每条指令对应了一个地址,所谓调用函数的过程不过是跳转执行某个地址开始的指令。

然而由于分别编译的缘故,在单个文件编译时不知道自己会被分配到哪个地址处,每个符号有着不确定的地址,对于函数符号无法调用执行,对于变量无法确定分配内存位置读写。

那么链接时,在完成了符号解析后,显然需要对每个符号分配一个合理的地址,并且对于每个引用了这个符号的代码处填上合理的值。这个过程就是重定位(Relocation)

Object Files and ELF

Three Kinds of Objects Files

通过了编译器以及汇编器翻译后的代码是目标文件(Object Files),在第三章我们学过里面的二进制数对应了一个个汇编指令,常见的二进制文件有以下三种格式。

  1. Relocatable object file(.o file): 包含代码和数据,可以和其他可重定位二进制文件经过链接器处理形成可执行文件。

  2. Executable object file(.out file): 包含代码和数据,正如其名可以直接被加载到内存中执行

  3. Shared object file(.so file): 一类特定的可重定位文件,也被称为动态库(Dynamic Link Libraries DLLS)。我们将在库一节中详细介绍这种文件。

Excutable and Linkable Format(ELF)

Linux 中目标文件的标准二进制格式称为: ELF。正如其英文缩写展开后对应所示,包含可执行的程序以及通过链接后可执行的程序。本课程不会过于详细的介绍这种格式,只是简单介绍其中的一些内容为后面详细介绍链接过程提供必要的知识。对这种格式感兴趣的同学可以进一步查询资料了解这种格式~~(反正你做 Linkerlab 也得查)~~。

ELF 文件被被划分为一个个 section,不同的 section 对应了数据以及代码。基本的有 .text 对应了代码部分、.data 对应了数据部分。其中为了链接有几个特殊的 section 。

.symtab, 符号表(symbol table)对应了本文件中符号信息,无论是定义的符号还是引用的符号,在符号解析时链接器会到符号表中查询符号的定义以及处理待解析的符号。

.rel.text 存储了 .text 中待重定位的信息,.rel.data 存储了 .data 中待重定位的信息。在重定位时链接器就会到这些段中查找待处理的重定位需求


© 2025. ICS Team. All rights reserved.