gcc编译器眼中的C语言

103 阅读9分钟

编程语言一般分为编译期、运行期

有的编程语言是编译期与运行期合并在一起的,比如Python、PHP…即解释型语言

有的则是编译期与运行期分开的,比如C、C++、Java…即编译型语言

今天咱们深入聊聊编译期的C语言

运行期的C语言,我在我之前的课程中已经详细讲过,感兴趣的自己去看视频

编译期,即编译器工作的期间

一般编译器,都是基于《编译原理》实现的

但是C语言的编译器的实现,在《编译原理》基础上做了拓展,如图

接下来详细讲讲C语言编译器的各个阶段

预处理

在真正编译C语言程序之前,需要对C语言程序进行预处理,预处理是由集成在编译器中的预处理器完成的

从《编译原理》的角度,词法分析是编译器的第一个阶段。但是从C语言编译器的角度,预处理是第一个阶段

预处理阶段主要完成四件事:删除注释、宏展开、文件包含、条件编译

如果你要做实验论证,gcc -E即可实现,比如

image.png

举个例子吧:删除注释、宏展开,比如代码

预处理以后,注释没有了,宏展开了。C语言的预处理器,就是简单的宏替换

你会发现,预处理器会在开头多生成点东西,这些东西是什么呢?有什么用呢?

这些看似神秘的 # 开头的行,实际上叫做行控制信息(line control directives),是 GCC 预处理器在输出中自动插入的特殊指令,是方便后续编译器、调试器、诊断工具理解源码位置的信息

词法分析

经过预处理后,得到的就是完整的C语言程序了,就可以开始编译了。词法分析器启动…

词法分析器的职责是:输入源程序,输出token

来看看上面的程序生成的token

如何查看C语言程序生成的token呢?

image.png 你可能想问:为什么用clang,而不是用gcc?因为gcc作为老牌的编译器,不支持这个功能

来看看词法分析器生成token的过程

