被隐藏了的过程

  1. #include <stdio.h>
  2. int main()
  3. {
  4. printf("Hello World");
  5. return 0;
  6. }
  • 预处理(Preprocessing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)

image.png

预处理

  1. gcc -E hello.c -o hello.i

对于C++预编译后的扩展名.ii

预编译过程主要处理那些源代码文件中的以“#”幵始的预编译指令
经过预编译后的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题

编译

GCC使用cc1将预编译和编译两个步骤合成一个步骤

  1. gcc -S hello.i -o hello.s

对于C++来说, 预编译和编译的Linux下的程序是cc1plus

实际上 gcc 这个命令只是这些后台程序的包装,它会根据不同的参数要求去调用预编译编译程序 cc1、汇编器 as、链接器 Id

汇编

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了,“汇编”这个名字也来源于此。

  1. as hello.s -o hello.o

或者

  1. gcc -c hello.s -o hello.o

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件(object file)

  1. gcc -c hello.c -o hello.o

链接

链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?链接过程到底包含了什么内容?为什么要链接?

得到a.out

  1. $ld -static /usr/lib/crtl.o /usr/lib/crti.o
  2. /usr/lib/gcc/i486-linux-gnu/4.1.3/crtbeginT.o
  3. -L/usr/lib/gcc/i486-linux-gnu/4.1.3 -L/usr/lib -L/lib hello.o --start-group
  4. -lgcc -lgcc_eh -1c --end-group /usr/lib/gcc/i486-linux-gnu/4.1.3/crtend.o
  5. /usr/lib/crtn.o

编译器做了什么

从最直观的角度来讲,编译器就是将高级语言翻译成机器语言的一个工具
高级语言使得程序员们能够更加关注程序逻辑的本身,而尽量少考虑计算机本身的限制,如字长、内存大小、通信方式、存储方式等高级编程语言的出现使得程序开发的效率大大提高,高级语言的可移植性也使得它在多种计算机平台下能够游刃有余。

从源代码到最终目标代码

  • 编译过程: 扫描, 语法分析, 语义分析, 源代码优化, 代码生成和目标代码优化

image.png

以下面C程序举例

  1. array[index] = (index + 4) * (2 + 6)

词法分析

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

记号 类型
array 标识符
[ 左方括号
index 标识符
] 右方括号
= 赋值
( 左圆括号
index 标识符
+ 加号
4 数字
) 右圆括号
* 乘号
( 左圆括号
2 数字
+ 加号
6 数字
) 右圆括号

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

有一个叫做lex的程序可以实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。因为这样一个程序的存在,编译器的开发者就无须为每个编译器开发一个独立的词法扫描器,而是根据需要改变词法规则就可以了。

另外对于一些有预处理的语言, 比如C语言,它的宏替换和文件包含等工作一般不归入编译器的范围而交给一个独立的预处理器。

语法分析

接下来语法分析器(Grammar Parser) 将对由扫描器产生的记号进行语法分析,从而产生语法树(SyntaxTree)。整个分析过程釆用了上下文无关语法(Context-free Grammar)的分析手段,如果你对上下文无关语法及下推自动机很熟悉,那么应该很好理解。否则,可以参考一些计算理论的资料,一般都会有很详细的介绍。此处不再赘述。简单地讲,由语法分析器生成的语法树就是以表达式(Expression)为节点的树。

C语言的一个语句是一个表达式,而复杂的语句是很多表达式的组合
上面例子中的语句就是一个由赋值表达式、加法表达式、乘法表达式、数组表达式、括号表达式组成的复杂语句
它在经过语法分析器以后形成如图所示的语法树
image.png
在语法分析的同时,很多运算符号的优先级和含义也被确定下来了. 另外有些符号具有多重含义,比如星号*在C语言中可以表示乘法表达式,也可以表示对指针取内容的表达式,所以语法分析阶段必须对这些内容进行区分。如果出现了表达式不合法,比如各种括号不匹配、表达式中缺少操作符等,编译器就会报告语法分析阶段的错误
正如前面词法分析有 lex —样,语法分析也有一个现成的工具叫做 yacc ( Yet Another Compiler Compiler)。它也像 lex —样,可以根据用户给定的语法规则对输入的记号序列进行解析,从而构建出一棵语法树。对于不同的编程语言,编译器的开发者只须改变语法规则,而无须为每个编译器编写一个语法分析器,所以它又被称为“编译器编译器(Compiler Compiler)”。

语义分析

接下来进行的是语义分析,由语义分析器(Semantic Analyzer) 来完成。
语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义

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

链接器年龄比编译器长

模块拼装-静态链接