ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SysY到RISC-V编译器实现:从词法分析到代码生成的完整实践

SysY到RISC-V编译器实现:从词法分析到代码生成的完整实践 简介编译原理课程实践资源基于C实现从SysY语言到RISC-V指令集的完整编译器包含项目源码与实践报告面向计算机专业学生尤其适合课程设计、期末大作业等场景是一份系统完整、可直接参考的高分方案。压缩包共二十七个文件其中十个hpp头文件与六个cpp源文件构成核心代码词法与语法规则由l/y定义koopa为中间代码表示s和o分别为生成的汇编与目标文件另有md说明文档与CMake构建配置等包体仅一百零八KB结构精炼且目录清晰方便按模块快速定位。资源已有一百三十六人学习下载获评满分大作业功能完善、操作简单实用性获得认可。项目代码注释充分新手也能轻松读懂部署简单即可运行实践报告与源码相互对照完整展示词法分析、语法分析、中间代码生成和RISC-V汇编生成全流程并附有hello等测试用例便于验证结果与二次扩展无论基础如何都能从中受益作为编译原理课程设计的高分参考价值很高。1. 这门大作业到底在做什么把 SysY 从头到尾送到 RISC-V编译原理课如果只讲理论不写代码你对龙书里那些词法分析、语法分析、语义分析的理解就永远停在纸面上。这个基于 C 实现的 SysY 到 RISC-V 编译器是把整条编译流水线真跑了一遍的课程实践项目——从sysy.l做词法扫描、sysy.y做语法归约到构建 AST再到生成 Koopa 中间表示最后落到 RISC-V 汇编。我拆这个包的时候最深的感受是它不追求造一个工业级编译器而是把课件里每个抽象概念落成了能跑、能调试、能对着实践报告讲清楚的代码。适合正在做编译原理期末大作业、课程设计或者想一口气把 flex/bison 和指令生成串起来读一遍的人。2. 源码包先拆开看目录结构、构建方式和三条处理链拿到源码包先别急着编译花十分钟把目录结构读一遍后面能少走很多弯路。这个项目的组织方式很有课程设计的典型特征核心代码集中在src测试样例放在test根目录有CMakeLists.txt、.vscode/settings.json、README.md和.gitignore。读懂这个结构你就知道了它前前后后要经过哪几道处理。2.1 src 目录文件分工谁管词法、谁管中间代码、谁管目标代码src目录是整套编译器的心脏文件不多但每个文件对应编译流程里的一个明确阶段sysy.lflex 词法规则文件负责把 SysY 源码切成 tokensysy.ybison 语法规则文件负责把 token 序列归约成语法树ast相关文件AST 节点定义语法分析时同步构造的中间产物main.cpp程序入口串联词法、语法、语义和代码生成builder.cppAST 到 Koopa IR 的生成器项目里最重的逻辑基本都在这koopa_util.cpp/koopa_util.hpp调用 libkoopa 接口把 Koopa IR 转成 RISC-V 汇编symbol_list.hpp符号表实现管理变量、函数的作用域与类型信息block_maintainer.hpp基本块维护器处理控制流时负责块的生成与跳转loop_maintainer.hpp循环状态栈记录循环嵌套层级供 break/continue 定位跳转目标我拿到一个编译器项目习惯先按流水线给文件排序词法 → 语法 → AST → IR → 目标代码。这样看源码的思路非常清晰不会一头扎进细节里出不来。这套分工里最值得先看的是builder.cpp因为它决定了前面的 AST 结构设计得好不好用也决定了后面koopa_util.cpp能不能把指令映射得干净。2.2 CMakeLists.txt 构建细节flex/bison 和 libkoopa 怎么串起来课程设计项目最怕依赖装不齐这个包的依赖其实很收敛flex、bison 和 libkoopa。CMakeLists.txt里做了这三件事的声明我拆给你看。cmake_minimum_required(VERSION 3.16) project(SysYCompiler) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(koopa REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(FLEX REQUIRED flex) pkg_check_modules(BISON REQUIRED bison) add_executable(compiler src/main.cpp src/builder.cpp src/koopa_util.cpp ) set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) target_link_libraries(compiler PRIVATE koopa Threads::Threads)第一段是 C17 标准声明第二段找 libkoopa 和 flex/bison 的依赖第三段把可执行文件编译出来。这里有两个关键点find_package(koopa REQUIRED)要求 libkoopa 已经安装到系统路径否则 CMake 会直接报错pkg_check_modules是给 flex 和 bison 用的如果你的环境里这两个工具不是通过包管理器装的可能需要在.vscode/settings.json里手动指定路径。.vscode/settings.json通常配置的是头文件路径和编译参数常见做法是把 koopa 的 include 路径和 lib 路径写进includePath和c_cpp_properties这样 VSCode 的 IntelliSense 才能正确解析koopa/koopa.h。如果你在别的机器上复现记得先跑sudo apt install flex bison或对应包管理器命令再装 libkoopa顺序别反。构建执行的命令很简单mkdir build cd build cmake .. make -j$(nproc)mkdir build是搞一个独立的构建目录避免 CMake 生成的中间文件污染源码目录这是个好习惯。cmake ..会去读根目录的CMakeLists.txt并检测依赖如果这一步报错九成是 libkoopa 没找到先去确认koopa的安装路径有没有被 CMake 扫描到。make -j$(nproc)用多核并行编译$(nproc)会自动探测 CPU 核心数。2.3 test 目录里的产物链hello.c 到 hello.s 到可执行文件test目录是这份资源里第二个值得细看的地方里面躺着hello.c、hello.koopa、hello.s、hello.o和可执行文件hello。这一组文件如果按生成顺序排正好是一条完整的产物链文件生成阶段说明hello.c输入SysY 源码课程里定义的 C 子集hello.koopaIR 生成AST 转成 Koopa IR 中间表示hello.s目标代码Koopa IR 转成 RISC-V 汇编hello.o汇编由汇编器从 .s 生成目标文件hello链接链接运行时库后的可执行文件我一般用这组文件来做冒烟测试拿到编译器先不对着实践报告读直接跑一遍 hello.c如果最终能生成可执行文件并正确输出说明主链路是通的。后面改任何一个环节也用这条链做回归基准。3. 词法语法分析sysy.l 与 sysy.y 如何把 SysY 变成 AST编译器的前前后后最直观的就是前端词法分析把字符流切成 token语法分析把 token 流归约成树形结构。这一段是 flex/bison 的经典应用场景SysY 语言是 C 的一个教学子集语法规模控制得很适度正好用来理解词法规则和语法规则怎么配合。3.1 sysy.l 词法规则数字、标识符、运算符怎么匹配flex 用正则表达式匹配输入流匹配上就执行对应动作并返回 token。sysy.l里典型的一段长这样%{ #include ast.hpp #include sysy.y.hpp %} %option noyywrap %% [0-9] { yylval.int_val atoi(yytext); return INT_CONST; } [a-zA-Z_][a-zA-Z0-9_]* { yylval.str_val strdup(yytext); return IDENT; } { return PLUS; } - { return MINUS; } * { return MUL; } / { return DIV; } { return AND; } || { return OR; } ! { return NOT; } { return EQ; } ! { return NEQ; } { return LT; } { return LE; } { return GT; } { return GE; } if { return IF; } else { return ELSE; } while { return WHILE; } return { return RETURN; } [ \t\n] { /* skip whitespace */ } . { return yyerror(unknown char); } %%[0-9]匹配整数字面量yylval.int_val把字符串转成整型值存进 yylval 传给语法分析器[a-zA-Z_][a-zA-Z0-9_]*匹配标识符用strdup复制一份字符串存进str_val。这里有个细节if、else这类关键词的规则写在标识符规则前面flex 按最长匹配和规则先后顺序取优先级所以if会先被识别成 IF 关键词而不是一个叫if的标识符。运算符的返回 token 也很好理解PLUS/MINUS/MUL/DIV这些 token 会被 bison 用在语法规则里。跳过空白字符那条规则容易漏写没有它你会发现每个空格都触发unknown char报错这是新手经常翻车的地方。3.2 sysy.y 语法结构表达式优先级和悬空 else 的分界bison 文件里最关键的是优先级声明和文法规则。SysY 的表达式优先级和 C 一致乘除高于加减逻辑与高于逻辑或一元负号高于二元运算。sysy.y里一般用%left和%right来声明%left OR %left AND %left EQ NEQ %left LT LE GT GE %left PLUS MINUS %left MUL DIV %right UMINUS NOT%left表示左结合越靠下优先级越高。UMINUS是用%right单独声明的一元负号优先级比 MUL 还高这样才能正确处理-a * b这类表达式应该解析成(-a) * b。文法规则部分常见的长这样exp: INT_CONST { $$ new IntExpr($1); } | IDENT { $$ new VarExpr($1); } | exp PLUS exp { $$ new BinaryExpr($1, $3, PLUS); } | exp MUL exp { $$ new BinaryExpr($1, $3, MUL); } | MINUS exp %prec UMINUS { $$ new UnaryExpr($2, MINUS); } | IDENT LPAREN args RPAREN { $$ new CallExpr($1, $3); } | LPAREN exp RPAREN { $$ $2; } ;每条规则的$$是归约后生成的 AST 节点$1、$3是对应位置的子节点。这里MINUS exp %prec UMINUS的%prec告诉 bison 这条规则使用UMINUS的优先级而不是MINUS的这样才能正确区分一元负号和二元减法。悬空 else 的处理是 bison 的经典坑默认的移进优先会让if (a) if (b) c; else d;里的 else 跟最近的 if 配对这恰好符合 C 语义。如果你想让 else 跟外层 if 配对就得在文法上动手脚。这个项目遵循 C 标准默认行为就是对的但你要清楚这个机制不然写自己的语法时会被这个玄学坑得很惨。3.3 AST 节点设计接口统一遍历器才好写AST 是整个编译器前后端的接口协议设计得好不好直接决定 builder 好不好写。这个项目把ast放在src下典型的做法是定义一个基类BaseAST再派生出各个具体节点类型class BaseAST { public: virtual ~BaseAST() default; virtual void generateIR() 0; // 生成 Koopa IR 的入口 }; class IntExpr : public BaseAST { public: int val; void generateIR() override; }; class BinaryExpr : public BaseAST { public: BaseAST *lhs, *rhs; int op; void generateIR() override; };这种设计的好处是遍历逻辑统一builder 只需要对每个节点调用generateIR()节点自己知道怎么生成中间代码。缺点是每个节点都要写一份生成逻辑文件会越写越长。builder.cpp的规模主要取决于 SysY 语法点的数量从实践报告看这份作业把表达式、语句、函数、全局变量、数组这几块都覆盖到了。AST 设计有三个点值得注意一是节点要不要保存源码位置信息调试时有用但不保存也能过作业二是表达式节点用统一基类语句节点和表达式节点要分开三是数组类型节点的维度信息保存方式这会影响后面的 IR 生成。这个项目把节点生成逻辑分散到各个类里而不是用一个巨型 switch 集中处理维护起来顺手很多。4. 从 AST 到 Koopa 再到 RISC-Vbuilder 是整条流水线的核心前端把 AST 搭好之后真正的重头戏来了怎么把 AST 变成中间表示再变成 RISC-V 汇编。这个项目的选择是先用builder.cpp把 AST 翻译成 Koopa IR再用koopa_util.cpp调用 libkoopa 生成汇编。这一步是整个编译器里信息量最大的一层。4.1 为什么选 Koopa 而不是直接生成目标代码很多编译器课程设计会让学生直接生成 MIPS 或 x86 汇编绕开中间表示。这个项目选了 Koopa思路更接近现代编译器的分层设计。中间表示存在的意义是把前端和后端解耦前端只负责把语法翻译成 IR后端只负责把 IR 翻译成目标代码两边都不需要知道对方的细节。Koopa 是北大编译原理课程配套的中间表示设计得简洁指令集规模适合教学实验用。它有 C 接口libkoopa能把 Koopa IR 的文本形式解析成 raw program再生成 RISC-V 汇编。这就让后端的工作量大大压缩你不需要自己设计寄存器分配和指令选择的完整逻辑只需要把 IR 映射到 RISC-V 上。我拆这份源码后的判断是选 Koopa 是为了把精力集中在 AST 到 IR 的 translation 上这是编译原理课最想让你练的部分。如果你自己写编译器这个选型思路值得借鉴——先选定一个稳定的中间表示再谈目标代码。4.2 builder.cpp 的遍历逻辑符号表、基本块、循环如何协作builder.cpp是项目里最长的源文件。它的核心逻辑是遍历 AST边遍历边生成 Koopa IR 字符串同时维护三类运行时结构符号表symbol_list、基本块维护器block_maintainer、循环状态栈loop_maintainer。void BinaryExpr::generateIR() { lhs-generateIR(); rhs-generateIR(); // 弹出两个操作数生成二元运算指令 std::string rhs_val koopa_current_block().pop_operand(); std::string lhs_val koopa_current_block().pop_operand(); std::string result koopa_new_var(); std::string op_map[4] {add, sub, mul, div}; koopa_emit(%0 %1 %2, %3, result, op_map[op], lhs_val, rhs_val); koopa_current_block().push_operand(result); }这段伪代码式的写法描述的是整体思路先递归生成子表达式的 IR然后从当前 block 的操作数栈里弹出两个操作数生成指令后把结果压回去。实际builder.cpp里的实现方式可能不同但数据流就是这样一个右值传递的模式。符号表symbol_list.hpp的角色是管理变量和函数的类型与作用域。变量定义时查表使用时查表块结束时出表。常见问题是全局和局部作用域区分这个放下一章讲。block_maintainer.hpp维护的是基本块列表——if分支、while循环都要生成独立的基本块块的创建、跳转目标的管理都由它管。loop_maintainer.hpp维护的是循环状态栈栈顶记录当前最内层循环的标签break和continue生成跳转指令时从栈顶拿目标标签。这三个维护器是写控制流 IR 的基础设施它们的协作顺序直接影响生成代码的正确性。你读源码的时候建议按这个顺序读先读symbol_list.hpp再读block_maintainer.hpp最后读loop_maintainer.hpp然后再回来看builder.cpp。4.3 koopa_util.cpp 把 IR 映射成 RISC-V 指令的边界koopa_util.cpp的主要工作可以分成两步把 Koopa IR 文本解析成 raw program然后把 raw program 里的指令翻译成 RISC-V 汇编。#include koopa/koopa.h #include stdio.h void generate_riscv(const char *koopa_ir_str) { koopa_program_t program; koopa_error_code_t err koopa_parse_from_buf(koopa_ir_str, program); if (err ! KOOPA_EC_SUCCESS) return; koopa_generate_riscv(program, stdout); }这里用到了 libkoopa 的两个核心函数koopa_parse_from_buf把 IR 文本解析成内存中的 raw programkoopa_generate_riscv把 program 转换成 RISC-V 汇编输出到 stdout。你可以在这两步中间插入自己的自定义处理比如做指令调度或者插入调试信息但课程设计一般不需要。指令映射的边界很清楚Koopa 的算术运算指令add/sub/mul/div一对一映射到 RISC-V 同名指令内存访问走 load/store分支跳转走 branch 和 jump。复杂一点的是函数调用的栈帧管理RISC-V 约定a0-a7传参返回值放a0栈要对齐到 16 字节。这些细节 libkoopa 已经封装好了你要操心的是 IR 生成阶段不要漏了栈帧的建立和销毁。5. 避坑记录从能编译到能跑对的五个坎这一章是我重读源码加跑测试样例时最有体感的部分。下面这几条坑几乎每个写 SysY 编译器的人都会碰到我把现象、原因和解决方式都写清楚你踩到的时候可以直接照着排查。5.1 悬空 else 匹配错乱现象是输入if (a) if (b) c 1; else d 2;时生成的 IR 把 else 配给了内层 if但期望是配给外层 if。原因bison 的默认策略是移进优先于归约else 会嫁给最近的 if这恰好也是 C 标准的行为。解决确认sysy.y里的 if 规则没有手动干预优先级默认行为就能满足要求。如果你为了某些情况改写过规则可以用%prec显式指定优先级或者干脆在语法层面禁止嵌套 if 不写 else 的形式。5.2 符号表作用域污染全局变量和局部变量同名冲突现象定义了一个全局变量int x又在某个函数里定义局部变量int x结果函数里所有对x的引用都解析成了全局变量生成的 IR 数据流直接错掉。原因符号表只在函数入口和出口做了 push/pop没有在块级作用域的入口做区分查表时先命中了全局的x。解决在symbol_list.hpp里加入作用域层级字段每次进入代码块时压入一层退出时弹出一层查表时从当前层向全局层反向检索。我一般会用std::vectorstd::unordered_mapstd::string, Symbol这种结构来实现分层符号表。5.3 RISC-V 调用约定的栈对齐现象生成的汇编在调用printf处理格式化字符串时部分测试样例输出异常或者直接段错误。原因RISC-V 的栈指针必须保持 16 字节对齐函数调用前没对齐。SysY 编译器最常调用的外部函数就是printf它内部会依赖栈对齐做 SIMD 或者原子操作。解决在函数入口处计算栈帧大小时加上 padding 让栈指针保持 16 字节对齐。判断方法也简单看生成的汇编里有没有addi sp, sp, -N的地方 N 不是 16 的倍数。5.4 循环嵌套时 break/continue 跳错标签现象两层while嵌套时内层循环的break跳到了外层循环的结束标签程序逻辑直接乱掉。原因loop_maintainer的循环状态栈没有随循环退出及时弹出break生成时拿到的是旧信息。解决循环入口push标签信息循环出口pop保证任何时候栈顶都是当前最内层循环。调试这类问题可以打开生成的.koopa文件看跳转标签的编号规律正常是后进先出如果出现交叉就说明栈维护有 bug。5.5 数组初始化与零初始化段混淆现象全局数组int a[10] {1,2,3}生成的汇编把数组放进了 data 段但后面的 7 个元素没有清零程序运行时读出来是随机值。原因Koopa IR 里 aggregate 初始化和 zeroinit 是两种不同的全局变量定义builder 生成 IR 时只发了带初始值的前三个元素没补零初始化。解决当初始化列表长度小于数组长度时生成 IR 时要先输出一个 zeroinit 声明再叠加局部初始化或者直接把整段作为 aggregate 初始化并在缺的位置补zeroinit项。判断方法看生成的.s文件里.data和.bss段的内容对不对。6. 端到端验证技巧把 test 目录当回归测试用写好一个编译器之后最怕的不是编译不过而是改一处坏两处。我的习惯是把test目录下的产物链变成一条固定命令每次改完代码就跑一遍确认主链路没断。cd build ./compiler ../test/hello.c -o ../test/hello.s riscv64-unknown-elf-gcc ../test/hello.s -o ../test/hello qemu-riscv64 ../test/hello第一条命令调编译器把 SysY 源码转成 RISC-V 汇编第二条用交叉编译器把汇编转成可执行文件第三条用 qemu 模拟器跑起来。每次改builder.cpp或koopa_util.cpp之后我都会按这个顺序过一遍同时比对生成的hello.koopa和hello.s有没有明显异常。生成的.koopa文件是人可读的文本格式里面有变量定义、基本块标签和指令序列逐行看它和源码的对应关系能最快定位 IR 生成阶段的问题。还有一个小技巧我会把实践报告里对每个语法点的预期输出整理成测试用例表比如算术运算、if 分支、while 循环、函数调用各一条。系统功能里的每个特性都能找到对应的测试输入跑之前先明确预期结果跑完再对比比拿着整个程序盲试高效得多。从那以后我每次改完代码都强制走一遍这三条命令加比对流程编译原理这种课你能反复验证到每一个语法点都跑通说明处理流程已然成形。希望帮到你。本文还有配套的精品资源点击获取
返回列表