c51编译器编写日记
<h1>c51编译器编写日记</h1><h2>0. 前言</h2>
<pre><code>之前在做c51汇编编译器的时候就在想, 啥时候也做一个c语言编译器, 然后就挖坑了.
正好最近发现一个项目(MAZUCC)的教学目的的pc端编译器, 语法分析写的很清晰,
就尝试写一下c51的线性寄存器分配模型, 发现似乎可行, 就开启了填坑的工作.
本日志算是记录, 算是督促.
欢迎star: https://github.com/peitianyu/c51cc
大家测试中的问题可以贴在帖子下方
</code></pre>
<h2>1. 测试并梳理标准(MAZUCC)所缺语法</h2>
<p>一、表达式与运算符</p>
<pre><code>一元:
有:+ - ++ -- * & !
无:sizeof (type)cast typeof ~
二元:
有:+ - * / % << >> < > ==& | && || =
无:^ . -> <<= >>= &= |= ^= >= <= != += -= *= /= %=
三元:
有:?:
常量 & 字面量
有:十进制, 字符, 字符串, 十进制浮点
无:十六进制(0x12), 不支持二进制(0b00010002), 八进制整型, 不支持后缀{L, f等}
</code></pre>
<p>二、声明与类型系统</p>
<pre><code>基础类型
有:char int float double
无:signed/unsigned
聚合类型
有:结构体变量, 联合体, 一维/多维数组, 指针
无:结构体, 联合体变量不支类型声明, 不支持位域, 不支持{}初始化
修饰符(全部禁用)
存储类:auto register static extern typedef
类型限定:const volatile restrict _Atomic
函数限定:inline _Noreturn
</code></pre>
<p>三、语句与控制流</p>
<pre><code>有:if else for return
无:
else if(语法可写,但按嵌套 if-else 理解)
while do-while switch/case/default break continue goto
</code></pre>
<p>四、函数</p>
<pre><code>有:
普通函数定义、递归、函数指针
无:
void foo(void){} 空形参列表写法(必须空括号)
可变参数 ...、<stdarg.h>
inline、_Noreturn、函数原型默认参数提升规则外的任何扩展
</code></pre>
<p>五、预处理 & 编译单元</p>
<pre><code>有:无(整个预处理器被移除)
无:
</code></pre>
<p>六、标准库</p>
<pre><code>有:
无(整个标准库不存在)
</code></pre>
<p>七、存储模型与生命周期</p>
<pre><code>有:
全局变量(程序整个生命期)
局部变量(进入复合语句时创建,退出即销毁)
指针算术与任意层级指针
无:
静态局部变量、static 全局、extern 跨文件链接
动态内存 malloc/free、_Thread_local、线程存储期
</code></pre>
<p>八、单片机专属扩展</p>
<pre><code>无: sfr sfr16 sbit bit data xdata idata bdata code reentrant interrupt using
</code></pre>
<h2>2. 搭建设计测试框架</h2>
<p>之前我一直希望有一个类似于rust的 <code>cargo run</code>与 <code>cargo test</code>的测试框架, 正好之前实现过一个c_test工具.<br />
但发现还是没那么好用, 且不支持tcc编译, 查资料发现c语言中有一个 <code>section</code>, 可以直接将变量在编译阶段就预设好, 这就很nice了.<br />
于是就有了接下来的测试框架, 代码非常少, 单头文件, 宏定义实现, 支持tcc与gcc编译. 非常符合我的审美了.</p>
<pre><code class="language-c">/* minitest.h */
#ifndef MINITEST_H
#define MINITEST_H
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#ifdef MINITEST_IMPLEMENTATION
#ifdef __TINYC__
__asm__(".global __start_testsec\n"
"__start_testsec = .\n"
".global __stop_testsec\n"
"__stop_testsec = .\n");
#undef __attribute__
#endif
extern void *__start_testsec;
extern void *__stop_testsec;
typedef void (*TestFn)(void);
typedef struct { const char *name; TestFn fn; } Test;
extern int g_argc;
extern char **g_argv;
#define TEST(suite, name) \
static void suite##_##name(void); \
static const Test __t_##suite##name \
__attribute__((section("testsec"))) = { \
#suite "." #name, suite##_##name }; \
static void suite##_##name(void)
#define ASSERT(cond) do { if (!(cond)) { printf("FAIL %s:%d%s\n", __FILE__, __LINE__, #cond); exit(1); } } while (0)
#define ASSERT_TRUE(x) ASSERT(x)
#define ASSERT_EQ(a, b) ASSERT((a) == (b))
#define ASSERT_STREQ(a,b) ASSERT(strcmp((a),(b)) == 0)
#define RUN_ALL_TESTS(argc, argv) \
do { \
g_argc = (argc); \
g_argv = (argv); \
Test *b = (Test*)&__start_testsec, *e = (Test*)&__stop_testsec; \
size_t n = e - b; \
if (!n) { puts("No tests"); break; } \
printf("\n\033[36m===== Tests =====\033[0m\n"); \
for (size_t i = 0; i < n; ++i) printf("\033.name); \
printf("\033[35m# (0=all, ENTER=quit): \033[0m"); fflush(stdout); \
int c = getchar(); \
if (c == '\n' || c == EOF) break; \
ungetc(c, stdin); \
int k;if (scanf("%d", &k) != 1) break; \
while (getchar() != '\n');/* 吃掉行尾 */ \
if (k < 0 || (size_t)k > n) { puts("\033[31mBad#\033[0m"); break; } \
for (size_t i = (k ? k - 1 : 0), lim = (k ? i + 1 : n); i < lim; ++i) { \
printf("\033\033.name); b.fn(); \
printf("\033\033.name); \
} \
} while (0)
#else
#define ASSERT(cond) do { if (!(cond)) { printf("FAIL %s:%d%s\n", __FILE__, __LINE__, #cond); exit(1); } } while (0)
#define ASSERT_TRUE(x) ASSERT(x)
#define ASSERT_EQ(a, b) ASSERT((a) == (b))
#define ASSERT_STREQ(a,b) ASSERT(strcmp((a),(b)) == 0)
#define TEST(suite, name) static void suite##_##name(void)
#define RUN_ALL_TESTS(argc, argv) do{}while(0)
#endif
#endif /* MINITEST_H */
/* main.c */
#ifdef MINITEST_IMPLEMENTATION
#include "core/minitest.h"
int g_argc;
char **g_argv;
void main(int argc, char **argv) {
RUN_ALL_TESTS(argc, argv);
}
#else
#include "stdio.h"
int main() {
printf("hello main\n");
return 0;
}
#endif
/* run.sh */
tcc main.c core/*.c -run -DMINITEST_IMPLEMENTATION
</code></pre>
<h2>3. 一些想法与设计思路</h2>
<ol>
<li>关键词:</li>
</ol>
<pre><code>关键词
将仅支持储存关键词(data, idata, pdata, xdata, edata, code)
register标识将直接代替sfr, sfr16, sbit, 使用方式类似 register (int, char, bit) -> (sfr16, sfr, sbit)
不再使用interrupt, using关键字, 直接使用特定函数表示, void interupt_func(const char* name, int bank_id)
因此语法将不直接支持keil等
</code></pre>
<ol start="2">
<li>实现流程:</li>
</ol>
<pre><code>1. 完善前端语法器ast(暂不支持预编译)
2. bril思路做常规优化后端
3. 特化为硬件相关指令, 做硬件相关优化
4. 线性扫描寄存器分配
5. 生成汇编
6. 实现预编译
7. 生成链接器
8. 生成烧录文件(HEX)
9. 优化代码结构, 并提供一些有趣的实现
</code></pre>
<h2>4. 前端语法器实现</h2>
<p>前端处理没啥好说的, 按需要一个个添加即可</p>
<pre><code>✔ 添加struct union定义ast, 并展示 @done(26-01-12 21:54)
✔ struct顺序初始化, {0};初始化 @done(26-01-13 17:50)
✔ struct指定初始化器初始化 @done(26-01-13 17:50)
✔ char int 初始化 @done(26-01-13 22:49)
✔ float double初始化 @done(26-01-13 22:49)
✔ ptr初始化 @done(26-01-13 22:49)
✔ 二元运算左右符号推导输出符号 @done(26-01-13 22:51)
✔ 添加类型修饰 const volatile restrict unsigned register static extern typedef修饰符支持 @done(26-01-14 17:47)
✔ 添加函数限定 inline noreturn @done(26-01-14 17:51)
✘ 添加sfr sfr16 sbit bit data xdata idata bdata code reentrant interrupt using等修饰符支持 @cancelled(26-01-14 17:52)
✔ 添加对于void foo(void){}语句支持 @done(26-01-12 21:00)
✔ 增加更加清晰的调试打印 @done(26-01-14 21:21)
✔ 添加常量类型支持 (16进制, 2进制) @done(26-01-14 21:52)
✔ 添加var与init的检查 @done(26-01-15 11:39)
✔ 添加变量重复定义, 未定义检测 @done(26-01-15 12:34)
✔ 添加函数重复定义与未声明检测 @done(26-01-15 12:51)
✔ 添加对于bool的支持 (这部分在c51中表示为bit) @done(26-01-15 13:20)
✔ 添加enum类型支持 @done(26-01-15 18:03)
✔ 添加typedef类型检测 @done(26-01-15 18:46)
✔ 添加控制语句 while do-while continue breakelse if @done(26-01-15 22:29)
✔ 添加goto语句 @done(26-01-15 23:04)
✔ 添加switch/case/default语句 @done(26-01-16 08:33)
✔ 添加case 1 ... 9:语法支持 @done(26-01-16 09:13)
✔ 添加位域的支持 @done(26-01-16 10:36)
✔ 添加判断>= <= !=支持 @done(26-01-16 11:01)
✔ 多维数组序列初始化 @done(26-01-16 11:22)
✔ 添加表达式 sizeof @done(26-01-16 15:46)
✔ 添加~ ^支持 @done(26-01-16 16:16)
✔ (type)cast @done(26-01-16 16:32)
✔ 函数声明检查 @done(26-01-17 12:59)
☐ <<= >>= &= |= ^= += -= *= /= %=
☐ 添加`,`运算符支持
☐ 添加对可变参数支持
☐ 添加常用库支持
☐ 添加编译器拓展
☐ 常用attribute子项
☐ section
☐ alias
☐ aligned
☐ asm指令嵌入支持
☐ 添加typeof语句
☐ 添加typestr语句
☐ 添加defer语句
</code></pre>
<p>暂时前端先这么着, 开始中间代码与后端的实现部分</p>
C51编译器 有开源的 SDCC-51,
能否直接 考虑从 SDCC-51 到 支持 STC32G/AI8051U,
或从 GNU C++ 出发 神农鼎 发表于 2026-1-14 21:49
C51编译器 有开源的 SDCC-51,
能否直接 考虑从 SDCC-51 到 支持 STC32G/AI8051U,
或从 GNU C++ 出发 ...
最后的输出应该是多平台支持的(类似tinyc), 只是编译选项的问题, 初步计划是以mcs51为开端, 后面会逐步添加 神农鼎 发表于 2026-1-14 21:49
C51编译器 有开源的 SDCC-51,
能否直接 考虑从 SDCC-51 到 支持 STC32G/AI8051U,
或从 GNU C++ 出发 ...
另外为什么没从sdcc或者gnu开始的原因是,
看代码理解他们架构的时间往往已经够我自己实现一一遍,
他们过于庞大了(不过应该会参考sdcc中的asm优部分),
且可自由发挥的空间也小了,
毕竟设计这个的初衷是玩, 学习
{:4_269:}
握爪,做C251编译器也有一段时间了
感觉是个持久战
越后面进展越慢{:4_165:}
gentleman 发表于 2026-1-15 08:51
握爪,做C251编译器也有一段时间了
感觉是个持久战
越后面进展越慢
哈哈哈, 已经做好心里准备 <h2>5. 中间语言设计</h2>
<p>这里采用三地址码的设计, 配合ssa, 但会参考bril的设计, 将这一部分拆出来方便独立的调试优化</p>
<h3>1. 类型</h3>
<p>类型支持常见的int, uint, 以及float</p>
<ul>
<li>int<bit_size> uint<bit_size> float<bit_size></li>
</ul>
<h3>2. 修饰符</h3>
<p>思路是能转换就转换出去, 不能就储存到 <code>attributes</code> 字段中</p>
<ul>
<li>const 仅用于前端检查, 不再往下传</li>
<li>typedef 纯前端语法糖, 后端完全不处理</li>
<li>extern 主要在前端处理, 表声明使用, 避免单文件编译阶段报错, 留给链接阶段, bril中不体现</li>
<li>signed/unsigned bril中添加 <code>"unsigned": true</code>, 字段用于后边生成代码阶段解析</li>
<li>static 分为 <code>file.static</code>与 <code>func.static</code> 这部分将直接在json的globals中体现</li>
<li>restrict 在bril中添加 <code>"attributes": ["restrict"]</code>, 之后在alias-analysis pass 可读取该标记, 假设"无别名"做激进优化, c51中不处理</li>
<li>register/data bril中添加 <code>"attributes": ["register", "xdata"]</code>, 之后在代码生成阶段直接按需处理</li>
<li>volatile bril中添加 <code>"attributes": ["volatile"]</code>, 表示后端优化 pass(死码删除, 公共子表达式, 寄存器分配)看到该字段即跳过</li>
<li>inline 在bril中添加 <code>"attributes": ["inline"]</code></li>
<li>noreturn 在bril中添加 <code>"attributes": ["noreturn"]</code>, 后续“死码消除”pass 利用它</li>
</ul>
<h3>3. ssa编码</h3>
<p>不再使用三趟式(Cytron)输出ssa, 使用单趟式的算法, 这里用作者名(Braun算法)实现(可以在todo文件夹找到文章braun13cc.pdf),<br />
注意这里为了简单, 在braun算法的后半段将不做phi函数合并, 因此最好代码实现是可规约的(简单理解为是不使用goto语句),<br />
否则可能会产生一些多余语句.</p>
<p>解释一下braun算法:</p>
<ol>
<li>ast节点流式进入</li>
<li>翻译基本块, 变量定义时记录记忆, 并对变量ssa</li>
<li>在读取变量时按需方向查找记忆</li>
<li>在存在多个前驱时插入phi函数</li>
<li>封口时补phi函数操作数, 若可折叠则直接删除phi函数</li>
</ol>
<p>这种算法的优点是, 最后整理代码的时候, 可以直接将构造ast与ssa构造合到一块做, 只需单趟就可以输出简单优化后的ssa,<br />
且之后可以直接转特定平台即可, 编译速度超快, 有点tinyc那味了, 且这种算法还可以支持解释编译(jit)</p>
<p>该算法天生支持一些优化:</p>
<ol>
<li>自动的剪枝SSA</li>
<li>平凡φ函数即时消除</li>
<li>常量传播与折叠的协同优化</li>
<li>公共子表达式消除(CSE)的自然集成</li>
</ol>
<p>不过只能进行局部优化, 全局优化还得后期优化</p>
<p>todolist</p>
<pre><code>✔ 初步完成ssa结构体设计 @done(26-01-19 20:05)
✔ 设计ssa生成框架 @done(26-01-20 17:52)
☐ 逐步补全ssa生成代码
踩了一些坑, 重新研究学习braun算法
✔ 完成ast->ssa的braun算法学习 @done(26-01-23 14:54)
✔ 尝试编写单独算法测试模块 @done(26-01-23 14:54)
✔ 参考测试模块编写cc模块的ast转ssa算法代码 @done(26-01-27 15:20)
✔ 完成基础架构搭建 @done(26-01-27 15:20)
✔ 完成基本块代码编写 @done(26-01-27 15:20)
✔ 完成if测试 @done(26-01-27 15:20)
✔ 完成while测试 @done(26-01-27 15:20)
✔ 完成混合模式测试 @done(26-01-27 15:20)
☐ 需要处理ctypeattr类型在ssa中优化问题
✔ const: 作为只读提示(可选)与检查 - SSA类型透传与折叠基础 @done(26-01-29 10:26)
✔ const: 全局只读load折叠 @done(26-01-29 10:33)
✔ volatile: load/store禁止删除、合并、重排 (DCE已接入) @done(26-01-29 10:05)
✘ restrict: 预留别名分析入口(可忽略) @cancelled(26-01-29 16:42)
✔ static: 区分文件级/函数内(链接性/存储期) - SSA结构已接入 @done(26-01-29 10:05)
✔ extern: 仅声明符号引用处理 - extern-only已跳过全局收集 @done(26-01-29 10:05)
✔ unsigned: 影响zext/sext与比较(常量折叠已支持) @done(26-01-29 10:26)
✔ register(sfr/sbit/sfr16): 视为MMIO/volatile (DCE已接入) @done(26-01-29 10:05)
✔ data(含xdata/idata/edata/code): 地址空间标记透传(SSA打印支持) @done(26-01-29 10:12)
✘ typedef: 仅解析层,不进入SSA优化 @cancelled(26-01-29 16:43)
✔ inline: 标记函数属性(预留内联优化) - SSA结构已接入 @done(26-01-29 10:05)
✔ noreturn: 控制流优化(消除后继/死代码) - 入口/调用处已接入 @done(26-01-29 10:05)
✔ 注意添加全局变量处理 @done(26-01-29 17:12)
</code></pre>
<h3>4. 预处理模块实现</h3>
<p>这部分按语法规则添加即可<br />
待办事项</p>
<pre><code>✔ 创建预处理器模块 (pp.c) @done(26-01-28 09:26)
✔ #include - 文件包含 @done(26-01-28 09:26)
✔ #define - 对象式宏定义 @done(26-01-28 09:26)
✔ #define - 函数式宏定义(解析参数) @done(26-01-28 09:26)
✔ #undef - 取消宏定义 @done(26-01-28 09:26)
✔ #ifdef/#ifndef/#else/#endif - 条件编译 @done(26-01-28 09:26)
☐ #if/#elif - 常量表达式条件编译
☐ ## 运算符 - 标记粘贴(token pasting)
☐ # 运算符 - 字符串化(stringification)
☐ 变参宏 - __VA_ARGS__ 支持
☐ 预定义宏 - __FILE__, __LINE__, __DATE__, __TIME__
☐ #error/#warning - 错误/警告指令
☐ #pragma - 编译器指令
☐ 宏展开递归保护 - 防止无限递归
☐ 函数式宏参数展开 - 当前只解析不展开参数
☐ 处理由于预处理导致的行号/文件名变化
</code></pre>
<h3>5. 后端代码生成设计</h3>
<p>后端代码生成分为这样几个步骤:</p>
<pre><code>1. 先使用ssa_pass优化代码流程
2. 根据8051指令集选择指令
3. 线性扫描寄存器分配
4. 指令降级/展开
5. 窥孔优化
6. 汇编器: ASM → ObjFile(段/符号/重定位)
7. 链接器:布局与重定位
8. 输出 ASM/HEX
</code></pre>
peitianyu 发表于 2026-1-29 17:29
4. 预处理模块实现
这部分按语法规则添加即可
待办事项
喜大普奔, 生成第一个hex代码 , 之后的代码, 将在此基础上迭代, 此版本为0.0.1
.equ P1, 0x90
.global main
.section .text CODE 1
.label main
.label Lmain_0
mov P1, #0
mov A, 0x90
jnz Lmain_2
.label Lmain_1
mov P1, #15
sjmp Lmain_3
.label Lmain_2
mov P1, #170
.label Lmain_3
mov A, #0
ret
:10000000759000E590700575900F80037590AA7447
:020010000022CC
:00000001FF
页:
[1]
2