找回密码
 立即注册
查看: 618|回复: 16

c51编译器编写日记

[复制链接]
  • 打卡等级:初来乍到
  • 打卡总天数:9
  • 最近打卡:2026-04-14 16:55:09

17

主题

68

回帖

477

积分

中级会员

积分
477
发表于 2026-1-14 19:08:55 | 显示全部楼层 |阅读模式

c51编译器编写日记

0. 前言

之前在做c51汇编编译器的时候就在想, 啥时候也做一个c语言编译器, 然后就挖坑了. 
正好最近发现一个项目(MAZUCC)的教学目的的pc端编译器, 语法分析写的很清晰, 
就尝试写一下c51的线性寄存器分配模型, 发现似乎可行, 就开启了填坑的工作.
本日志算是记录, 算是督促. 
欢迎star: https://github.com/peitianyu/c51cc
大家测试中的问题可以贴在帖子下方

1. 测试并梳理标准(MAZUCC)所缺语法

一、表达式与运算符

一元:
有:+ - ++ -- * & !
无:sizeof (type)cast typeof ~
二元:
有:+ - * / % << >> < > ==  & | && || =
无:^ . -> <<= >>= &= |= ^= >= <= != += -= *= /= %=
三元:
有:?:
常量 & 字面量
有:十进制, 字符, 字符串, 十进制浮点
无:十六进制(0x12), 不支持二进制(0b00010002), 八进制整型, 不支持后缀{L, f等}

二、声明与类型系统

基础类型
有:char int float double
无:signed/unsigned
聚合类型
有:结构体变量, 联合体, 一维/多维数组, 指针
无:结构体, 联合体变量不支类型声明, 不支持位域, 不支持{}初始化

修饰符(全部禁用)
存储类:auto register static extern typedef
类型限定:const volatile restrict _Atomic
函数限定:inline _Noreturn

三、语句与控制流

有:if else for return
无:
else if(语法可写,但按嵌套 if-else 理解)
while do-while switch/case/default break continue goto

四、函数

有:
普通函数定义、递归、函数指针
无:
void foo(void){} 空形参列表写法(必须空括号)
可变参数 ...、<stdarg.h>
inline、_Noreturn、函数原型默认参数提升规则外的任何扩展

五、预处理 & 编译单元

有:无(整个预处理器被移除)
无:

六、标准库

有:
无(整个标准库不存在)

七、存储模型与生命周期

有:
全局变量(程序整个生命期)
局部变量(进入复合语句时创建,退出即销毁)
指针算术与任意层级指针
无:
静态局部变量、static 全局、extern 跨文件链接
动态内存 malloc/free、_Thread_local、线程存储期

八、单片机专属扩展

无: sfr sfr16 sbit bit data xdata idata bdata code reentrant interrupt using

2. 搭建设计测试框架

之前我一直希望有一个类似于rust的 cargo runcargo test的测试框架, 正好之前实现过一个c_test工具.
但发现还是没那么好用, 且不支持tcc编译, 查资料发现c语言中有一个 section, 可以直接将变量在编译阶段就预设好, 这就很nice了.
于是就有了接下来的测试框架, 代码非常少, 单头文件, 宏定义实现, 支持tcc与gcc编译. 非常符合我的审美了.

