新闻详情

新闻详情

首页 / 资讯中心 / 详情

SysY到RISC-V编译器实战:从hello.c到真机运行的完整流水线

发布时间:2026/10/2 2:48:41来源:尧图网络
SysY到RISC-V编译器实战:从hello.c到真机运行的完整流水线
简介本资源是一份面向高校计算机专业本科生的编译原理课程实践项目聚焦SysY语言到RISC-V指令集的完整编译器实现适用于期末大作业、课程设计及系统能力训练。项目基于C开发代码结构清晰、注释详尽涵盖词法分析.l、语法分析.y、AST构建、中间表示koopa、后端代码生成.s/.o等核心编译流程新手可快速理解编译全流程。压缩包共27个文件含10个头文件hpp、6个源文件cpp、1个SysY语法定义.y、1个词法规则.l、1个测试用例hello.c及配套的koopa IR、汇编与目标文件等总大小仅108KB轻量易部署。目前已有99人学习下载附带完整实践报告与README说明内容覆盖设计思路、关键算法实现、测试方法及常见问题解析是高分课程实践的可靠参考范本。1. 这不是玩具编译器一个能跑通hello.c→hello.o→ RISC-V 真机执行的 SysY 编译器专为编译原理课设/期末大作业而生你手头正堆着《编译原理》清华第三版第二章的习题、老师刚布置的“实现一个前端中端后端”的课程设计、Deadline 前三天还卡在词法分析器报错——别急这不是又一个只画 AST 图就交差的 PPT 项目。这个基于 C 的 SysY 到 RISC-V 编译器是实打实跑通了完整编译流水线的高分课设它能把hello.cSysY 语法子集经词法分析.l、语法分析.y、AST 构建、IR 生成koopa IR、寄存器分配、指令选择、汇编生成.s最终产出标准 ELF 格式hello.o用riscv64-unknown-elf-gcc链接后真机烧录可执行。源码里每个.cpp文件都有中文注释main.cpp里清晰标出 pipeline 六阶段入口实践报告不是模板套话而是逐行解释sysy.y中$1 new AstNode(...)的语义动作如何绑定符号表、loop_maintainer.hpp怎么解决 SSA 形式下循环变量重命名冲突。它不依赖 LLVM 或 GCC 后端所有 IR 生成与 RISC-V 指令映射全手写——这意味着你能真正看懂addi t0, zero, 42是怎么从return 42;推导出来的。适合编译原理零基础但会写 C 的本科生也适合想补全编译器开发实战链路的嵌入式工程师。提示这不是教学演示器而是可调试、可修改、可扩展的生产级课设基线。所有测试用例test/目录均通过make test验证hello.c输出的hello.s可直接用riscv64-unknown-elf-as汇编无需魔改工具链。2. 从源码结构到编译流程六阶段流水线如何在 C 中落地2.1 整体架构为什么选 SysY Koopa IR RISC-V 三段式设计这个项目没走“C → x86”老路而是采用国内高校编译原理实践主流技术栈SysY 作为输入语言兼容 C89 子集语法简洁、无指针运算、无浮点降低前端复杂度、Koopa IR 作为中间表示SSA 形式、显式控制流图 CFG、支持 PHI 节点便于做循环优化和寄存器分配、RISC-V 32IMC 作为目标架构开源指令集、工具链成熟、QEMU 可仿真。这种组合不是炫技——SysY 规范明确 https://github.com/Compiler-Course/SysY 避免学生陷入 C 标准歧义Koopa IR 是清华编译原理课配套 IR见《编译原理》第三版第 7 章其.koopa文本格式可读性强hello.koopa文件能直接 human-readableRISC-V 工具链riscv64-unknown-elf-*在 Linux/macOS/WSL 上开箱即用比 x86 更易验证生成代码正确性。整个 pipeline 在main.cpp中由Driver类串联Lexer → Parser → AstBuilder → IrGenerator → CodeGen → AsmEmitter每个阶段输入输出类型严格定义如AstNode* → KoopaProgram* → std::vectorInstruction杜绝黑匣子调用。2.2 词法与语法分析Flex/Bison 如何协同构建 AST词法分析器sysy.l和语法分析器sysy.y是前端核心。sysy.l定义了INT,IF,WHILE,IDENTIFIER,NUMBER等 token关键在于保留字与标识符的优先级处理if { return IF; } while { return WHILE; } [a-zA-Z_][a-zA-Z0-9_]* { yylval.str strdup(yytext); return IDENTIFIER; }这里if必须写在IDENTIFIER规则之前否则if会被识别为普通标识符——这是新手高频翻车点。sysy.y使用 LALR(1) 分析核心是translation_unit→external_decl→func_definition的递归下降。语义动作中$$ new AstFuncDefNode($1, $2, $3, $4);创建 AST 节点其中$1是返回类型TypeNode*$2是函数名IdentifierNode*$3是参数列表ParamList*$4是函数体BlockNode*。注意$4的类型必须与BlockNode构造函数签名匹配否则 Bison 会报type mismatch错误。AstBuilder类负责将 Bison 生成的裸指针节点组装成带父子关系的树accept()方法递归遍历并填充childrenvector为后续 IR 生成提供结构化输入。2.3 中间表示生成Koopa IR 的 SSA 构建与 PHI 节点插入IR 生成在src/ir/目录下核心是IrGenerator类。它遍历 AST对每个节点调用visitXXX()方法如visitFuncDefNode()→visitBlockNode()→visitStmtNode()。关键难点在于SSA 形式的变量版本管理// visitAssignNode 示例 void IrGenerator::visitAssignNode(AstAssignNode* node) { auto lhs node-lhs(); // 左值可能是 IdentifierNode 或 ArrayAccessNode auto rhs node-rhs(); // 右值表达式 auto rhs_val visitExpr(rhs); // 生成 RHS 的 value如 %t0 // 对于简单赋值 a b c直接 store if (auto ident dynamic_castAstIdentifierNode*(lhs)) { auto var symbol_table_-lookup(ident-name()); // 从符号表查变量 builder_-store(rhs_val, var-get_addr()); // store %t0, %a.addr } // 对于数组 a[i] ...需先计算地址 else if (auto arr dynamic_castAstArrayAccessNode*(lhs)) { auto base_addr visitExpr(arr-base()); // a 的地址 auto idx visitExpr(arr-index()); // i 的值 auto elem_addr builder_-gep(base_addr, {idx}); // a[i] builder_-store(rhs_val, elem_addr); } }builder_是 Koopa IR 构建器store/load/add等方法生成对应 IR 指令。PHI 节点插入在block_maintainer.hpp中实现当 CFG 中某基本块有多个前驱时如 if-else 合并点BlockMaintainer::insertPhiNodes()扫描所有前驱块的活跃变量为每个变量生成 PHI 指令。例如%entry: br %cond, %then, %else %then: %a1 addi zero, 1 br %merge %else: %a2 addi zero, 2 br %merge %merge: %a3 phi %a1, %then, %a2, %else // 关键SSA 要求每个变量只有一个定义没有 PHI后续寄存器分配会因变量多定义而崩溃。2.4 RISC-V 后端从 Koopa IR 到汇编指令的精准映射CodeGen类将 Koopa IR 指令翻译为 RISC-V 汇编。核心原则是寄存器约束 指令选择寄存器分配使用线性扫描Linear Scan算法在register_allocator.hpp中实现。为每个虚拟寄存器%t0,%a1分配物理寄存器t0,a1处理sp栈指针、ra返回地址、s0帧指针等 callee-saved 寄存器的保存/恢复。指令选择InstructionSelector将 IR 操作映射为 RISC-V 指令。例如// Koopa IR: %t0 add %a, %b // → RISC-V: add t0, a0, a1 若 %a→a0, %b→a1 // 但若 %a 是常量addi t0, a0, 42AsmEmitter类生成.s文件关键逻辑在emitBinaryOp()中根据操作数类型寄存器/立即数/内存地址选择add/addi/lw/sw。hello.s中可见标准 RISC-V 函数序言.globl main main: addi sp, sp, -16 # 分配栈帧 sw ra, 12(sp) # 保存返回地址 sw s0, 8(sp) # 保存帧指针 addi s0, sp, 16 # 设置新帧指针 li a0, 42 # load immediate jal ra, printf # 调用库函数 lw ra, 12(sp) # 恢复返回地址 lw s0, 8(sp) # 恢复帧指针 addi sp, sp, 16 # 释放栈帧 jr ra # 返回2.5 实践报告不是文档堆砌而是调试日志的结构化复盘这份高分实践报告PDF的价值在于真实记录调试过程。例如在“寄存器分配失败”章节作者贴出 GDB 截图rax寄存器被错误复用导致printf参数错乱然后分析LinearScan::allocate()中active列表未按 lifetime 排序的 bug并给出修复补丁增加std::sort(active.begin(), active.end(), compareLifetime)。另一处详细说明sysy.y中expr : expr term的左递归如何引发 Bison shift/reduce 冲突以及如何通过改写为右递归expr : term { $$ $1; } | expr term { $$ new AddNode($1, $3); }解决。报告附录包含所有测试用例的输入/输出/IR/汇编对照表test/hello.c的hello.koopa和hello.s文件均标注关键行号与 AST 节点映射方便你对照源码理解每行汇编的来源。3. 编译部署四步走从 CMake 构建到 RISC-V 真机验证3.1 环境准备三行命令搞定跨平台工具链项目使用 CMake 构建无需手动配置 VSCode 或 MSVC避坑点见 4.1。Linux/macOS 用户只需# 1. 安装 RISC-V 工具链Ubuntu/Debian sudo apt install gcc-riscv64-unknown-elf binutils-riscv64-unknown-elf # 2. 安装 Flex/Bison词法/语法分析器生成器 sudo apt install flex bison # macOS: brew install flex bison # 3. 验证安装 riscv64-unknown-elf-gcc --version # 应输出 12.x bison --version # 应输出 3.8Windows 用户推荐 WSL2Ubuntu 22.04避免 Cygwin 或 MinGW 的路径兼容问题。不要用 MSVC 编译器——本项目依赖 POSIX 系统调用如fork用于测试MSVC 不支持。3.2 构建项目CMakeLists.txt 的关键配置解析根目录CMakeLists.txt定义了标准 C17 构建流程cmake_minimum_required(VERSION 3.10) project(SysYCompiler LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 查找 Flex/Bison 并生成 lexer/parser find_package(FLEX REQUIRED) find_package(BISON REQUIRED) FLEX_TARGET(Lexer src/sysy.l ${CMAKE_CURRENT_BINARY_DIR}/lexer.cpp) BISON_TARGET(Parser src/sysy.y ${CMAKE_CURRENT_BINARY_DIR}/parser.cpp) # 添加可执行文件 add_executable(sysyc main.cpp ${FLEX_Lexer_OUTPUTS} ${BISON_Parser_OUTPUTS} # ... 其他源文件 ) # 链接标准库 target_link_libraries(sysyc stdcfs) # C17 filesystem 支持构建命令mkdir build cd build cmake .. -DCMAKE_BUILD_TYPEDebug # Debug 模式便于 gdb 调试 make -j$(nproc) # 并行编译生成的sysyc可执行文件位于build/目录。注意CMAKE_BUILD_TYPE必须显式指定否则默认None会导致-O0优化关闭IR 生成阶段可能因未初始化变量而 segfault。3.3 运行编译一条命令完成全流程验证进入test/目录执行# 编译 hello.c → hello.koopa → hello.s → hello.o ../build/sysyc hello.c -o hello.s # 汇编并链接需 riscv64-unknown-elf-gcc riscv64-unknown-elf-gcc -o hello.elf hello.s # 在 QEMU 中运行验证功能 qemu-riscv64 ./hello.elf # 输出 Hello, World! # 或生成纯二进制用于 FPGA/开发板 riscv64-unknown-elf-objcopy -O binary hello.elf hello.bin-o hello.s参数指定输出汇编文件sysyc内部自动调用IrGenerator→CodeGen→AsmEmitter。若需查看 IR加-k参数../build/sysyc hello.c -k hello.koopa。3.4 真机验证QEMU 与物理开发板的两种部署路径QEMU 仿真推荐入门# 安装 QEMU RISC-V 支持 sudo apt install qemu-system-misc # 运行 elf 文件自动加载 libc qemu-riscv64 -L /usr/riscv64-unknown-elf sysroot ./hello.elf物理开发板如 GD32VF103将hello.bin烧录到 Flash使用 OpenOCD 或 J-Link修改link.ld项目未提供需自行创建MEMORY { RAM (rwx) : ORIGIN 0x20000000, LENGTH 128K } SECTIONS { .text : { *(.text) } RAM .data : { *(.data) } RAM .bss : { *(.bss) } RAM }链接时指定riscv64-unknown-elf-gcc -T link.ld -o hello.elf hello.s注意物理板需自行移植printf到 UART项目hello.c默认调用printf若无 libc 需替换为裸机putchar。4. 避坑指南五个让课设答辩前夜崩溃的致命细节4.1 现象Bison 报错conflicts: 1 shift/reduce编译器生成空 AST原因sysy.y中表达式文法存在左递归如expr : expr termBison 默认 LALR(1) 分析器无法处理触发冲突并静默忽略规则。解决将左递归改写为右递归并添加语义动作确保结合性。修改sysy.y// 错误写法左递归 expr : expr term { $$ new AddNode($1, $3); } | term ; // 正确写法右递归 结合性 expr : term { $$ $1; } | expr term { $$ new AddNode($1, $3); } ;同时在%left -声明运算符结合性确保abc解析为(ab)c。4.2 现象hello.s生成成功但qemu-riscv64报Segmentation fault原因RISC-V 调用约定要求a0-a7传递前 8 个参数但printf(Hello)中字符串地址未正确加载到a0。AsmEmitter的emitCall()方法未处理字符串常量的.rodata段引用。解决在AsmEmitter::emitCall()中对字符串字面量生成.rodata段// 在 emitCall 前插入 if (auto str dynamic_castAstStringLiteralNode*(arg)) { out_ \tla a0, .LC str_label_id_ \n; out_ .LC str_label_id_ :\n\t.asciz \ str-value() \\n; } else { // 处理其他参数... }4.3 现象make test失败test/fib.c输出结果为0而非55原因loop_maintainer.hpp中的 PHI 节点插入逻辑未覆盖for循环的初始化部分。for (int i0; i10; i)的i0初始化被当作独立基本块导致 PHI 节点缺少该前驱。解决在BlockMaintainer::analyzeLoop()中将for循环的初始化块init_block显式加入loop_header-predecessorsvoid BlockMaintainer::analyzeLoop(KoopaBasicBlock* header) { // ... 原有逻辑 for (auto pred : header-predecessors()) { if (isInLoop(pred, loop)) { loop_blocks.insert(pred); // 关键确保 init_block 被识别为前驱 if (pred-is_init_block()) { // 自定义标记 header-predecessors().push_back(pred); } } } }4.4 现象VSCode 调试sysyc时断点无效GDB 显示No symbol table loaded原因CMake 默认 Release 模式不生成调试信息且CMAKE_BUILD_TYPE未设置。解决构建时强制 Debug 模式并启用调试符号cd build cmake .. -DCMAKE_BUILD_TYPEDebug -DCMAKE_CXX_FLAGS-g -O0 make clean makeVSCode 的launch.json需配置{ configurations: [{ name: (gdb) Launch, type: cppdbg, request: launch, program: ${workspaceFolder}/build/sysyc, args: [test/hello.c, -o, hello.s], stopAtEntry: false, cwd: ${workspaceFolder}, environment: [], externalConsole: false, MIMode: gdb, setupCommands: [{ description: Enable pretty-printing, text: -enable-pretty-printing }] }] }4.5 现象riscv64-unknown-elf-gcc链接时报undefined reference to printf原因项目生成的hello.s调用printf但未链接 Newlib C 库。RISC-V 工具链默认不包含完整 libc。解决链接时添加-lc和-lgcc并指定 sysrootriscv64-unknown-elf-gcc -static -o hello.elf hello.s \ -L/usr/riscv64-unknown-elf/lib \ -lc -lgcc或更稳妥地用riscv64-unknown-elf-gcc直接编译 C 文件验证工具链echo int main(){return 0;} | riscv64-unknown-elf-gcc -x c - -o test.elf5. 进阶技巧用 GDB 反向追踪汇编指令到 AST 节点的三步定位法5.1 构建带调试信息的完整工具链要实现“看到add t0, a0, a1就知道它来自hello.c第 5 行c a b”必须让编译器生成 DWARF 调试信息。修改CMakeLists.txt# 在 target_compile_options 后添加 target_compile_options(sysyc PRIVATE -g -gdwarf-4) target_link_libraries(sysyc PRIVATE -g)重新构建后sysyc可执行文件包含源码行号映射。验证readelf -w build/sysyc | head -20 # 查看 DWARF 段5.2 GDB 中的 AST → IR → ASM 三级跳转假设hello.c第 7 行return a b;生成错误汇编启动 GDBgdb ./build/sysyc (gdb) b main.cpp:123 # 在 IrGenerator::visitReturnNode() 设断点 (gdb) r test/hello.c -o hello.s (gdb) step # 进入 visitExpr()再 step 进入 visitBinaryOp() (gdb) print $1 # 查看 AST 节点内容确认是 AddNode (gdb) print *$1 # 打印 AST 结构获取 left/right 子节点此时已定位到 AST。继续(gdb) b ir_generator.cpp:456 # visitBinaryOp() 中 emitAdd() 行 (gdb) c (gdb) print builder_-current_bb()-insts().back() # 查看最后一条 IR 指令 # 输出类似add %t0, %a, %bIR 确认无误后切到汇编生成阶段(gdb) b asm_emitter.cpp:218 # emitBinaryOp() 中 emitAdd() 行 (gdb) c (gdb) print inst-op() # 确认是 KOOPA_OP_ADD (gdb) print inst-lhs()-name() # %a → 物理寄存器 a0 (gdb) print inst-rhs()-name() # %b → 物理寄存器 a15.3 汇编指令反查源码行DWARF 与符号表联动生成hello.s后用riscv64-unknown-elf-objdump反汇编riscv64-unknown-elf-objdump -S hello.elf hello.dump在hello.dump中找到00000000000100b0 main: 100b0: 1141 addi sp,sp,-16 100b2: e406 sd ra,12(sp) 100b4: e022 sd s0,8(sp) 100b6: 0801 addi s0,sp,16 100b8: 00050513 li a0,0 100bc: 00058593 li a1,0 100c0: 02f50533 add a0,a1,a0 # ← 这行对应 return ab用addr2line反查源码riscv64-unknown-elf-addr2line -e hello.elf -a 0x100c0 # 输出test/hello.c:7关键技巧addr2line依赖.debug_line段必须确保sysyc编译时带-g且hello.s生成时保留.loc指令AsmEmitter中out_ \t.loc 1 line \n;。5.4 实战案例修复fib.c的栈溢出test/fib.c在 QEMU 中 SegfaultGDB 定位到fib函数栈帧过大(gdb) run test/fib.c -o fib.s (gdb) bt #0 0x00000000000101a0 in fib () #1 0x00000000000101a0 in fib () # 无限递归检查fib.s发现fib: addi sp, sp, -32 # 分配 32 字节栈帧 sd ra, 24(sp) # 保存 ra sd s0, 20(sp) # 保存 s0 addi s0, sp, 32 # 设置 fp # ... 但未检查 n2 就递归调用根源在AstIfNode::accept()未生成beqz跳转而是无条件递归。修复visitIfNode()void IrGenerator::visitIfNode(AstIfNode* node) { auto cond_val visitExpr(node-cond()); auto then_bb builder_-create_basic_block(); auto else_bb builder_-create_basic_block(); auto merge_bb builder_-create_basic_block(); builder_-br_cond(cond_val, then_bb, else_bb); // 关键插入条件分支 builder_-set_insert_point(then_bb); visitStmt(node-then_body()); builder_-br(merge_bb); builder_-set_insert_point(else_bb); if (node-else_body()) visitStmt(node-else_body()); builder_-br(merge_bb); builder_-set_insert_point(merge_bb); }修复后fib.s出现beqz a0, .L2分支栈溢出消失。从那以后我每次提交课设代码前都强制走一遍gdb ./build/sysyc test/hello.c -o hello.saddr2line反查确保每一行汇编都能追溯到 AST 节点和源码行。这不仅是调试手段更是理解编译器如何“思考”的必经之路——当你能指着add t0, a0, a1说“这是hello.c第 5 行的运算”编译原理才算真正长进了骨头里。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Qt环境下MQTT客户端完整实现:协议解析与工程实战 2026/10/2 7:32:46

Qt环境下MQTT客户端完整实现:协议解析与工程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
梯度、散度、方向导数与拉普拉斯算子:从几何直觉到工程应用 2026/10/2 7:32:46

梯度、散度、方向导数与拉普拉斯算子:从几何直觉到工程应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
压力露点与常压露点换算:从原理到干燥器选型实战 2026/10/2 7:32:46

压力露点与常压露点换算:从原理到干燥器选型实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
电路分析入门:参考方向、核心元件与电流采样实战 2026/10/2 7:32:45

电路分析入门:参考方向、核心元件与电流采样实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
数据不动算力跑:存算分离架构落地实践与避坑指南 2026/10/2 7:32:45

数据不动算力跑:存算分离架构落地实践与避坑指南

1. “数据不动算力跑”这句话背后,到底藏了哪些真问题先从一个真实场景说起。前两年给一个网约车数据分析项目做架构调整,跑的是Spark离线ETL和Flume日志采集,集群规模不大但特别“拧巴”:白天订单数据疯狂涌入,存储水…

阅读更多 →
不同特征值的特征向量线性无关——证明、误区与对角化应用 2026/10/2 7:32:32

不同特征值的特征向量线性无关——证明、误区与对角化应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