写在前面:本篇文章基于《程序员的自我修养》第二章–静态链接。尝试使用自己的语言去理解和描述所学内容。
写博客不能只是文字的迁移,要有自己的思考,要将知识联系起来,理解其本质。

第二部分 静态链接

被隐藏的过程

由于集成开发环境的使用,使得我们可以直接将源代码文件构建为一个可执行文件。这无疑是很方便的,但是我们在使用这种便利工具的同时,也要了解IDE将源代码文件构建成为可执行文件的过程。这是一个很复杂的过程,下面我们将一起去探寻其中奥秘。

这个过程可以被分为4个部分,即:预处理、编译、汇编、链接。或许你听说过这些名字,但是你是否知道这些步骤的具体作用与流程呢?
我将结合书籍和实际演示为你展现这个过程。(限于知识的匮乏,可能会有所疏漏,以后会慢慢补充)

gcc编译过程

预处理(Prepressing)

场所:预编译器cpp
gcc指令:gcc -E main.c -o main.i
结果:得到.i文件

  • 实际操作
  • 删除所有的“define”并展开所有的宏定义
  • 处理所有的条件预编译指令如:“#if”、”#elif”等
  • 处理“#include”预编译指令,将包含的文件插入到该预编译指令的位置。(递归进行)
  • 删除所有的注释
  • 添加行号和文件名标识,便于编译是编译器产生调试用的行号信息和用于编译是产生编译错误时可以显示行号
  1. 实际演示
    main.c

main.i

编译(Compilation)

场所:现版本的gcc会把预处理与编译放在一起,使用ccl程序完成。
gcc指令:gcc -S main.i -o main.s
结果:经过一系列词法分析、语法分析、优化后生成汇编代码文件(即.s文件)

具体过程会在后面提到,这里直接上结果(c代码编程汇编代码)

main.s

补充:gcc指令的扩展

gcc指令只是后台程序的包装,通过固定的参数调用不同的程序,例如上面使用-c参数调用ccl程序进行编译。

汇编(Assembly)

场所:汇编器(as)
gcc指令:gcc -c main.s -o main.o
as指令:as main.s -o main.o
结果:根据汇编指令和机器指令的对照表一一翻译,得到机器指令文件(.o文件)

与上一步编译相比,这一步不需要进行语法、语义的分析,也不需要进行指令的优化。

链接(Linking)

场所:链接器(ld)
结果:将很多目标文件链接在一起形成一个可执行文件。

ld

可以看见,在链接的过程中,需要用到很多其它文件。此过程以后再说,先一步步进行介绍。

编译器做了什么

我们先介绍一下在上述四个过程里,编译器到底做了什么。

什么是编译器?

最直观的角度来说,编译器就是把高级语言翻译成机器语言的工具。

为什么要有高级语言?

答案很简单,为了提高编程的效率。

从前面的程序可以看到,汇编语言或者机器语言是一些很复杂的代码,使用他们来进行程序的编写是一件很乏味且耗时的工作。
并且机器语言和汇编语言编写的代码往往依赖于特等的机器,一个程序在不同cpu往往无法运行。
所以就有了高级语言。高级语言是程序员可以在编程的过程里更加关注逻辑本身,尽量不需要考虑计算机本身的限制。其可移植性也使得不同计算机平台上可以运行调试同一个程序。

编译的6个步骤

扫描->语法分析->语义分析->源代码优化->代码生成->目标代码优化
(简略进行介绍)

源代码

汇编流程

  1. 词法分析:运用类似于有限状态机的算法将源代码的字符序列分割成一系列记号。
    eg

ps:记号可以大体分为关键字、标识符、字面量(数字、字符串等)、特殊符号(运算符等)
在识别记号的同时将他们分配到对应的表里。

  1. 语法分析:对记号进行语法分析,产生语法树。树的节点代表表达式
    eg2

  2. 语义分析:完成对表达式语法层次的分析,只能进行静态语义的分析。包括声明与类型的匹配、类型的转换。得到被标识了类型的语法树。

eg3

  1. 源代码优化:对语法树进行优化,比如对可以得出的结果进行合并等。这里得到的中间代码已经很接近目标代码了,但是此时的代码与目标机器和运行时环境无关。eg3
  2. 目标代码生成:将中间代码转换为目标代码,这个过程依赖于目标机器。

目标代码

  1. 目标代码的优化:对目标代码进行优化,比如寻址合适的的寻址方式、使用位移代替乘法运算、删除多余的指令等。

优化后

编译过程的复杂性

为什么编译的过程这么复杂呢?原因如下:

