peitianyu 发表于 2026-1-14 19:08:55

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>一元:
有:+ - ++ -- * &amp; !
无:sizeof (type)cast typeof ~
二元:
有:+ - * / % &lt;&lt; &gt;&gt; &lt; &gt; ==&amp; | &amp;&amp; || =
无:^ . -&gt; &lt;&lt;= &gt;&gt;= &amp;= |= ^= &gt;= &lt;= != += -= *= /= %=
三元:
有:?:
常量 &amp; 字面量
有:十进制, 字符, 字符串, 十进制浮点
无:十六进制(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){} 空形参列表写法(必须空括号)
可变参数 ...、&lt;stdarg.h&gt;
inline、_Noreturn、函数原型默认参数提升规则外的任何扩展
</code></pre>
<p>五、预处理 &amp; 编译单元</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 &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;stdlib.h&gt;

#ifdef MINITEST_IMPLEMENTATION

#ifdef __TINYC__
__asm__(&quot;.global __start_testsec\n&quot;
      &quot;__start_testsec = .\n&quot;
      &quot;.global __stop_testsec\n&quot;
      &quot;__stop_testsec = .\n&quot;);
#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(&quot;testsec&quot;))) = { \
            #suite &quot;.&quot; #name, suite##_##name }; \
    static void suite##_##name(void)

#define ASSERT(cond)      do { if (!(cond)) { printf(&quot;FAIL %s:%d%s\n&quot;, __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*)&amp;__start_testsec, *e = (Test*)&amp;__stop_testsec;   \
      size_t n = e - b;                                                   \
      if (!n) { puts(&quot;No tests&quot;); break; }                              \
      printf(&quot;\n\033[36m===== Tests =====\033[0m\n&quot;);                     \
      for (size_t i = 0; i &lt; n; ++i) printf(&quot;\033.name); \
      printf(&quot;\033[35m# (0=all, ENTER=quit): \033[0m&quot;); fflush(stdout);   \
      int c = getchar();                                                \
      if (c == '\n' || c == EOF) break;                                 \
      ungetc(c, stdin);                                                   \
      int k;if (scanf(&quot;%d&quot;, &amp;k) != 1) break;                            \
      while (getchar() != '\n');/* 吃掉行尾 */                           \
      if (k &lt; 0 || (size_t)k &gt; n) { puts(&quot;\033[31mBad#\033[0m&quot;); break; } \
      for (size_t i = (k ? k - 1 : 0), lim = (k ? i + 1 : n); i &lt; lim; ++i) { \
            printf(&quot;\033\033.name); b.fn();    \
            printf(&quot;\033\033.name);               \
      }                                                                   \
    } while (0)

#else

#define ASSERT(cond)      do { if (!(cond)) { printf(&quot;FAIL %s:%d%s\n&quot;, __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 &quot;core/minitest.h&quot;

int g_argc;
char **g_argv;
void main(int argc, char **argv) {
    RUN_ALL_TESTS(argc, argv);
}
#else

#include &quot;stdio.h&quot;

int main() {
    printf(&quot;hello main\n&quot;);
    return 0;
}

#endif

/* run.sh */
tcc main.c core/*.c -run -DMINITEST_IMPLEMENTATION
</code></pre>

peitianyu 发表于 2026-1-14 20:19:30

<h2>3. 一些想法与设计思路</h2>
<ol>
<li>关键词:</li>
</ol>
<pre><code>关键词
    将仅支持储存关键词(data, idata, pdata, xdata, edata, code)
    register标识将直接代替sfr, sfr16, sbit, 使用方式类似 register (int, char, bit) -&gt; (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)
✔ 添加判断&gt;= &lt;= !=支持 @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)
☐ &lt;&lt;= &gt;&gt;= &amp;= |= ^= += -= *= /= %=
☐ 添加`,`运算符支持
☐ 添加对可变参数支持
☐ 添加常用库支持
☐ 添加编译器拓展
    ☐ 常用attribute子项
      ☐ section
      ☐ alias
      ☐ aligned
    ☐ asm指令嵌入支持
☐ 添加typeof语句
☐ 添加typestr语句
☐ 添加defer语句
</code></pre>
<p>暂时前端先这么着, 开始中间代码与后端的实现部分</p>

神农鼎 发表于 2026-1-14 21:49:55

C51编译器 有开源的 SDCC-51,
能否直接 考虑从 SDCC-51 到 支持 STC32G/AI8051U,
或从 GNU C++ 出发

peitianyu 发表于 2026-1-14 21:57:34

神农鼎 发表于 2026-1-14 21:49
C51编译器 有开源的 SDCC-51,
能否直接 考虑从 SDCC-51 到 支持 STC32G/AI8051U,
或从 GNU C++ 出发 ...

最后的输出应该是多平台支持的(类似tinyc), 只是编译选项的问题, 初步计划是以mcs51为开端, 后面会逐步添加

peitianyu 发表于 2026-1-14 22:02:20

神农鼎 发表于 2026-1-14 21:49
C51编译器 有开源的 SDCC-51,
能否直接 考虑从 SDCC-51 到 支持 STC32G/AI8051U,
或从 GNU C++ 出发 ...
另外为什么没从sdcc或者gnu开始的原因是,
看代码理解他们架构的时间往往已经够我自己实现一一遍,
他们过于庞大了(不过应该会参考sdcc中的asm优部分),
且可自由发挥的空间也小了,
毕竟设计这个的初衷是玩, 学习

gentleman 发表于 2026-1-15 08:51:03

{:4_269:}

握爪,做C251编译器也有一段时间了
感觉是个持久战
越后面进展越慢{:4_165:}

peitianyu 发表于 2026-1-15 13:24:22

gentleman 发表于 2026-1-15 08:51
握爪,做C251编译器也有一段时间了
感觉是个持久战
越后面进展越慢

哈哈哈, 已经做好心里准备

peitianyu 发表于 2026-1-17 16:47:40

<h2>5. 中间语言设计</h2>
<p>这里采用三地址码的设计, 配合ssa, 但会参考bril的设计, 将这一部分拆出来方便独立的调试优化</p>
<h3>1. 类型</h3>
<p>类型支持常见的int, uint, 以及float</p>
<ul>
<li>int&lt;bit_size&gt; uint&lt;bit_size&gt; float&lt;bit_size&gt;</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>&quot;unsigned&quot;: true</code>, 字段用于后边生成代码阶段解析</li>
<li>static 分为 <code>file.static</code>与 <code>func.static</code> 这部分将直接在json的globals中体现</li>
<li>restrict 在bril中添加 <code>&quot;attributes&quot;: [&quot;restrict&quot;]</code>, 之后在alias-analysis pass 可读取该标记, 假设&quot;无别名&quot;做激进优化, c51中不处理</li>
<li>register/data bril中添加 <code>&quot;attributes&quot;: [&quot;register&quot;, &quot;xdata&quot;]</code>, 之后在代码生成阶段直接按需处理</li>
<li>volatile bril中添加 <code>&quot;attributes&quot;: [&quot;volatile&quot;]</code>, 表示后端优化 pass(死码删除, 公共子表达式, 寄存器分配)看到该字段即跳过</li>
<li>inline 在bril中添加 <code>&quot;attributes&quot;: [&quot;inline&quot;]</code></li>
<li>noreturn 在bril中添加 <code>&quot;attributes&quot;: [&quot;noreturn&quot;]</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-&gt;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>

peitianyu 发表于 2026-1-29 17:29:08

<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-2-2 17:06:48

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
查看完整版本: c51编译器编写日记