第二章 编译和链接
构建(Build):集成开发环境(IDE,Integrated Development Environment)通常将编译和链接合并到一起
深入理解gcc hello.c
的过程
预处理 Prepressing
- C++ 程序的源代码文件扩展名是
.cpp
或.cxx
,头文件扩展名是.h
或.hpp
,预编译后文件扩展名是.ii
- 只执行预编译
gcc -E hello.c -o hello.i
- 预编译主要处理源码中以
#
开头的预编译指令,比如#define
,#include
等,处理规则如下- 删除
#define
,展开宏定义 - 处理条件预编译指令,比如
#if
,#ifdef
#else
#endif
- 处理
#include
预编译指令,将被包含的头文件插入到该预编译指令的位置,此过程是归队执行的 - 删除注释
//
和/**/
- 添加行号和文件名标识,比如
#2 "hello.c" 2
,便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时可以显示行号 - 保留
#pragma
编译器指令
- 删除
- 经过预编译后的
.i
文件不包含任何宏定义,且包含的头文件被插入到.i
文件。所以当无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题
编译 Compilation
- 编译是对预处理之后的文件进行词法分析、语法分析、语义分析及优化后产生响应的汇编代码文件
- 执行编译
/usr/lib/gcc/x86_64-linux-gnu/4.8/ccl hello.i
,查看分析流程- C++ 代码的预编译和编译程序shi
cclplus
gcc -S hello.i -o hello.s
gcc -S hello.c -o hello.s
- C++ 代码的预编译和编译程序shi
汇编 Assembly
- 汇编器将汇编代码转变成机器可执行的指令,每一个汇编语句对应一条机器指令
- 执行汇编
gcc -c hello.s -o hello.o
,生成目标文件(Object File)as -c hello.s -o hello.o
gcc -c hello.c -o hello.o
链接 Linking
- 调用
ld
产生一个可以正常运行的程序
编译器
- 编译器将高级语言翻译成机器语言。
- 使用机器语言或汇编语言编写程序十分麻烦
- 使用机器语言或汇编语言编写程序依赖特定的机器
- 高级语言更加关注程序逻辑的本身,而尽量少的考虑计算机本身的限制,如字长、内存大小、通信方式、存储方式等
- 高级语言大大提高了程序开发效率
- 高级语言的可移植性
编译过程
Source Code
>> Scanner >> Tokens
>> Grammar Parser >> Syntax Tree
>> Scemantic Analyzer >> Commented Syntax Tree
>> Source Code Optimizer >> Intermediate Representation
>> Code Generator >> Target Code
>> Code Optimizer >> Final Target Code
扫描,词法分析
- 扫描器(Scanner),进行词法分析,运用类似于有限状态机(Finite State Machine)的算法将源代码的字符序列分割成一系列的记号(Token)
- 记号一般分为:关键字、标识符、字面量(数字、字符串等)和特殊符号(加号、等号等)
- 扫描器同时将标识符放到符号表,将数字、字符串常量放到文字表
- 程序 lex 可以实现词法扫描,按照用户之前描述好的词法规则将输入的字符串分割成一个个记号,这样编译器的开发者只需要根据需要改变词法规则就可以解析不同的语言
- 对于有预处理的语言,比如 C 语言的宏替换和头文件包含等工作交给单独的预处理器处理
语法分析
- 语法分析器(Grammar Parser):语法分析器对记号进行语法分析,生成语法树
- 采用上下文无关语法(Context-free Grammar)的分析手段
- 语法树是以表达式(Expression)为节点的树
- 符号和数字是最小的表达式,通常作为语法树的叶节点
- 语法树已经确定了运算符号的优先级和含义
- 出现了表达式不合法,如括号不匹配、表达式缺少操作符等,编译器会报告语法分析阶段的错误
- 程序 yacc(Yet Another Compiler Compiler)可以根据用户给定的语法规则对输入的记号序列进行解析,从而构造出语法树
- 编译器的开发者只需要高边语法规则就可以编写语法分析器,所以被称为
编译器编译器(Compiler Compiler)
语义分析
- 语义分析(Semantic Analyzer):判断语句是否真正有意义
- 编译器分析的语义是静态语义(Static Semantic),是在编译期可以确定的语义,动态语义(Dynamic Semantic)是在运行期才能确定的语义
- 静态语义通常包括声明和类型的匹配、类型的转换
- 动态语义是在运行期出现的语义相关的问题,比如 将 0 作为除数
- 经过语义分析,正哥语法树的表达式被标识了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树种插入响应的转换节点
源代码优化
- 源代码优化器(Source Code Optimizer):在源代码级别进行优化
- 源代码优化器往往将整个语法树转换成中间代码(Intermediate Code),是语法树的顺序表示。和目标机器和运行时环境无关
- 中间代码常见的形式:三地址码(Three-address Code)和 P-代码(P-Code)
- 中间代码使得编译器分为前端和后端:前端负责产生机器无关的中间代码,后端负责将中间代码转换成目标机器代码
- 对于可以跨平台的编译器,可以针对不同的平台使用同一个前端和针对不同机器平台的数个后端
代码生成
- 代码生成器(Code Generator),属于编译器后端,将中间代码转换成目标机器代码
- 此过程依赖于目标机器,不同的机器有不同的字长、寄存器、整数数据类型和浮点数数据类型等
目标代码优化
- 目标代码优化器(Code Optimizer),属于编译器后端,优化目标代码
- 比如选择合适的寻址方式、使用位移代替乘法运算、删除多余的指令等
链接器
- 现代的编译器将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。
- 重定位(Relocation):重新计算各个目标的地址过程
- 程序设计的模块化:将代码安装功能或性质划分,分别形成不同的功能模块,不同的模块之间按照层次结构或其他结构来组织。
静态链接
- 链接(Linking)的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。
- 链接主要包括地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等
- 符号决议有时也叫做符号绑定(Symbol Binding)、名称绑定(Name Binding)、名称决议(Name Resolution),或地址绑定(Address Binding)、指令绑定(Instruction Binding)。“决议”倾向于静态链接,而“绑定”倾向于动态链接,二者使用的范围不同。在静态链接,统称为符号决议。
- 静态链接将目标文件(Object File,扩展名一般是
.o
或.obj
)和库(Library)一起链接形成最终可执行文件。- 库其实是一组目标文件的包,是将一些最常用的代码编译成目标文件后打包存放
- 常见的是运行时库(Runtime Library)
- 重定位就是链接器在链接目标文件的时候,将定义在其他目标文件中的函数调用或变量的地址进行修正
- 重定位入口(Relocation Entry),指的是地址要被修正的地方