本文最后更新于:2020年6月27日 晚上

* 从第二章开始不再按照目录的顺序总结,而是将大块知识点总结在一起。。。 *

第二章 编译和链接

集成开发环境(IDE)一般都将编译和链接的过程一步完成,此过程成为构建(Bulid)。但其掩盖了系统软件运行机制。

$gcc hello.c
$./a.out

一个可执行文件的生成,可以分解成4个步骤:预处理(Prepressing)编译(Compilation)汇编(Assembly)链接(Linking)

GCC编译过程分解

1.预编译:C程序源代码文件和相关头文件被预编译器cpp预编译成 .i 文件。C++程序被预编译后的文件拓展名为 .ii 。

/* -E表示只进行预编译 */
$gcc -E hello.c -o hello.i$cpp hello.c > hello.i

预编译过程主要处理源代码文件中以“#”开始的预编译指令。

  • 将所有的“#define”删除,并展开所有宏定义

  • 处理所有条件预编译指令,如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”

  • 处理“#include”预编译指令,将包含的文件插入到该预编译指令的位置。此过程是递归的。

  • 删除所有的注释“//”和“/**/”

  • 添加行号和文件标识,以便于编译时器编译器产生调试用的行号信息及编译时产生错误或警告的时显示行号

  • 保留所有#pragma编译器指令,因为编译器需要使用它们

2.编译:把预处理完的文件进行一系列词法分析语法分析语义分析优化后生成相应的汇编代码文件

/* -S表示只进行编译 */
$gcc -S hello.i -o hello.s$gcc -S hello.c -o hello.s

现在版本的GCC将预编译和编译两个步骤合并在一个步骤中,由编译器完成。对于C语言代码使用cc1程序、C++语言使用cc1plus、Objective-C语言使用cc1obj、fortran语言使用f771、Java语言使用jc1。

实际上gcc命令只是后台程序的包装,其根据不同的参数调用预编译器cc1、汇编器as、链接器ld。

编译器是将高级语言翻译成机器语言的一个工具,其编译过程一般分为6步:扫描语法分析语义分析源代码优化代码生成目标代码优化

编译过程

  • 语法分析:源代码程序被输入到扫描机(Scanner),运用一种类似于有限状态机(Finite State Machine)的算法,将源代码的字符序列分割成一系列记号(Token)
    词法分析产生的记号一般分为:关键字、标识符、字符量(包含数字、字符串等)和特殊符号(如加号、等号)。此过程中,扫描器也将标识符存放到符号表,将数字、字符串常量存放到文字表。
    语法树

符号和数字是最小的表达式,它们不再由表达式来组成,它们通常作为整个语法树的叶节点。语法分析的同时运算符号的优先级和含义也被确定下来。如果出现了表达式不合法,比如括号不匹配、表达式中缺少操作符等,编译器会报告语法分析阶段的错误。

  • 语义分析语义分析器(Semantic Analyzer)判断语句是否正真的有意义。编译器能分析的是静态语义(Static Semantic),是在编译期可以确定的语义,通常包括声明、类型的匹配、类型的转换。而动态语义(Dynamic Semantic)只有在运行期才能确定,比如0作为除数。
    语义分析后,语法树的标识符都标识了类型,若有类型的隐式转换,语义分析器会在语法树中插入相应的转换结点。
    这里写图片描述

  • 中间语言生成源代码优化器(Source Code Optimizer)在源代码级别进行优化。源代码优化器往往将整个语法树转换成中间代码(Intermediate Code),它是语法树的顺序表示
    它与目标代码非常接近,但其跟目标机器和运行环境无关不包含数据的大小变量地址寄存器名等。中间代码常见类型有:三地址码(Three-address Code)P-代码(P-Code)
    优化后的语法树
    上图语法树翻译成三地址码为:

    t1 = 2 + 6
    t2 = index + 4
    t3 = t2 * t3
    array[index] = t3
    /*继续优化*/
    t2 = index + 4
    t2 = t2 * 8

    中间代码使编译器分成前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。

  • 目标代码优化与生成:编译器后端主要包括代码生成器(Code Generator)目标代码优化器(Target Code Optimizer)
    代码生成器将中间代码转换成目标机器代码,目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用位移代替乘法运算、删除多余指令等。

3.汇编:汇编器将汇编代码转化成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。根据汇编指令和机器指令的对照表一一翻译。

$as hello.s -o hello.o$gcc -c hello.s -o hello.o$gcc -c hello.c -o hello.o

4.链接:重新计算各个目标地址的过程叫做重定位(Relocation)符号(Symbol)用来表示一个地址,这个地址可能是函数或变量的起始地址。汇编器在每次汇编程序的时候重新计算符号的地址,把所有引用该符号的指令修正到这个正确的地址。

一个程序被分割成多个模块之后,模块之间的通信方式有模块间的函数调用模块间的变量访问两种方式,归结为模块间符号的引用。

定义符号的模块多出一块区域,引用该符号的模块刚好少了那一块区域。两者的拼接过程即链接(Linking)。

模块间拼接

  • 模块拼接——静态链接

将每个源代码模块独立地编译,然后按照需要将它们组装起来,这个组装模块的过程就是链接(Linking)。

链接过程主要包括地址和空间分配(Address and Storage Allocation)符号决议(Symbol Resolution)重定位(Relocation)等。每个模块的源代码文件经过编译器编译成目标文件(Object File,一般拓展名为.o或.obj)目标文件和库(Library)一起链接形成最终可执行文件

连接过程

每个模块都是单独编译,编译器编译a.c时并不知道引用的函数的地址,所以暂时把调用该函数的目标地址搁置,等待最后链接时由链接器将这些指令的目标地址修正。链接器在链接时根据所引用的符号,自动去相应的模块查找该符号的地址,然后将a.c模块中所有引用到该符号的指令重新修正,让其目标地址为真正的符号的地址。
地址修正的过程叫做重定位(Relocation),每个要被修正的地方叫一个重定位入口(Relacation Entry)。

* 下次想总结一下UDP/TCP相关。。。 *