编译流程

559 阅读4分钟

编译流程

截屏2021-06-01 下午8.30.49.png

一、预编译

.m --> .i

主要处理那些源代码文件中的以“#”开始的预编译指令。比如 “#include”、“#define ”等

1.将所有的 “#define ”删除,并且展开所有的宏定义。

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

3.处理 “#include ”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。

4.删除所有的注释“//”和“/* */”。

5.添加行号和文件名标识,比如#2“hello.c”2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。

6.保留所有的 #pragma 编译器指令,因为编译器须要使用它们。

二、编译

.i --> .s

Objective C/C/C++ 使用的编译器前端是Clang,Swift是swift,后端都是LLVM.

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

编译器分为前后端原因

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

编译器前端

负责生成机器无关的中间代码
  1. 词法分析(扫描)
词法扫描器 将代码分割生成记号token,比如关键字、标识符、字面量,特殊符号(+、-运算符)
  1. 语法分析 使用上下文无关语法,生成语法树,以表达式为节点的树,处理表达式中括号不匹配,缺少操作符,但是并不了解整个语句的意义

  2. 语义分析(静态语义分析)

编译期能做的就是静态语义分析,运行时做的是动态语义分析,静态语义分析主要做的是声明与类型的匹配 ,类型的转换

  1. 源代码优化 中间代码的生成

比如一些简单运算 加减表达式可以直接计算确定 也叫做 三地址码 x = y op z

编译器后端

负责将中间代码转换成目标机器代码
  1. 代码生成器 中间代码生成目标机器代码 ,依赖目标机器
  2. 目标代码优化器 位移代替乘法运算

这个阶段如果变量跟源代码定义在了同一个编译单元里面,那么编译器可以为它们分配空间和地址,定义在其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候确定。

三、汇编

clang -c main.s -o main.o

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

四、链接

clang -c main.o -o main.out

编译器将源代码文件编译成了未链接的目标文件,链接器将这些目标文件链接成可执行文件,可以是静态库或者动态库。这个过程主要包括地址和空间分配、符号决议和重定位。比如说有个全局变量 var 定义在目标文件 A 里面,目标问文件 B 里面要访问这个全局变量,由于在编译目标文件 B 的时候,编译器并不知道 var 变量的目标地址,所以将目标地址设置为 0,等待链接器将目标文件 A 和目标文件 B 链接起来的时候再将其修正,这个地址修正的过程也叫做重定位

参考

节选自《程序员的自我修养:链接、装载与库》 -- 俞甲子 石凡 潘爱民

Wander