explorer

万丈高楼平地起,勿在浮沙筑高台

0%

[What] 链接、装载与库 --> 编译和链接

学习编译及链接的大体过程。

预处理(Preprocessing)

将源码文件及其相关头文件被 cpp 预编译成一个 .i 文件。

  • 对于 c++ 来说是被预编译为 .ii 文件

预编译的处理规则如下:

  • 将所有 #define 删除,并且展开所有的宏定义
  • 处理所有条件预编译指令,比如 #if, #ifdef, #elif, #else, #endif
  • 处理 #include 预编译指令,将被包含的文件 插入到该预编译指令的位置
    • 这个过程是递归进行的,如果被包含文件还包含其他文件,也会依次展开
  • 删除所有的代码注释
  • 添加行号和文件名标识,便于编译时编译器产生调试用的符号信息和错误或警告时的提醒
  • 保留所有的 #pragma 编译器指令,在编译阶段被使用

通过查看预编译后的文件,可以检查宏是否正确以及文件包含是否正确。

编译(Compilation)

编译的过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析以及优化后生成相应的汇编代码文件。

在 gcc 中,将从预编译到编译使用命令 gcc -S hello.i -o hello.s,也可以使用 cc1 来完成预处理和编译.

编译过程一般可以分为6步:扫描,语法分析,语义分析,源代码优化,代码生成,目标代码优化。

扫描器(词法分析)

源代码程序首先被输入到扫描器(Scanner),扫描器的任务很简单,只是简单的进行词法分析,运用一种类似于有限状态机(Finite State Machine) 可以很轻松的将源代码的非空格字符序列分割成一系列的记号(Token)

词法分析产生的记号一般可以分为如下几类:关键字,标识符,字面量(数字,字符串等)和特殊符号(加号,等号等)在识别记号的同时,扫描器也完成了其他工作,比如将标识符存放到符号表,将数字、字符串常量存放到文字表等。

扫描器的程序工具叫做lex

语法分析(Grammar Parser)

对扫描器产生的记号进行语法分析,从而产生语法树(Syntax Tree),整个分析过程采用了上下文无关语法(Context-free Grammar) 的分析手段。

语法分析器的工具叫做 yacc(Yet Another Compiler Compiler)。

语义分析(Semantic Analyzer)

语法分析仅仅完成了对表达式的语法层面分析,但是它并不了解这个语句是否真正有意义。

编译器所能分析的是静态语义(Static Semantic),是指在编译时可以确定的语义,与之对应的是动态语义(Dynaic Semantic)就是只有在运行期才能确定的语义。

经过语义分析后,语法树的表达式都被标识了类型,如果有些类型需要作隐式转换,语义分析程序会在语法树中插入相应的转换节点。

中间语言生成

源代码优化器(Source Code Optimizer) 在源代码级别进行优化,但是由于直接在语法树上做优化比较困难,所以源代码优化器往往将整个语法树转换成中间代码(Intermediate Code),它是语法树的顺序表示。

中间代码有很多种形式,在不同的编译器中有着不同的形式,比较常见的有:三地址码(Three-address Code)P-代码(P-Code)

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

这样对于一些跨平台的编译器而言,它们可以针对不同的平台使用同一个前端和不同的后端。

目标代码生成与优化

源代码级优化器产生中间代码标志着下面的过程都属于后端,后端主要包括代码生成器(Code Generator)目标代码优化器(Target Code Optimizer)

代码生成器将中间代码转换为目标机器码,这个过程十分依赖于目标机器。

汇编(Assembly)

将编译后的汇编代码转变为二进制文件,因为汇编都有对应的二进制值,所以汇编器的汇编过程相对于编译器来讲比较简单。

在 gcc 中,将汇编文件到二进制文件使用命令 gcc -c hello.s -o hello.o / gcc -c hello.c -o hello.o,也可以使用 as 来完成。

经过预编译、编译、汇编之后的 .o 文件被称为目标文件

链接(Linking)

将目标文件与库文件以一定的规则,确定的地址 结合,才能形成最后的可执行文件。