gcc编译器的C语言

今天用一篇文章告诉你gcc编译器眼中的C语言是什么样的。别眨眼,开始
编程语言一般分为编译期、运行期。有的编程语言是编译期与运行期合并在一起的,比如Python、PHP…即解释型语言。有的则是编译期与运行期分开的,比如C、C++、Java…即编译型语言
今天咱们深入聊聊编译期的C语言。运行期的C语言,我在我之前的课程中已经详细讲过,感兴趣的自己去看视频
编译期,即编译器工作的期间。一般编译器,都是基于《编译原理》实现的
 
 
添加图片注释,不超过 140 字(可选)
但是C语言的编译器的实现,在《编译原理》基础上做了拓展,如图
 
 
 
添加图片注释,不超过 140 字(可选)
接下来详细讲讲C语言编译器的各个阶段
01 预处理
在真正编译C语言程序之前,需要对C语言程序进行预处理,预处理是由集成在编译器中的预处理器完成的
从《编译原理》的角度,词法分析是编译器的第一个阶段。但是从C语言编译器的角度,预处理是第一个阶段
预处理阶段主要完成四件事:删除注释、宏展开、文件包含、条件编译
如果你要做实验论证,gcc -E即可实现,比如:gcc -E 1.c -o 1.i
举个例子吧:删除注释、宏展开,比如代码
 
 
添加图片注释,不超过 140 字(可选)
预处理以后,注释没有了,宏展开了。C语言的预处理器,就是简单的宏替换
 
 
添加图片注释,不超过 140 字(可选)
你会发现,预处理器会在开头多生成点东西,这些东西是什么呢?有什么用呢?
这些看似神秘的 # 开头的行,实际上叫做行控制信息(line control directives),是 GCC 预处理器在输出中自动插入的特殊指令,是方便后续编译器、调试器、诊断工具理解源码位置的信息
02 词法分析
经过预处理后,得到的就是完整的C语言程序了,就可以开始编译了。词法分析器启动…
词法分析器的职责是:输入源程序,输出token
 
 
添加图片注释,不超过 140 字(可选)
来看看上面的程序生成的token
 
 
添加图片注释,不超过 140 字(可选)
如何查看C语言程序生成的token呢?clang -cc1 -dump-tokens 1.c
你可能想问:为什么用clang,而不是用gcc?因为gcc作为老牌的编译器,不支持这个功能
来看看词法分析器生成token的过程
 
 
添加图片注释,不超过 140 字(可选)
如果你想透彻理解词法分析的底层原理,你可以使用词法分析工具flex生成词法分析器,去实战。或者自己从零写一个词法分析器,体会将程序的点点滴滴转成token的过程,你的困惑就解开了…(这部分内容,我做的课程手写编程语言中有教,感兴趣的可以咨询班主任jvm-anan)
03 语法分析
拿到源程序对应的token,就可以去做语法分析了。语法分析器启动…
语法分析器的职责是:输入token,输出抽象语法树AST。后面的阶段,都是围绕AST进行的
 
 
添加图片注释,不超过 140 字(可选)
换个程序,来看看经过语法分析生成的抽象语法树
 
 
添加图片注释,不超过 140 字(可选)
对应的AST
 
 
添加图片注释,不超过 140 字(可选)
如何查看呢?两种方式:clang-check 1.c --ast-dump -- -std=c11;clang -Xclang -ast-dump -fsyntax-only 1.c
其实gcc也可以:gcc -fdump-tree-all -c 1.c,就是生成的文件太多,看起来不直观
会生成这些文件
 
 
添加图片注释,不超过 140 字(可选)
来看看token生成AST的过程,比如print语句
 
 
添加图片注释,不超过 140 字(可选)
如果你想透彻理解语法分析的底层原理,你可以使用语法分析工具bison生成语法分析器,去实战。或者自己从零写一个语法分析器,体会将token转成AST的过程,你的困惑就解开了…(这部分内容,我做的课程手写编程语言中有教,感兴趣的可以咨询班主任jvm-anan)
04 语义分析
语义分析器的职责是:输入AST,输出AST + 符号表
 
 
添加图片注释,不超过 140 字(可选)
比如程序生成的AST长这样
 
 
添加图片注释,不超过 140 字(可选)
语义分析器会遍历AST,生成符号表。这个无法查看,你可以大概理解成是这样
 
 
添加图片注释,不超过 140 字(可选)
然后是在AST表中加上注解
 
 
添加图片注释,不超过 140 字(可选)
语义分析是一种什么感觉呢?就像拿着一棵语法树边走边做笔记,遇到变量声明就记下来(符号表),遇到变量使用就去查阅笔记,发现有问题就报错
语义分析具体做哪些事情呢?我们所知的如:类型检查、类型转换、生命周期检查、控制流检查、访问权限…完整的如图
类别
子任务
符号绑定
名称绑定 (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到那个感觉即可
06 中间代码生成
万事俱备,可以生成中间代码了。比如程序
 
 
添加图片注释,不超过 140 字(可选)
生成的中间代码长这样
 
 
添加图片注释,不超过 140 字(可选)
如何查看的呢?gcc -fdump-tree-all 1.c
前面说了,这样干会生成很多文件,后缀名是.gimple的才是
C语言的中间代码(IR)称为:GIMPLE, 三地址码形式
07 优化
在编译之前,还要做一件事:优化。我们使用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
将冗余存储延后或提前
安全检查
未定义行为提前检查
比如整除零检测
举个例子帮助大家理解编译优化,比如常量折叠,优化前
 
 
添加图片注释,不超过 140 字(可选)
优化后
 
 
添加图片注释,不超过 140 字(可选)
看到这,是不是有一种悟的感觉了…
09 代码生成
代码生成阶段是由代码生成器完成的。代码生成器的职责是:将优化后的中间表示(IR)转换为目标平台的汇编代码
 
 
添加图片注释,不超过 140 字(可选)
比如代码
 
 
添加图片注释,不超过 140 字(可选)
生成汇编代码
 
 
添加图片注释,不超过 140 字(可选)
如果你想查看C语言程序生成的汇编程序,运行时查看反汇编即可,编译时呢?这样查看:gcc -S 1.c -o 1.s
这就是C语言代码生成的all
10 编译
这个阶段就是将汇编代码编译成机器码
 
 
添加图片注释,不超过 140 字(可选)
注意,这时候还不是可执行文件,是目标文件.o
 
 
添加图片注释,不超过 140 字(可选)
如何得到目标文件呢?gcc -c test.s -o test.o
 
 
添加图片注释,不超过 140 字(可选)
理解这个阶段非常重要,因为我们写很多底层程序,比如操作系统,需要使用汇编+C语言,就是将汇编程序编译成目标文件,C语言程序编译成目标文件,然后链接成可执行文件,才得到真正的操作系统代码
11 链接
万事俱备,只欠可执行文件了。链接器启动…
 
 
添加图片注释,不超过 140 字(可选)
执行链接操作:gcc test.o -o test即可生成可执行文件
 
 
添加图片注释,不超过 140 字(可选)
如果有多个中间文件,可以将多个中间文件链接:gcc test.o 1.o 2.o -o test
注意,gcc完成链接工作,背后其实是调用链接器ld实现的
以上就是C语言程序编译期的全部,你学废了吗?
 
参考文献链接
posted @ 2025-06-14 19:58  吴建明wujianming  阅读(40)  评论(0)    收藏  举报