深入解析DoctorWkt/acwj项目:从解释器到x86-64汇编代码生成
acwj A Compiler Writing Journey 项目地址: https://gitcode.com/gh_mirrors/ac/acwj
项目背景与目标
DoctorWkt/acwj项目是一个循序渐进构建编译器的教程项目。在第四部分中,我们将实现一个重要的里程碑:将之前的解释器转换为真正的编译器,能够生成x86-64架构的汇编代码。这一转变标志着我们从简单的表达式求值迈向了真正的代码生成阶段。
解释器与编译器的关键区别
在之前的实现中,我们构建了一个AST解释器,它能够递归地遍历抽象语法树并计算结果。解释器的工作方式是即时执行,而编译器的工作方式则是代码生成。两者的核心区别在于:
- 执行时机:解释器在运行时计算,编译器在编译时生成代码
- 输出形式:解释器直接输出结果,编译器输出可执行的机器代码
- 性能特征:解释器每次运行都需要重新解析,编译器只需编译一次
代码生成器的设计思路
通用代码生成框架
我们设计了一个通用的代码生成框架(gen.c
),它负责AST的遍历和调度,而将具体的汇编代码生成委托给平台特定的实现(cg.c
)。这种分层设计使得:
- 前端解析和AST构建保持平台无关
- 后端代码生成可以针对不同架构实现
- 核心逻辑集中在通用层,减少重复代码
寄存器管理策略
x86-64架构有16个通用寄存器,但在我们的初始实现中,我们保守地使用了4个寄存器(%r8-%r11)。寄存器管理包括三个核心操作:
- 分配:标记寄存器为已使用状态
- 释放:标记寄存器为可用状态
- 全部释放:重置所有寄存器状态
这种简单的寄存器分配策略虽然不够高效,但足够让我们开始工作。后续可以改进为更复杂的分配算法。
x86-64汇编代码生成详解
基本操作实现
-
加载立即数:
movq $value, %reg
将常量值加载到指定寄存器
-
加法运算:
addq %src, %dest
结果存储在目标寄存器中
-
减法运算:
subq %src, %dest
从目标寄存器减去源寄存器
-
乘法运算:
imulq %src, %dest
有符号乘法,结果存储在目标寄存器
-
除法运算:
movq %dividend, %rax cqo ; 符号扩展 idivq %divisor ; 结果在%rax
除法需要特殊处理,使用固定寄存器
函数调用约定
为了打印结果,我们需要遵循x86-64的调用约定:
- 第一个参数放在%rdi寄存器
- 使用call指令调用函数
- 被调用函数负责保存寄存器状态
我们的printint
函数就是按照这个约定实现的辅助函数。
示例代码分析
考虑表达式2 + 3 * 5 - 8 / 3
,生成的汇编代码展示了完整的编译流程:
- 计算3*5=15,存储在%r10
- 计算2+15=17,结果保留在%r10
- 计算8/3=2,结果存储在%r8
- 最后计算17-2=15,结果通过%rdi传递给printint
这个例子验证了我们编译器的正确性,生成的汇编代码计算结果与解释器一致。
架构设计的思考
为什么需要AST而不仅仅是即时代码生成?AST提供了几个关键优势:
- 优化机会:可以在生成代码前对AST进行各种优化
- 多目标支持:同一AST可以生成不同架构的代码
- 语言扩展:更容易支持控制流等复杂结构
- 错误检查:可以在代码生成前进行全面的语义分析
后续发展方向
在实现了基本的表达式编译后,我们可以考虑以下扩展:
- 支持变量和赋值语句
- 添加控制流结构(if/while)
- 改进寄存器分配算法
- 增加浮点运算支持
- 实现函数定义和调用
这个项目展示了编译器构建的核心原理,从解释器到真正代码生成的转变是一个重要的里程碑。通过这种循序渐进的方式,我们可以深入理解编译技术的各个方面。
acwj A Compiler Writing Journey 项目地址: https://gitcode.com/gh_mirrors/ac/acwj
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考