|
编程语言一般分为编译期、运行期
有的编程语言是编译期与运行期合并在一起的,比如Python、PHP…即解释型语言
有的则是编译期与运行期分开的,比如C、C++、Java…即编译型语言
今天咱们深入聊聊编译期的C语言
运行期的C语言,我在我之前的课程中已经详细讲过,感兴趣的自己去看视频
编译期,即编译器工作的期间
一般编译器,都是基于《编译原理》实现的 但是C语言的编译器的实现,在《编译原理》基础上做了拓展,如图 接下来详细讲讲C语言编译器的各个阶段
预处理在真正编译C语言程序之前,需要对C语言程序进行预处理,预处理是由集成在编译器中的预处理器完成的
从《编译原理》的角度,词法分析是编译器的第一个阶段。但是从C语言编译器的角度,预处理是第一个阶段
预处理阶段主要完成四件事:删除注释、宏展开、文件包含、条件编译
如果你要做实验论证,gcc -E即可实现,比如 gcc -E 1.c -o 1.i
举个例子吧:删除注释、宏展开,比如代码 预处理以后,注释没有了,宏展开了。C语言的预处理器,就是简单的宏替换 你会发现,预处理器会在开头多生成点东西,这些东西是什么呢?有什么用呢?
这些看似神秘的 # 开头的行,实际上叫做行控制信息(line control directives),是 GCC 预处理器在输出中自动插入的特殊指令,是方便后续编译器、调试器、诊断工具理解源码位置的信息
词法分析经过预处理后,得到的就是完整的C语言程序了,就可以开始编译了。词法分析器启动…
词法分析器的职责是:输入源程序,输出token 来看看上面的程序生成的token 如何查看C语言程序生成的token呢? clang -cc1 -dump-tokens 1.c
你可能想问:为什么用clang,而不是用gcc?因为gcc作为老牌的编译器,不支持这个功能
来看看词法分析器生成token的过程 如果你想透彻理解词法分析的底层原理,你可以使用词法分析工具flex生成词法分析器,去实战。或者自己从零写一个词法分析器,体会将程序的点点滴滴转成token的过程,你的困惑就解开了…(这部分内容,我做的课程手写编程语言中有教,感兴趣的可以咨询班主任jvm-anan)
语法分析拿到源程序对应的token,就可以去做语法分析了。语法分析器启动…
语法分析器的职责是:输入token,输出抽象语法树AST。后面的阶段,都是围绕AST进行的 换个程序,来看看经过语法分析生成的抽象语法树 对应的AST 如何查看呢?两种方式 clang-check 1.c --ast-dump -- -std=c11clang -Xclang -ast-dump -fsyntax-only 1.c
其实gcc也可以,就是生成的文件太多,看起来不直观 gcc -fdump-tree-all -c 1.c
会生成这些文件 来看看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语言程序生成的汇编程序,运行时查看反汇编即可,编译时呢?这样查看 gcc -S 1.c -o 1.s
这就是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语言程序编译期的全部,你学废了吗?
|