编译与静态分析
编译器把源程序转换为能够被计算机执行或被其他工具处理的表示。静态分析则在不执行目标程序的前提下,研究程序的结构、数据依赖与可能的行为。二者共享许多基础技术,其中一个重要连接点就是中间表示。
本教程以 SSA 为线索:先理解单个值的定义,再研究分支、循环、闭包与对象,最后回到数据流和控制流上的分析问题。
从语法到中间表示
一个常见的编译流程可以分为前端、中端与后端。前端处理语言的语法与语义,中端在 IR 上进行分析和变换,后端则将表示转换为目标代码。
这是理解分工的一种常见模型,具体编译器可能采用多个 IR 层次。用于安全分析时,也可以保留 IR 与源码的映射,以便把查询结果对应回原始代码。
为什么讨论 SSA
静态单赋值形式(Static Single Assignment,SSA)让每个 IR 值只有一个定义。源程序中对同一个变量的多次赋值,转化为不同的值版本;在控制流汇合处,用 Phi 节点表达路径与值之间的对应关系。
SSA 并不使所有值都成为编译期常量。它让定义与使用的关系更加明确,为数据流分析提供一种便于操作的结构。
一个常量传播示例
下面的源程序对 x 赋值两次:
x = 5
y = x + 3
x = 10
z = x + y
改写为教学用的 SSA 表示后:
x₁ = 5
y₁ = x₁ + 3
x₂ = 10
z₁ = x₂ + y₁
y₁ 依赖 x₁,z₁ 依赖 x₂ 与 y₁,不需要依靠变量名 x 推测它究竟指哪一次赋值。这个例子中的操作数可以静态确定,因此得到 y₁ = 8、z₁ = 18。
如果输入来自函数参数、外部调用或不同控制路径,分析仍需要传播和合并信息。单一定义不意味着“无需迭代”,也不意味着“所有程序行为在编译时已知”。
从编译优化到安全分析
优化往往关心哪些计算可以简化、哪些结果可以复用;安全分析则常常关心输入影响了哪些操作、关键调用使用了什么值,以及相关防护条件是否成立。
二者都需要研究值与程序结构之间的关系,但分析目标和所需的证据并不相同。不能把一个查询匹配直接等同于完整的漏洞结论。
Yak SSA 与 SyntaxFlow 的分工
- Yak SSA 将源程序编译为用于分析的中间表示,组织值、调用、成员、基本块及相关依赖。
- SyntaxFlow 在这一表示上表达查询和分析规则。
- 源码映射与结果展示 帮助使用者检查规则命中所对应的实际代码。
统一表示减少了查询对具体语法形态的依赖,但不能自动消除不同语言与框架之间的语义差异。讨论具体能力时,应同时考虑编译器对目标语言的建模范围。
本教程的阅读顺序
- 静态单赋值形式:变量版本、基本块和 Phi 节点。
- 高级语言的 SSA 构建:从语言结构进入分析表示。
- 闭包与对象建模:环境、成员与副作用。
- 数据流与跨过程分析:跨越调用边界理解值的传播。
- 控制流、支配与 Phi:区分依赖关系、执行路径和支配关系。
想先获得实践反馈,可以并行完成 SyntaxFlow 快速入门。
原始资料与参考
- Cytron, Ferrante, Rosen, Wegman, Zadeck. Efficiently Computing Static Single Assignment Form and the Control Dependence Graph, 1991。Brown 大学技术报告。该工作系统讨论了 SSA 与控制依赖图的高效构建方法。
- LLVM. Language Reference Manual。中间表示参考与 Phi 指令。
- Yaklang。项目源码。通用编译理论与实际实现应结合阅读。