手写小型C编译器:从词法分析到x86-64汇编的完整实现 简介一份以C语言实现的小型编译器完整源码面向想弄懂编译原理的开发者与计算机专业学生适合用来对照教材逐模块研读。资源压缩包共86个文件以C源文件50个.c和头文件12个.h为主体另有少量配置文件、批处理脚本和说明文档整体仅210KB非常轻量。源码覆盖词法分析、语法分析、语义分析、优化与代码生成等核心阶段并包含ANSI C与x86/68k后端的实现。通过阅读和调试这些代码可以具体看到符号表管理、表达式求值、语句处理、寄存器分配等环节如何落地包内同时提供实验批处理与构建脚本便于自行编译运行观察每个阶段的输出变化。已有477人学习下载适合作为编译原理课程的课外补充或个人研究入门材料。1. 写一个小型C编译器先搞清楚你要的到底是个什么玩意如果你是一个C语言初学者或者刚入门的系统软件爱好者看到“小型C编译器实现的源代码”这种标题第一反应多半是这东西高深莫测代码量肯定几万行起步根本看不懂。但实际上当一个编译器被限制在“小型”这个范围时事情远没有想象中复杂。我自己动手写的这个编译器源码总共不到四千行C代码就能完成对C语言一个子集的完整编译最终生成x86-64汇编并链接成可执行文件。这个项目能做三件事把纯文本的C源文件解析成抽象语法树做基础的语义检查和类型推导然后生成可运行的汇编代码。它适合谁来看想搞明白编译器到底是怎么回事的C语言学习者、需要为某个脚本语言设计解释器的嵌入式开发者、以及单纯对“源代码如何变成可执行文件”充满好奇的程序员。看完之后你会明白编译器和编辑器完全是两回事——编辑器只负责帮你写文本编译器负责把文本变成机器能懂的东西。我选择C语言作为被编译对象而不是Java或者Python是因为C语言本身语法简洁又足够底层指针和结构体正好能和编译器的核心数据结构天然对应。再加上C标准中有大量现代编译器都支持但严格语法精简后并不影响主流程的语言特性裁剪起来非常顺手。另一个现实原因是我自己写编译器这台“编译器”用的就是C语言自举的思路从一开始就埋下了伏笔。2. 从零搭框架编译器的四个阶段是如何协调工作的2.1 整个编译流水线的设计一个编译器的经典流水线包括词法分析、语法分析、语义分析、代码生成。我写这个小编译器时没有做太复杂的优化所以中间表示也简化成了一种类似于三地址码的结构体链表而不是像GCC那样搞出七八层IR。词法分析器的作用通俗说就是把源代码拆成“单词”。比如int x 10 5;这一个字符串会被拆成int、x、、10、、5、;这些token。我在实现时维护了一个全局的token列表每个token包含类型、文本值、行号和列号。行号和列号必须留否则后面语法报错你根本没法定位源码位置。语法分析器我用了递归下降法这是手写编译器最常用也最好维护的解析方案。它本质上就是为每个语法规则写一个函数例如“表达式”对应parse_expr()“赋值语句”对应parse_assign_stmt()。这种写法比用lex/yacc自动生成器更加直观虽然手写的词法分析器要多花点时间但整个项目可控性强得多出了问题你知道吗——直接断点进函数看就行不需要去理解自动机生成工具的展开逻辑。2.2 为什么我放弃了lex/yacc和LLVM后端很多教程让你用flex和bison生成词法、语法分析器再用LLVM的API生成中间码。我必须说这个路线只适合机器配置好、网速快、环境干净的“标准环境”一旦你换一台没有这些依赖的机器或者需要调试某个细节复杂度立刻上来。我这个项目从一开始就定了死规矩除了系统自带的gcc/as/ld之外不依赖任何外部工具。原因有二一是为了可移植性整套源码在任何装着Linux的电脑上都能直接编译运行二是为了学习价值自己手写词法分析器和递归下降解析器才能真正理解那些被工具隐藏掉的细节。后端方面我没有走LLVM而是直接针对x86-64汇编生成目标代码。选择这种方案的好处是生成的汇编代码人类可读性非常高你编译完可以直接打开.s文件查看看到movl %eax, -4(%rbp)这种指令你能直接感受到程序的数据是如何在栈上流转的。LLVM IR虽然更高级但对初学者来说反而增加了一层抽象障碍。3. 核心模块源码解析从token到汇编的完整实现3.1 词法分析器如何把字符流变成token流这是整个编译器最基础的模块。我把token类型定义成了枚举用结构体来保存每个token的详细信息。以下是关键源码结构typedef enum { TOK_EOF, TOK_INT, TOK_RETURN, TOK_IF, TOK_ELSE, TOK_WHILE, TOK_IDENT, TOK_NUMBER, TOK_ASSIGN, TOK_PLUS, TOK_MINUS, TOK_STAR, TOK_SLASH, TOK_SEMI, TOK_LPAREN, TOK_RPAREN, TOK_LBRACE, TOK_RBRACE, TOK_LT, TOK_GT, TOK_EQ, TOK_EXCLAIM } TokenType; typedef struct { TokenType type; const char *start; int length; int line; int col; } Token;这里的startlength组合比直接复制字符串更高效因为大部分情况我们只需要比较词法单元是不是某个标识符没必要分配内存做字符串拷贝。词法分析主循环就是一个大switch每个case处理一个字符或一个字符族遇到空白和换行就跳过遇到字母或下划线就继续读直到非字母数字为止再做关键字匹配。有一个细节值得分享关键字和标识符的判断顺序。我先收集完整的词素再查表判断是不是关键字。如果反着来先按字符匹配关键字很容易在读到intx这种复合标识符时出错。实际代码就十几行但犯过一次错之后记忆特别深刻。3.2 递归下降语法分析器表达式优先级是个难点语法分析器我写成了一组互相递归调用的函数基本结构和文法产生式一一对应。表达式解析是里面最讲究的部分我实现的是经典优先级爬升static Node *parse_expr() { return parse_binary_expr(0); } static Node *parse_binary_expr(int min_prec) { Node *left parse_unary_expr(); for (;;) { TokenType op peek()-type; int prec get_binop_prec(op); if (prec min_prec) break; advance(); Node *right parse_binary_expr(prec 1); left new_binary_node(op, left, right); } return left; }核心技巧在于parse_binary_expr(prec 1)这里传的是prec 1而不是prec。这样做能保证左结合性让a - b - c被解析成(a - b) - c而不是a - (b - c)。我当时在这里踩了坑传成prec导致所有同优先级运算符都变成了右结合生成的代码数学上完全错误。语法树的节点我用了一个统一的结构体类型用枚举区分额外存储子节点指针。没有做太复杂的面向对象式设计因为C语言本身就是结构体加指针这样的树结构最轻量typedef struct Node { NodeType type; Token token; struct Node *children[3]; TypeInfo *type_info; int offset; } Node;3.3 符号表与语义分析类型检查不能省符号表我用了一个简单的链式表每个作用域对应一张表子作用域通过指针指回父作用域。查找变量时从当前作用域一路向上找这模拟了C语言的作用域遮蔽规则。typedef struct Symbol { char *name; TypeInfo *type; int offset; int is_const; struct Symbol *next; } Symbol; typedef struct Scope { Symbol *head; struct Scope *parent; } Scope;这里有个经验符号表里一定要保存栈上的偏移量。因为我这个编译器后续生成汇编时所有局部变量都放在栈上符号表里的offset字段就是变量在栈帧中相对于%rbp的偏移。如果不存下来后面生成movl %eax, -8(%rbp)这样的指令时还得再扫一遍所有变量白白浪费时间。语义分析阶段做的事情不算多但必须做全。我做了基本的类型检查——赋值语句左右类型是否兼容二元运算两侧是否是整数函数调用的参数个数是否匹配return语句是否存在于非void函数中。这些检查用递归遍历语法树实现大概两百行代码但效果立竿见影本来很多运行时才能发现的错误在编译期就挡住了。3.4 代码生成基于栈帧的x86-64汇编输出这是最让我兴奋也最折磨人的模块。我选择x86-64 ATT风格的汇编遵循System V调用约定函数的开头结尾都遵循标准的栈帧设置pushq %rbp movq %rsp, %rbp subq $N, %rspN是当前函数所有局部变量占用的总空间这个数值是在语义分析阶段通过累计每个变量的offset计算出来的。代码生成阶段做的事情就是把抽象语法树翻译成一条条汇编指令核心思路是一个简单的栈式表达式求值器生成表达式代码时把中间结果压栈遇到运算符再从栈里弹出操作数计算。比如x 10 5;这句话我生成的汇编大致长这样movl $10, %eax pushq %rax movl $5, %eax popq %rcx addl %ecx, %eax movl %eax, -8(%rbp)这种生成方式性能不高但胜在实现简单、直观易调试。等整套流程跑通之后再去考虑寄存器分配和优化才是合理的学习路径。4. 调试与踩坑实录编译器开发中绕不过去的几道坎4.1 用测试用例驱动开发而不是写完再看我开发这个编译器时吃过最大的亏就是写了一大段代码之后才开始测试结果第一个测试文件就出了十几个错误根本无从排查。后来我调整了策略为每个功能点写一个最小测试用例每实现一个特性就跑一遍全量测试集。整个测试集我用shell脚本维护基本逻辑是这样的每个.c测试文件先用我的编译器编译再用gcc原生编译同一个文件然后分别运行比对输出结果是否一致。这样做虽然土但非常有效。你甚至可以故意写有语法错误的测试用例验证编译器报错信息是否友好。4.2 最常见的三个bug类型与排查方法我整理了一个速查表基本上编译器开发初期所有问题都能归到这三类bug类型典型表现排查方法词法分析边界问题变量名最后一个字符被吞掉或者数字溢出未报错打印token流检查每个token的start和length语法分析优先级错误1 2 * 3计算出9而不是7用-p参数打印语法树人工检查树结构代码生成栈帧错误函数调用结束后返回值错乱或栈指针不平衡编译成汇编后用gdb打断点单步执行并watch寄存器其中栈帧错误的排查最麻烦因为错误可能不是当前函数引起的而是调用者没有正确保存某个寄存器。我遇到过两个函数互相递归时其中一个覆盖了另一个的返回地址最终导致段错误。排查了整整一个晚上最后打印出来所有函数的栈帧偏移才发现原来是某个函数在设置栈帧之前就把参数存到了%rsp下面——这个顺序错误直接把返回地址冲掉了。4.3 工程化问题内存管理比算法设计更费心写编译器最容易被忽视的就是内存管理。语法树节点、符号表项、字符串字面量每一个都需要动态分配。我一开始采用随处malloc、用完了就free的策略结果频繁出现重复释放和泄漏。后来我换成**区域内存池region-based allocation**方式编译每个源文件时维护一个全局的内存池整个文件编译完一次性释放整池内存。这个方案简单粗暴又极其有效因为编译器的生命周期很短——跑完一次编译就退出程序结束后操作系统会自动回收所有内存。你不需要精确跟踪每个节点的释放时机只要保证池不无限增长即可。这种方式在真实生产编译器里偶尔也能见到比如某些语言的前端就是基于arena分配器的。实际写下来这个决定至少帮我节省了两天调试时间。5. 后续还能怎么玩小型编译器的扩展空间写完这个编译器之后你可能会有一种意犹未尽的感觉因为C语言的子集毕竟是子集很多东西还没有支持。我自己接下来的计划里有几个非常值得做的扩展方向。第一个方向是增加指针支持。C语言最具特色的能力就是指针一旦加了指针语法树的节点结构、类型系统、以及代码生成中的寻址方式都要跟着变。指针的存取值会涉及%rax和(%rax)这种间接寻址对初学者理解汇编栈模型非常有帮助。第二个方向是增加函数调用的多参数支持。目前我只支持最多六个整数参数的函数调用因为x86-64的System V调用约定里六个参数以内全部用寄存器传递超过六个要用栈传。把参数个数上限提升到任意数量是一个很好的栈帧练习。第三个方向是做一个简单的优化pass。比如常量折叠——把10 5在编译期就算成15而不是生成两条压栈弹栈汇编指令。这种优化入门成本极低但能让你直观感受到优化对代码质量的改变。而如果你有强烈的探索欲可以把这个编译器移植到Windows平台把生成的汇编格式从ATT改成Intel风格或者把后端换成生成机器码直接写可执行文件那将是对你整个计算机系统知识的一次全面检验。最后分享我在这个项目中最深的体会动手写一个编译器最大的收获不是把C语言某个子集编译跑通的那种成就感而是你从此以后再看“源代码”这个词会多出一个全新的理解维度——源代码不只是给人看的文本它是一个可以经过严谨流水线变成机械指令的蓝图。这种认知上的转变才是这个小型C编译器项目最值钱的部分。本文还有配套的精品资源点击获取