/* 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[33m%2zu\033[0m : %s\n", i + 1, b[i].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[32m[ RUN ]\033[0m %s\n", b[i].name); b[i].fn();    \
            printf("\033[32m[  OK ]\033[0m %s\n", b[i].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
2 喜欢他/她就送朵鲜花吧,赠人玫瑰,手有余香!
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:9
  • 最近打卡:2026-04-14 16:55:09

17

主题

68

回帖

477

积分

中级会员

积分
477
发表于 2026-1-14 20:19:30 | 显示全部楼层

3. 一些想法与设计思路

  1. 关键词:
关键词
    将仅支持储存关键词(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等
  1. 实现流程:
1. 完善前端语法器ast(暂不支持预编译)
2. bril思路做常规优化后端
3. 特化为硬件相关指令, 做硬件相关优化
4. 线性扫描寄存器分配
5. 生成汇编 
6. 实现预编译
7. 生成链接器
8. 生成烧录文件(HEX)
9. 优化代码结构, 并提供一些有趣的实现

4. 前端语法器实现

前端处理没啥好说的, 按需要一个个添加即可

✔ 添加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 break  else 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语句

暂时前端先这么着, 开始中间代码与后端的实现部分

回复

使用道具 举报 送花

  • 打卡等级:以坛为家I
  • 打卡总天数:380
  • 最近打卡:2026-04-14 15:43:46

845

主题

1万

回帖

2万

积分

管理员

积分
22921
发表于 2026-1-14 21:49:55 | 显示全部楼层
C51编译器 有开源的 SDCC-51,
能否直接 考虑从 SDCC-51 到 支持 STC32G/AI8051U,
或从 GNU C++ 出发
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:9
  • 最近打卡:2026-04-14 16:55:09

17

主题

68

回帖

477

积分

中级会员

积分
477
发表于 2026-1-14 21:57:34 | 显示全部楼层
神*** 发表于 2026-1-14 21:49
C51编译器 有开源的 SDCC-51,
能否直接 考虑从 SDCC-51 到 支持 STC32G/AI8051U,
或从 GNU C++ 出发 ...

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

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:9
  • 最近打卡:2026-04-14 16:55:09

17

主题

68

回帖

477

积分

中级会员

积分
477
发表于 2026-1-14 22:02:20 | 显示全部楼层
神*** 发表于 2026-1-14 21:49
C51编译器 有开源的 SDCC-51,
能否直接 考虑从 SDCC-51 到 支持 STC32G/AI8051U,
或从 GNU C++ 出发 ...

另外为什么没从sdcc或者gnu开始的原因是,
看代码理解他们架构的时间往往已经够我自己实现一一遍,
他们过于庞大了(不过应该会参考sdcc中的asm优部分),
且可自由发挥的空间也小了,
毕竟设计这个的初衷是玩, 学习
回复

使用道具 举报 送花

  • 打卡等级:以坛为家I
  • 打卡总天数:232
  • 最近打卡:2026-01-15 08:51:03

36

主题

742

回帖

3157

积分

荣誉版主

积分
3157
发表于 2026-1-15 08:51:03 | 显示全部楼层


握爪,做C251编译器也有一段时间了
感觉是个持久战
越后面进展越慢
1 喜欢他/她就送朵鲜花吧,赠人玫瑰,手有余香!
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:9
  • 最近打卡:2026-04-14 16:55:09

17

主题

68

回帖

477

积分

中级会员

积分
477
发表于 2026-1-15 13:24:22 | 显示全部楼层
gentl*** 发表于 2026-1-15 08:51
握爪,做C251编译器也有一段时间了
感觉是个持久战
越后面进展越慢

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

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:9
  • 最近打卡:2026-04-14 16:55:09

17

主题

68

回帖

477

积分

中级会员

积分
477
发表于 2026-1-17 16:47:40 | 显示全部楼层

5. 中间语言设计

这里采用三地址码的设计, 配合ssa, 但会参考bril的设计, 将这一部分拆出来方便独立的调试优化

1. 类型

类型支持常见的int, uint, 以及float

  • int<bit_size> uint<bit_size> float<bit_size>

2. 修饰符

思路是能转换就转换出去, 不能就储存到 attributes 字段中

  • const 仅用于前端检查, 不再往下传
  • typedef 纯前端语法糖, 后端完全不处理
  • extern 主要在前端处理, 表声明使用, 避免单文件编译阶段报错, 留给链接阶段, bril中不体现
  • signed/unsigned bril中添加 "unsigned": true, 字段用于后边生成代码阶段解析
  • static 分为 file.staticfunc.static 这部分将直接在json的globals中体现
  • restrict 在bril中添加 "attributes": ["restrict"], 之后在alias-analysis pass 可读取该标记, 假设"无别名"做激进优化, c51中不处理
  • register/data bril中添加 "attributes": ["register", "xdata"], 之后在代码生成阶段直接按需处理
  • volatile bril中添加 "attributes": ["volatile"], 表示后端优化 pass(死码删除, 公共子表达式, 寄存器分配)看到该字段即跳过
  • inline 在bril中添加 "attributes": ["inline"]
  • noreturn 在bril中添加 "attributes": ["noreturn"], 后续“死码消除”pass 利用它

3. ssa编码

不再使用三趟式(Cytron)输出ssa, 使用单趟式的算法, 这里用作者名(Braun算法)实现(可以在todo文件夹找到文章braun13cc.pdf),
注意这里为了简单, 在braun算法的后半段将不做phi函数合并, 因此最好代码实现是可规约的(简单理解为是不使用goto语句),
否则可能会产生一些多余语句.

解释一下braun算法:

  1. ast节点流式进入
  2. 翻译基本块, 变量定义时记录记忆, 并对变量ssa
  3. 在读取变量时按需方向查找记忆
  4. 在存在多个前驱时插入phi函数
  5. 封口时补phi函数操作数, 若可折叠则直接删除phi函数

这种算法的优点是, 最后整理代码的时候, 可以直接将构造ast与ssa构造合到一块做, 只需单趟就可以输出简单优化后的ssa,
且之后可以直接转特定平台即可, 编译速度超快, 有点tinyc那味了, 且这种算法还可以支持解释编译(jit)

该算法天生支持一些优化:

  1. 自动的剪枝SSA
  2. 平凡φ函数即时消除
  3. 常量传播与折叠的协同优化
  4. 公共子表达式消除(CSE)的自然集成

不过只能进行局部优化, 全局优化还得后期优化

todolist

✔ 初步完成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)
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:9
  • 最近打卡:2026-04-14 16:55:09

17

主题

68

回帖

477

积分

中级会员

积分
477
发表于 2026-1-29 17:29:08 | 显示全部楼层

4. 预处理模块实现

这部分按语法规则添加即可
待办事项

✔ 创建预处理器模块 (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 - 编译器指令
☐ 宏展开递归保护 - 防止无限递归
☐ 函数式宏参数展开 - 当前只解析不展开参数
☐ 处理由于预处理导致的行号/文件名变化

5. 后端代码生成设计

后端代码生成分为这样几个步骤:

1. 先使用ssa_pass优化代码流程
2. 根据8051指令集选择指令
3. 线性扫描寄存器分配
4. 指令降级/展开
5. 窥孔优化
6. 汇编器: ASM → ObjFile(段/符号/重定位)
7. 链接器:布局与重定位
8. 输出 ASM/HEX
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:9
  • 最近打卡:2026-04-14 16:55:09

17

主题

68

回帖

477

积分

中级会员

积分
477
发表于 2026-2-2 17:06:48 | 显示全部楼层
peiti*** 发表于 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

点评

恭喜恭喜!坐等分享  发表于 2026-2-2 22:36
回复

使用道具 举报 送花

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|深圳国芯人工智能有限公司 ( 粤ICP备2022108929号-2 )

GMT+8, 2026-4-15 07:16 , Processed in 0.117057 second(s), 94 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表