从源文件到可执行程序

gcc将源代码文件处理成可执行程序,要经过四个过程:预处理——编译——汇编——链接。

1、预处理
预处理过程主要处理的是#include、#define、#if、#else、#ifdef、#endif等指令以及处理注释、行号(用于调试)等工 作。
这里假设源文件包括source.c以及其包含的所有头文件。执行下面的命令可以指示编译器在完成预处理过程后停止。
$gcc -E source.c -o source.i

预处理的操作包括很多方面:
1、展开所有宏定义
2、处理所有条件编译指令
3、处理文件包含指令
4、删除所有注释
5、添加行号和文件名标识,用于在调试时产生调试信息
6、仍然要保留#pragma指令,在后面的处理中,编译器需要使用这些信息。


2、编译
编译可以说是构建可执行程序过程中最重要的步骤,也是最复杂的步骤,这个过程涉及的理论通常需要一个学期的课程来讲述。如果你和我一样,从来没接触过编译 原理这门课,只需要大体了解其工作内容就行了。编译大致包括一下六个部分:扫描,语法分析,语义分析,源代码优化,代码生成,目标代码优化。

词法分析:这部分主要是由扫描器对源代码进行分析,产生记号。这些记号包括关键字,标识符,字面量,运算符等特殊符号。扫描的同时,会生成符号表(存放标 识符)和文字表(存放数字,常量等)等。

语法分析:语法分析器对扫描器产生的符号表,文字表进行分析,生成语法树。语法树是一种以表达式为节点的树,通常这种树中的叶子节点都是数字或符号。如下 所示,

语义分析:语法分析形成的语法树只能保证语法上合法,不能保证语义是否正确。如结构体和整数相乘struct * 3,语法上正确,但语义上就存在问题。语义可以分为静态语义和动态语义,静态语义在编译期间就可以确定,而动态语义需要在动态执行时确定。
静态语义包括声明和类型匹配,类型转换等。指针被赋值为浮点数,这就属于声明和类型匹配需要发现的问题,而浮点数用整数赋值就会产生类型转换的问题。最常 见的动态语义问题就是0作除数的问题。
语义分析结束后,语法树中的表达式都相应的标识了类型,还会按需插入类型转换等,同时符号表中也增加了相应的类型记录。

中间代码生成:现代编译器都有一个源代码级别的优化过程,诸如1+1这种表达式在优化过程中会被替换成2。说起来简单,但是在语法树上的修改很困难,所以 一般会将语法树转换成中间代码。这种代码不是特定于目标机器的,也不包含数据地址,寄存器编号等信息。
以中间代码为界,将编译器分为前端和后端,前端生成中间代码,后端生成目标机器码。像gcc这种跨平台的编译器,不同的平台可以使用同一个前端和多个不同 的后端。

目标代码的生成和优化:这一部分主要是将中间代码转换成目标机器代码。不同的机器由不同的代码格式,所以这部分的结果会因目标机器的不同而不同。通常生成 的目标代码还可以进行优化,较常见的如以移位运算代替乘法,选择合适的寻址方式,删除无用的指令等。随着计算机体系结构的日益复杂,诸如流水线,超标量, 多发射等技术的出现,代码的优化变得相当复杂,而且今后多处理器的流行会带来更大的挑战。

3、汇编
这一过程是通过汇编器完成的,输出文件是可以在指定机器上执行的指令。

4、链接
由上面的过程可以看出,目标文件都是单独生成的。学过c语言的都知道,有时候会进行跨文件的变量或者函数引用。那么,这种情况下需要确定其地址。链接的过 程就是将汇编后生成的目标文件(.o文件)组合成完整的可执行程序,在这个过程之中,就要完成变量或者函数的地址定位。
c/c++中的每个文件可以看作一个模块,模块之间京城需要进行互相访问(函数,变量),这种访问通过符号的引用来实现。这种引用带来的问题就是链接需要 解决的问题。这个过程主要包括了地址和空间分配,地址绑定以及重定位等步骤。如caller.c中的函数main调用了另一文件callee.c文件中的 函数func,由于模块是单独编译,main在编译期间不会清楚func的地址,这就要在链接期间进行函数的定位。可以想象,如果这工作要人工完成,简直 就是一场噩梦,当然,几十年前的程序员前辈们就是这么干的。而有了链接器,我们就可以不用管这些了。
不光是函数,变量的引用同样有这种问题。一般情况下,对于这种跨文件应用(也可能是引用的库),一般是使用一个特定的地址来充当目标地址(如000000 代表这个地址需要进行重定位),并在文件中标识出这个地址应该被替换成哪个变量或者函数的地址。如上文中所述,调用func可以生成指令call 000000,并标识此地需要的是函数func的地址。

你可能感兴趣的:(开发工具)