相关新闻

最新新闻

大数据可视化实战:从渲染性能到数据链路与工程化落地

大数据可视化实战:从渲染性能到数据链路与工程化落地

上个月帮一家公司排查数据可视化大屏卡顿的问题,打开浏览器控制台一看,三百多兆的JSON数据被直接塞进了ECharts的series数组里,页面白屏,浏览器直接崩溃。现场负责人还一脸无辜地跟我说:"后端已经把数据查出来了&…

2026/9/9 22:17:29
如何用 CMake 构建 Tesseract 并开启 BUILD_TRAINING_TOOLS 编译训练工具

如何用 CMake 构建 Tesseract 并开启 BUILD_TRAINING_TOOLS 编译训练工具

如何用 CMake 构建 Tesseract 并开启 BUILD_TRAINING_TOOLS 编译训练工具 【免费下载链接】tesseract Tesseract Open Source OCR Engine (main repository) 项目地址: https://gitcode.com/GitHub_Trending/te/tesseract 如果你要用 Tesseract 训练自己的语言模型&…

2026/9/9 22:17:29
泰坦尼克号生存预测实战:从数据清洗到模型调优的机器学习完整流程

泰坦尼克号生存预测实战:从数据清洗到模型调优的机器学习完整流程

一、项目概述与价值分析1.1 项目背景与核心需求拆解泰坦尼克号生存预测可以说是数据挖掘和机器学习领域最经典的入门项目之一。它的本质是一个二分类问题:给定一组乘客的特征数据(如年龄、性别、舱位等级、票价、登船港口等),我们…

