Go to file
2024-12-08 15:40:44 +08:00
demo test coverage 2024-12-08 11:40:47 +08:00
test fix overflow indirection 2024-12-08 14:34:58 +08:00
.gitignore unit test 2024-12-07 14:13:34 +08:00
boot-native.sh extern 2024-12-06 16:52:31 +08:00
boot.c fix prefix and div-assign 2024-12-08 15:40:44 +08:00
boot.sh extern 2024-12-06 16:52:31 +08:00
bootstrapping.png new readme 2024-11-30 17:47:52 +08:00
cov-boot.sh test coverage 2024-12-08 11:40:47 +08:00
cov-test.sh test coverage 2024-12-08 11:40:47 +08:00
README.md test coverage 2024-12-08 11:40:47 +08:00
run-native.sh extern 2024-12-06 16:52:31 +08:00
run.sh unit test 2024-12-07 14:13:34 +08:00
test.sh test coverage 2024-12-08 11:40:47 +08:00

RVBTCC

2000 行的轻量级自举编译器。

  • 旨在展示如何迅速编写一个自举编译器。
  • 语法类似 C输出 RISC-V 汇编。
  • 仅依赖几个 glibc 函数用于输入输出。
  • 仅作学习用途,请勿在生产环境中使用。

用法

如果你有 RISC-V 真机,可以采用真机运行,否则可以考虑模拟运行。两者行为应当是一致的。

真机运行

编译运行程序src 为本语言源代码。可以编译 demo 或 test 文件夹下的实例。

$ sh run-native.sh <src>

自举编译器,输出的文件位于 build 文件夹中。

$ sh boot-native.sh

模拟运行

安装以下依赖

sudo apt install gcc-12-riscv64-linux-gnu qemu-user qemu-system-misc

编译运行程序src 为本语言源代码。可以编译 demo 或 test 文件夹下的实例。

$ sh run.sh <src>

自举编译器,输出的文件位于 build 文件夹中。

$ sh boot.sh

自举过程

自举会输出六个文件,三个汇编文件和三个可执行文件:

源代码 编译器 汇编 可执行 代号 命名
boot.c gcc gcc.out G 自制编译器
boot.c gcc.out boot1.s boot1.out B1 自举自制编译器
boot.c boot1.out boot2.s boot2.out B2 自举自举自制编译器
boot.c boot2.out boot3.s B3 验证自举自举自制编译器

除了第一次编译全程由 gcc 完成之外,另外三次编译从源码到汇编由本编译器完成,从汇编到可执行文件由 gcc 完成。从汇编到可执行文件时需要将 glibc 链接进去,这对于 gcc 来说是默认的行为。

整个自举及其验证的过程如下图所示:

自举的目标为 G、B1、B2 的可执行文件行为一致,也就是说 B1、B2、B3 的汇编代码一致。

语言文档

注释

支持多行 /* ... */ 和单行 // 两种注释

支持六个基本类型

标量类型 指针类型
void void*
char char*
int int*
  • 注意指针类型不是复合得来的,而是被视作整体。因此也不存在二重指针。
  • 函数和数组不是类型系统的一部分。
    • 可以认为数组的类型就是其元素对应的指针类型。
    • 函数的参数类型和个数不会检查,返回值会参与类型检查。
    • 函数名只能被用于调用,函数调用被视为初等表达式。
  • 数组只支持一维数组,且数组的元素不能是指针类型。
  • 整数和字符字面量的类型是 int,字符串字面量的类型是 char*

支持的流程控制

  • if else
  • while for do
  • break continue
  • return

关键字

本语言包含的关键字即为支持的标量类型的关键字和流程控制的关键字,还有 externenum

extern 关键字

extern 在全局函数和变量的声明的开头中可以使用。

全局函数的声明和定义都会直接忽略这个关键字。全局函数的声明和定义由是否提供函数体决定,与该关键字无关。

全局变量如果使用了这个关键字,则有以下特性和限制:

  • 变量仅被声明,而没有被定义。
    • 如果需要使用这样的变量,需要稍后提供定义,或在外部已经定义。
  • 不可以初始化。
  • 不可是数组。

enum 关键字

用于定义整数常量。enum 的名字必须省略,因此不能用于定义枚举类型。

整数常量可以用于数组大小、全局变量初始化等需要常量的地方。

支持以下运算符

运算符 含义 结合性
() 初等表达式(字面量、标识符、函数调用、括号)
++ -- [] 后缀自增自减 数组下标 从左到右
++ -- + - * & ! ~ 前缀自增自减 正负号 取地址 解引用 逻辑非 按位非 从右到左
* / % 乘除余 从左到右
+ - 加减 从左到右
<< >> 左移和算术右移 从左到右
< <= > >= 关系比较 从左到右
== != 相等比较 从左到右
& 按位与 从左到右
^ 按位异或 从左到右
| 按位或 从左到右
&& 逻辑与 从左到右
|| 逻辑或 从左到右
?: 条件 从右到左
= += -= *= /= %= <<= >>= &= ^= |= 赋值 从右到左
, 逗号 从左到右
  • 同级表达式的求值顺序与结合性一致。
  • 加减号支持整数之间,指针与整数,指针之间的运算。
  • 算术运算的结果总是被提升为 int 类型。布尔值用 int 类型表示。
  • 由于空指针就是 0,因此指针和整数之间的比较运算没有禁止。
  • 逻辑与和逻辑或支持短路求值。

其它支持与不支持

  • 支持全局变量和局部变量,局部变量遮挡全局变量。
  • 不支持局部变量之间的遮挡,重名的局部变量为同一变量。
  • 函数只支持最多八个参数。函数声明中支持可变参数,仅用于兼容 C 语言库。
  • 类型检查有遗漏,若 C 编译器报错,而本语言编译通过,就可以认为是 UB。
    • 例如函数调用的参数和 return 语句不会检查类型。

限制

编译过程中涉及的以下参数:

  • 符号表总长度、字符串表总长度
  • 符号数、字符串数、局部变量数、(虚拟)寄存器数

不能超过源代码中指定的常数。

  • 目前源代码中的常数能够保证自举成功。如果有必要可以将它们适度加大。
  • 该设计保证了没有任何的动态内存分配。如果愿意,可以将它们改为 mallocfree 动态管理,本语言是完全支持的。

依赖

直接依赖下面这些 C 语言库函数和变量,在本语言中提供声明后调用。

  • printf

  • getchar

  • exit

  • ungetcstdin(理论上非必须,可以在本语言中手动模拟)

  • fprintfstderr(理论上非必须,仅用于输出错误信息)

测试

单元测试

直接运行

$ sh test.sh

覆盖率

安装如下可视化工具

$ pip install gcovr

然后

$ sh cov-boot.sh

$ sh cov-test.sh

就会在 cov 文件夹下生成自举或测试的 coverage 数据