shecc
是从零开始构建的,针对32位Arm和RISC-V架构,作为C语言子集的自编译编译器。尽管设计简单,但它作为独立的优化编译器能够执行基本的优化策略。
shecc
能够编译使用以下语法编写的C源文件:
+=
、-=
、*=
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"