2026/9/9 22:17:29
电力网格化运营指标体系与考核模型全解析

电力网格化运营指标体系与考核模型全解析

1. 电力网格化运营:一套指标体系解决的管理难题1.1 网格化管理为什么在电力行业火起来网格化运营这个词,在电力行业其实已经不算新鲜了,但真正把它做扎实、做出成效的,却远比想象中少。电网企业从过去的“按专业条线管设备”转向“…

2026/9/9 22:17:29
C++实现影像金字塔:图像重采样与插值算法实战解析

C++实现影像金字塔:图像重采样与插值算法实战解析

简介:一套面向遥感影像分析与计算机视觉开发者的C图像处理代码,以双线性内插算法为核心,对8位和24位Windows位图进行22模板重采样,并据此构造多分辨率影像金字塔,适用于图像缩放、目标检测与尺度空间分析等场景。压缩包…

2026/9/9 22:12:29
UDS诊断Python化:udsoncan库实现ISO-14229协议通信实战

UDS诊断Python化:udsoncan库实现ISO-14229协议通信实战

简介:一份基于 Python 3 的 ISO-14229 UDS 统一诊断服务协议实现源码包,面向汽车电子、车载诊断及 CAN 总线开发者,可用于诊断工具中的 ECU 通信、会话控制、数据读写和故障码读取等场景。代码源自 GitHub 开源项目 python-udsoncan&#xff…

2026/9/9 22:12:29