shecc : 自托管教育型C语言优化编译器
简介
shecc
是从零开始构建的,针对32位Arm和RISC-V架构,作为C语言子集的自编译编译器。尽管设计简单,但它作为独立的优化编译器能够执行基本的优化策略。
特性
- 为ARMv7-A和RV32IM生成可执行的Linux ELF二进制文件。
- 提供最小的C标准库,用于GNU/Linux上的基本I/O操作。
- 交叉编译器使用ANSI C编写,与大多数平台兼容。
- 包含一个自包含的C前端,集成了机器代码生成器;无需外部汇编器或链接器。
- 采用两遍编译过程:第一遍检查语法并将复杂语句分解为基本操作,第二遍将这些操作转换为Arm/RISC-V机器代码。
- 开发了一个适用于RISC风格架构的寄存器分配系统。
- 实现了一个与架构无关的、基于静态单赋值 (SSA)的中间端,以增强优化。
兼容性
shecc
能够编译使用以下语法编写的C源文件:
- 数据类型:char、int、struct和指针
- 条件语句:if、while、for、switch、case、break、return和一般表达式
- 复合赋值:
+=
、-=
、*=
- 支持的数据类型的全局/局部变量初始化
- 例如
int i = [expr]
- 例如
- 对预处理器指令的有限支持:
#define
、#ifdef
、#elif
、#endif
、#undef
和#error
- 带有
__VA_ARGS__
标识符的非嵌套可变参数宏
后端针对带有Linux ABI的armv7hf,在树莓派3上验证,并且还支持RISC-V 32位架构,通过QEMU验证。
自举
验证shecc
自举的步骤:
stage0
:shecc
源代码最初使用普通编译器编译,生成本机可执行文件。生成的编译器可用作交叉编译器。stage1
:构建的二进制文件读取自身的源代码作为输入,并生成ARMv7-A/RV32IM二进制文件。stage2
:生成的ARMv7-A/RV32IM二进制文件(通过QEMU或在Arm和RISC-V设备上运行)以自身源代码为输入,生成另一个ARMv7-A/RV32IM二进制文件。bootstrap
:构建stage1
和stage2
编译器,并验证它们是否逐字节相同。如果相同,则shecc
可以编译自身的源代码并生成该程序的新版本。
先决条件
shecc
中的代码生成器不依赖外部工具。你只需要普通的C编译器,如gcc
和clang
。然而,shecc
会自举,因此需要Arm/RISC-V ISA模拟。在GNU/Linux上安装QEMU以进行Arm/RISC-V用户模拟:
$ sudo apt-get install qemu-user
在macOS或Microsoft Windows上构建shecc
仍然可行。但由于缺少qemu-arm
,第二阶段自举将失败。
要执行快照测试,请安装以下软件包:
$ sudo apt-get install graphviz jq
构建和验证
配置你想要的后端,shecc
支持ARMv7-A和RV32IM后端:
$ make config ARCH=arm
# 目标机器代码切换到Arm
$ make config ARCH=riscv
# 目标机器代码切换到RISC-V
运行make
,你应该看到这样的输出:
CC+LD out/inliner
GEN out/libc.inc
CC out/src/main.o
LD out/shecc
SHECC out/shecc-stage1.elf
SHECC out/shecc-stage2.elf
文件out/shecc
是第一阶段编译器。其用法:
$ shecc [-o output] [+m] [--no-libc] [--dump-ir] <infile.c>
编译器选项:
-o
: 指定输出文件名(默认:out.elf
)+m
: 使用硬件乘除法指令(默认:禁用)--no-libc
: 排除嵌入式C库(默认:嵌入)--dump-ir
: 转储中间表示(IR)
示例:
$ out/shecc -o fib tests/fib.c
$ chmod +x fib
$ qemu-arm fib
通过在调用make
时指定check-snapshots
目标来验证发出的IR是否与快照相同:
$ make check-snapshots
shecc
附带单元测试。要运行测试,给出check
作为参数:
$ make check
参考输出:
...
int main(int argc, int argv) { exit(sizeof(char)); } => 1
int main(int argc, int argv) { int a; a = 0; switch (3) { case 0: return 2; case 3: a = 10; break; case 1: return 0; } exit(a); } => 10
int main(int argc, int argv) { int a; a = 0; switch (3) { case 0: return 2; default: a = 10; break; } exit(a); } => 10
OK
要清理生成的编译器文件,执行命令 make clean
。
要重置架构配置,使用命令 make distclean
。
中间表示
当向 shecc
传递 --dump-ir
选项时,将生成中间表示(IR)。以文件 tests/fib.c
为例。它包含一个递归的斐波那契数列函数。
int fib(int n)
{
if (n == 0)
return 0;
else if (n == 1)
return 1;
return fib(n - 1) + fib(n - 2);
}
执行以下命令生成IR:
$ out/shecc --dump-ir -o fib tests/fib.c
C源代码和IR的逐行解释:
C源代码 IR 解释
------------------+---------------------------------------+----------------------------------------------------------------------------------
int fib(int n) def int @fib(int %n) 表示函数定义
{ {
if (n == 0) const %.t1001, $0 将常量0加载到临时变量".t1001"中
%.t1002 = eq %n, %.t1001 测试"n"是否等于".t1001",并将结果写入临时变量".t1002"
br %.t1002, .label.1177, .label.1178 如果".t1002"等于零,跳转到假标签".label.1178",否则
跳转到真标签".label.1177"
.label.1177
return 0; const %.t1003, $0 将常量0加载到临时变量".t1003"中
ret %.t1003 返回".t1003"
j .label.1184 跳转到endif标签".label.1184"
.label.1178
else if (n == 1) const %.t1004, $1 将常量1加载到临时变量".t1004"中
%.t1005 = eq %n, %.t1004 测试"n"是否等于".t1004",并将结果写入临时变量".t1005"
br %.t1005, .label.1183, .label.1184 如果".t1005"等于零,跳转到假标签".label.1184"。否则,
跳转到真标签".label.1183"
.label.1183
return 1; const %.t1006, $1 将常量1加载到临时变量".t1006"中
ret %.t1006 返回".t1006"
.label.1184
return
fib(n - 1) const %.t1007, $1 将常量1加载到临时变量".t1007"中
%.t1008 = sub %n, %.t1007 从"n"中减去".t1007",并将结果存储在临时变量".t1008"中
push %.t1008 为函数调用准备参数
call @fib, 1 调用函数"fib",带一个参数
+ retval %.t1009 将返回值存储在临时变量".t1009"中
fib(n - 2); const %.t1010, $2 将常量2加载到临时变量".t1010"中
%.t1011 = sub %n, %.t1010 从"n"中减去".t1010",并将结果存储在临时变量".t1011"中
push %.t1011 为函数调用准备参数
call @fib, 1 调用函数"fib",带一个参数
retval %.t1012 将返回值存储在临时变量".t1012"中
%.t1013 = add %.t1009, %.t1012 将".t1009"和".t1012"相加,并将结果存储在临时变量".t1013"中
ret %.t1013 返回".t1013"
} }
已知问题
- 生成的ELF缺少.bss和.rodata段
- 对变参函数的支持不完整。无法使用
<stdarg.h>
。 或者,可以查看源文件lib/c.c
中printf
的实现,了解var_arg
的使用。 - C前端有些混乱,因为没有有效的AST。
许可证
shecc
可在BSD 2条款许可证下自由重新分发。
本源代码的使用受BSD风格许可证的约束,可以在LICENSE
文件中找到。