如果你想透彻理解词法分析的底层原理,你可以使用词法分析工具flex生成词法分析器,去实战。或者自己从零写一个词法分析器,体会将程序的点点滴滴转成token的过程,你的困惑就解开了…(这部分内容,我做的课程手写编程语言中有教,感兴趣的可以咨询班主任jvm-anan

语法分析

拿到源程序对应的token,就可以去做语法分析了。语法分析器启动…

语法分析器的职责是:输入token,输出抽象语法树AST。后面的阶段,都是围绕AST进行的

换个程序,来看看经过语法分析生成的抽象语法树

对应的AST

如何查看呢?两种方式

image.png 其实gcc也可以,就是生成的文件太多,看起来不直观

image.png

会生成这些文件

来看看token生成AST的过程,比如print语句

如果你想透彻理解语法分析的底层原理,你可以使用语法分析工具bison生成语法分析器,去实战。或者自己从零写一个语法分析器,体会将token转成AST的过程,你的困惑就解开了…(这部分内容,我做的课程手写编程语言中有教,感兴趣的可以咨询班主任jvm-anan)

语义分析

语义分析器的职责是:输入AST,输出AST + 符号表

比如程序生成的AST长这样

语义分析器会遍历AST,生成符号表。这个无法查看,你可以大概理解成是这样

然后是在AST表中加上注解

语义分析是一种什么感觉呢?就像拿着一棵语法树边走边做笔记,遇到变量声明就记下来(符号表),遇到变量使用就去查阅笔记,发现有问题就报错

语义分析具体做哪些事情呢?我们所知的如:类型检查、类型转换、生命周期检查、控制流检查、访问权限…完整的如图

类别子任务
符号绑定名称绑定 (Name Binding) 名字唯一性检查 (Duplicate Declaration Check)
符号表管理作用域嵌套管理 (Scope Management) 作用域隐藏检测 (Shadowing Detection)
类型系统类型检查 (Type Checking) 类型推导 (Type Inference, 在某些初始化表达式中) 兼容性检查 (Type Compatibility Checking)
类型转换隐式类型转换检查 (Implicit Conversion) 强制类型转换检查 (Explicit Cast Checking) 常量转换合法性检查
存储类别分析static, extern, auto, register, thread_local
链接属性分析内部链接、外部链接、可见性 (Linkage & Visibility Analysis)
生命周期分析生命周期合法性 (Object Lifetime Analysis) 未初始化变量使用检测
函数分析函数原型一致性检查 (Function Prototype Matching) 返回值类型检查
表达式分析表达式合法性检查 (e.g. lvalue/rvalue合法性) 算子适用性检查 (Operator Applicability)
控制流合法性return合法性 break/continue合法性 goto合法性(禁止跳转到未初始化变量作用域内)
常量表达式分析常量折叠 (Constant Folding) 编译期常量表达式合法性 (Constant Expression Validity, e.g. array size, switch case)
标签与跳转分析goto合法性 标签重复定义检测
结构体与联合体分析成员合法性检查 (Duplicate Member Check) 嵌套结构体合法性
枚举分析枚举值合法性 枚举常量范围分析
数组分析数组维度合法性 数组初始化合法性
指针分析指针类型一致性 不合法解引用检测
语言扩展检查特殊属性语义检查(如:attribute、aligned、packed)
内置与关键字检查内置函数合法性(如 __builtin_*) 关键字非法使用检测
兼容性与标准限制语言标准兼容性检查(如 C89、C99、C11、C17 差异处理)

语义分析是编译器前端中逻辑最复杂、实现难度最高、语言标准依赖最强、对整体编译正确性最关键的阶段。我在我的课程手写编程语言中,做了初步的语义分析,我觉得让大家touch到那个感觉即可

中间代码生成

万事俱备,可以生成中间代码了。比如程序

生成的中间代码长这样

如何查看的呢?

gcc -fdump-tree-all 1.c

前面说了,这样干会生成很多文件,后缀名是.gimple的才是

C语言的中间代码(IR)称为:GIMPLE, 三地址码形式

优化

在编译之前,还要做一件事:优化。我们使用gcc -O配置优化级别,就是在这个阶段完成的

gcc一共提供了5个优化

优化等级大致做了哪些 GIMPLE 优化
-O0几乎不做 GIMPLE 优化
-O1基础常量传播、死代码消除
-O2启动大部分 GIMPLE Pass,常用优化都做了
-O3激进优化(循环展开、向量化等)
-Ofast极限优化(关闭部分标准兼容性保障)

所有的优化任务

类别子任务简要说明
控制流优化CFG简化 (Control Flow Simplification)合并跳转块、消除不可达代码
死代码消除DCE (Dead Code Elimination)消除无用语句
常量传播CCP (Conditional Constant Propagation)将已知常量沿控制流传播
常量折叠Constant Folding编译期计算表达式结果
循环优化Loop Unrolling, Loop Invariant Code Motion (LICM)循环展开、移动不变代码出循环
归纳变量优化Induction Variable Simplification简化循环计数器
变量合并Scalar Replacement of Aggregates (SRA)把 struct 变量拆成普通变量
别名分析Alias Analysis变量间内存相关性分析
逃逸分析Escape Analysis分析是否能栈分配对象
SSA优化SSA Coalescing / Phi Elimination精简 SSA 形式、消除冗余 Phi 函数
全局值优化GVN (Global Value Numbering)消除全局公共子表达式
冗余消除PRE (Partial Redundancy Elimination)消除部分可重用子表达式
循环展开Loop Unrolling展开循环以便减少控制依赖
循环剥离Loop Peeling分离出循环前几次迭代
循环分裂Loop Splitting根据条件分裂不同路径循环
跳转线程化Jump Threading在多个跳转链中合并路径
纯函数优化Pure/Const Function Propagation纯函数调用结果缓存
内联Function Inlining将小函数内联展开
尾调用优化Tail Call Optimization改写尾递归为跳转形式
空间优化Stack Slot Reuse重用局部栈空间
代码移动Store Motion将冗余存储延后或提前
安全检查未定义行为提前检查比如整除零检测

举个例子帮助大家理解编译优化,比如常量折叠,优化前

优化后

看到这,是不是有一种悟的感觉了…

代码生成

代码生成阶段是由代码生成器完成的。代码生成器的职责是:将优化后的中间表示(IR)转换为目标平台的汇编代码

比如代码

生成汇编代码

如果你想查看C语言程序生成的汇编程序,运行时查看反汇编即可,编译时呢?这样查看

image.png

这就是C语言代码生成的all

汇编

这个阶段就是将汇编代码编译成机器码

注意,这时候还不是可执行文件,是目标文件.o

如何得到目标文件呢?gcc -c test.s -o test.o

理解这个阶段非常重要,因为我们写很多底层程序,比如操作系统,需要使用汇编+C语言,就是将汇编程序编译成目标文件,C语言程序编译成目标文件,然后链接成可执行文件,才得到真正的操作系统代码

链接

万事俱备,只欠可执行文件了。链接器启动…

执行链接操作:gcc test.o -o test即可生成可执行文件

如果有多个中间文件,可以将多个中间文件链接:gcc test.o 1.o 2.o -o test

注意,gcc完成链接工作,背后其实是调用链接器ld实现的

以上就是C语言程序编译期的全部,你学废了吗?

后续还会更新更多超硬核文章,感兴趣的话可以关注**【硬核子牙】**微信公众号