1、现代高级语言本身就是很复杂的。
2、现代的计算机cpu很复杂,为了支持cpu的诸多特性,编译器的指令优化就很复杂。
3、要支持多种硬件平台。


一个疑问?
经过上述编译过程得到的目标代码是否已经完备?
答案是肯定的,并不完备。因为经过上述过程得到的目标代码并不知道其index和array的地址,也就是说并不能在机器上进行执行。

该如何进行解决?
欲知后事如何,尽在链接之中。


链接器的年龄比编译器长

为什么说链接器的年龄比编译器长呢?
很简单,因为链接在编译之前就有了。前面我们已经知道,编译的目的是把高级语言转化为汇编语言,是在高级语言出现的基础上出现的。但是链接却不是依赖于高级语言。原始的链接概念远在高级程序语言出现之前就已经出现。

我们先从最开始的程序语言说起,即机器语言。但是机器语言本身无法在机器上运行,需要把1与0存储在纸带上,通过打孔的开始进行存储。这也就造成了每一条指令的地址随着孔数的打刻而被固定。而后随着程序规模的增大,其维护和使用也越来越繁琐。因为一旦需要插入或者输出一条语句就需要重新计算地址。(这种程序计算每一个目标地址的过程叫做重定位

所以汇编语言应运而生。我们通过比对这两种语言编写的同一个代码,不难得出结论,汇编语言不仅代码本身更加易于理解,逻辑更加清晰,还可以使得人们从具体的指令地址中解放出来。因为符号这个概念出现了,指令的确定不在仅限于一个绝对的地址,而是使用定义好的符号进行表示,即便其地址发生改变也可以通过引用符号的引用来修正整个程序。

可以说汇编语言的出现极大的解放了生产力,这也就导致了程序的代码量急剧增加,到了后来人们就开始考虑将不同功能的代码以一定的形式组织起来,以便于日后修改和重复利用。大势所趋,说干就干。于是人口开始将代码按照功能和性质进行划分为不同的模块,不同模块按照层次结构和其他结构来组织。

现在的软件开发过程中都是采用这种层次化以及模块化的方式处理源代码,那么这种方法有什么好处呢?用一种比较官方的说法:使得代码更加容易阅读、理解、重用,每个模块可以单独开发、编译、调试,改变某一个程序也不需要重新编译整个程序。

在将不同的代码封装为一个个模块后,我们接下来就要想着怎么把这些模块进行组合,其实不同模块之间的组合无非就是两个问题:函数的调用、变量的访问,而这两个问题归根结底还是模块间符号的引用。而这个实现不同模块拼接的技术就是链接

模块之间的组合

模块拼接——静态链接

前面说过,链接就是把各个模块之间的相互引用处理好,其本质和原始程序员手动调整地址无太大区别,因为从原理上来说,链接就是把一些指令对符号的地址的加以修正。

链接的过程可以分为以下几步:地址和空间分配符号决议重单位

下面是一个最简单的静态链接的流程图:
链接过程

可以看到,源代码经过编译后成为目标文件,而后多个目标文件以及库文件通过链接器结合在一起成为可执行文件。

定义介绍

运行时库(Runtime Library)

这是最常见的库,是支持程序运行的基本函数的集合。
而所谓库文件,就是一组目标文件的包,就是把一些最常用的代码编译为目标文件后打包存放。(ps:后面会详细介绍)

目标文件(Objcet)

全称为中间目标文件,是经过编译之后生成的文件,以.o结尾。这个文件的叫法有很多,可以被称为模块,也有的地方把它叫做可重定位目标文件。

符号决议(symbol resolution)

这一步要与符号绑定区分开。一般我们认为决议倾向于静态链接,绑定倾向于动态链接。至于二者的具体区分与过程,后面讲解。

重定位?

重定位就是对符号引用的地址进行修正,每一个被修正的地方被称为一个重定位入口,重定位就是给程序里每一个绝对地址引用的位置打补丁,把他们指向正确的地址。

静态链接最基本的过程

前面我们已经知道,一个程序涉及到很多模块,当你要在一个模块里使用另一个模块里的变量或者函数的时候,就需要其地址。但是编译器在编译的时候并不知道这些地址,所以编译器会将这些地址搁置。而静态链接就是通过程序引用的符号进行地址的修正,是的目标地址为真正调用的函数的地址。简单来说就是根据符号寻找地址并进行修正,这就是静态链接的过程与作用。


写在最后:本篇文章到此结束,但是这本书刚刚开始。下一章我们将走进目标文件,细致的了解一个目标文件的布局。再见。