ARTICLE DETAIL

资讯详情

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

南开编译原理作业:Flex+Bison实现C子集编译器实战指南

南开编译原理作业:Flex+Bison实现C子集编译器实战指南 简介本资源是南开大学软件学院《编译原理》课程的高质量课程设计成果面向计算机、人工智能、通信工程等专业在校学生及初学者提供一个可运行、可理解、可拓展的简易C语言编译器完整实现。压缩包共50个文件含16个C源文件cpp实现词法分析、语法分析与中间代码生成18个头文件h封装核心数据结构与接口6个C文件c支撑运行时支持与测试用例辅以Yacc/Bison语法定义.y、Lex词法描述.l、Makefile构建脚本及IR.md等关键文档整体仅54KB轻量易读。已有213人学习下载项目经实际编译测试与答辩验证功能完整、运行稳定平均评审分达96分。读者可直接运行示例程序如fibo.c、array.c结合README.md快速上手亦可深入理解符号表、语法树、三地址码等核心编译环节为课程设计、毕设开发或编译原理进阶学习提供扎实的代码范例与工程参考。1. 南开大学软件学院编译原理作业为什么一个“简单C语言编译器”能卡住90%的本科生这不是一个玩具项目而是一道真实压在南开软院学生课表上的硬核关卡——它要求你亲手写出一个能处理int main(){return 0;}到a b c * 2;级别语法、生成可执行汇编或中间代码、通过 Makefile 自动构建、附带完整文档说明的最小可行C子集编译器。它不追求支持指针运算或结构体嵌套但必须严格遵循词法分析→语法分析→语义检查→中间代码生成→目标代码输出的全流程闭环。很多同学栽在grammar.y文件里一个|符号位置不对或Makefile中yacc -d grammar.y和gcc -o compiler lex.yy.c y.tab.c的依赖顺序写反导致make报错却查不出根源。如果你正被“编译原理实验”“编译器未包含main类型”“vitis make[2]: *** [makefile:18: libs] error 1”这类错误反复折磨这篇笔记就是为你写的血泪复盘——它不讲龙书第几章只告诉你从 clone 仓库到./compiler test.c输出正确.s文件每一步该敲什么、为什么这么敲、哪里最容易翻车。2. 用 flex bison 在本地跑通 C 子集编译器从 grammar.y 到可执行二进制的最小命令链这个项目本质是用 Unix 工具链实现经典编译器前端三步词法扫描flex、语法解析bison、语义动作C 代码嵌入。南开软院作业明确要求使用grammar.yBison 输入和配套.l文件Flex 输入最终生成y.tab.c和lex.yy.c再经 GCC 编译链接。整个流程不能跳过任何一环否则make会报“找不到 y.tab.h”或“undefined reference to yyparse”。2.1 生成 parser.c 和 scanner.cbison 和 flex 的标准协作流程先确认系统已安装 flex 和 bisonUbuntu/Debian 下sudo apt install flex bisonmacOS 用brew install flex bison。注意不要用 macOS 自带的古早 bison它不兼容%define api.pure full语法必须用 Homebrew 安装新版# macOS 用户务必执行验证版本 ≥ 3.8 brew install bison echo export PATH/opt/homebrew/opt/bison/bin:$PATH ~/.zshrc source ~/.zshrc bison --version # 应输出 3.8.x 或更高然后进入项目根目录含grammar.y和lexer.l执行标准生成链# 1. 用 bison 生成语法分析器含头文件 y.tab.h bison -d -v grammar.y # -d 生成 y.tab.h-v 生成 grammar.output用于调试冲突 # 2. 用 flex 生成词法分析器注意必须先有 y.tab.h flex lexer.l # 3. 此时应得到y.tab.c, y.tab.h, lex.yy.c ls y.tab.* lex.yy.c关键逻辑说明bison -d生成y.tab.c含yyparse()函数和y.tab.h定义 token 枚举和 YYSTYPE 类型flex读取y.tab.h中的YYSTYPE和YYSTYPE定义才能让yylval类型与语法分析器对齐。若先运行flex再运行bisonlex.yy.c会因找不到y.tab.h而编译失败。2.2 编译链接生成 compiler 可执行文件GCC 参数与依赖顺序生成源文件后需用 GCC 编译并链接。常见错误是忽略y.tab.c和lex.yy.c的编译顺序或漏掉-ly链接 yacc 库# 正确编译命令四步合一显式指定所有依赖 gcc -o compiler y.tab.c lex.yy.c -ly # 若提示 undefined reference to yywrap说明 lexer.l 中未定义 yywrap() # 解决在 lexer.l 最末尾添加 %% int yywrap() { return 1; }参数说明-o compiler指定输出可执行文件名为compilery.tab.c lex.yy.c必须按此顺序因为y.tab.c调用yylex()而lex.yy.c提供该函数-ly链接系统 yacc 运行时库提供yyerror()默认实现等缺此参数会报undefined reference to yylex为什么不用gcc -o compiler *.c -ly因为项目可能含main.c或codegen.c盲目通配会引入未初始化的全局变量或重复定义main()必须显式列出核心文件。2.3 验证 parser 是否工作用 echo 测试最简输入在未接入完整语义动作前先验证语法分析器能否识别合法 C 片段# 创建测试文件 minimal.c echo int main() { return 0; } minimal.c # 运行编译器此时 compiler 应仅做语法检查不生成代码 ./compiler minimal.c # 若无输出说明语法接受若报 syntax error说明 grammar.y 有误调试技巧在grammar.y的%parse-param中加入FILE *out并在每个产生式末尾加fprintf(out, reduce: %s\n, FuncDef);再运行./compiler minimal.c 2 debug.log即可追踪归约路径。这是排查shift/reduce conflict的唯一可靠手段。3. Makefile 的三个必调参数解决 “make 没有指明目标并且找不到 makefile” 和 “vitis make[2]: *** [makefile:18: libs] error 1”南开作业明确要求提供Makefile且必须支持make all、make clean、make test。网络上大量学生因Makefile编写不规范在 Vitis、WSL 或 macOS 上遭遇No targets specified and no makefile found或make[2]: *** [makefile:18: libs] error 1。根本原因不是环境问题而是 Makefile 缺少显式默认目标、隐式规则覆盖不当、依赖关系断裂这三点。3.1 必须声明 .PHONY 和默认目标避免 “No targets specified”很多学生直接写# ❌ 错误写法无默认目标且 clean 不是伪目标 compiler: y.tab.c lex.yy.c gcc -o compiler y.tab.c lex.yy.c -ly clean: rm -f y.tab.c y.tab.h lex.yy.c compiler当执行make时GNU Make 会尝试把compiler当作文件名去检查是否存在若compiler二进制已存在就跳过编译——导致改了grammar.y却不重新生成y.tab.c。正确写法必须# ✅ 正确 Makefile 片段开头必须有 .PHONY: all clean test all: compiler compiler: y.tab.c lex.yy.c gcc -o compiler y.tab.c lex.yy.c -ly # 显式声明依赖y.tab.c 必须由 grammar.y 生成lex.yy.c 必须由 lexer.l 生成 y.tab.c y.tab.h: grammar.y bison -d -v grammar.y lex.yy.c: lexer.l y.tab.h flex lexer.l clean: rm -f y.tab.c y.tab.h lex.yy.c compiler *.output test: compiler echo Testing with minimal.c... ./compiler minimal.c echo ✅ Syntax OK || echo ❌ Syntax Error为什么y.tab.h要列为lex.yy.c的依赖因为flex生成的lex.yy.c会#include y.tab.h若y.tab.h不存在或过期gcc编译lex.yy.c时直接报错。Make 会自动检测y.tab.h时间戳确保先更新头文件再生成扫描器。3.2 处理 vitis/make[2] 错误定位第 18 行的真实含义报错vitis make[2]: *** [makefile:18: libs] error 1中的makefile:18是触发错误的那行命令所在行号而非错误本身。例如15: libs: $(OBJ) 16: $(CC) -shared -o libparser.so $(OBJ) # ← 第 16 行 17: 18: $(OBJ): %.o: %.c # ← 第 18 行这里是规则定义不执行 19: $(CC) -c $ -o $ # ← 真正出错的是第 19 行的命令此时make[2]表示这是子 make 进程error 1意味着第 19 行的gcc -c xxx.c命令返回非零退出码。排查步骤手动执行gcc -c y.tab.c -o y.tab.o看是否报y.tab.c:123:10: error: unknown type name ‘YYSTYPE’若报此错说明y.tab.h未被y.tab.c正确包含检查y.tab.c开头是否有#include y.tab.hbison 默认生成但若手动修改过可能删掉若无报错再执行gcc -shared -o libparser.so y.tab.o lex.yy.o看是否提示undefined reference to yylex—— 这说明lex.yy.o未链接需确认$(OBJ)变量是否包含lex.yy.o3.3 适配不同平台的 CC 和 CFLAGS解决 msvc/gcc 混用问题南开作业未限定编译器但学生常在 Windows 上用 MSVC、Linux 上用 GCC、macOS 上用 Clang。Makefile必须可移植# 根据系统自动选择编译器 ifeq ($(OS),Windows_NT) CC cl CFLAGS /c /W3 LIBS else UNAME_S : $(shell uname -s) ifeq ($(UNAME_S),Linux) CC gcc endif ifeq ($(UNAME_S),Darwin) CC clang endif CFLAGS -Wall -g LIBS -ly endif # 统一编译规则 %.o: %.c $(CC) $(CFLAGS) -c $ -o $注意MSVC 不支持-ly其等价操作是/link legacy_stdio_definitions.lib但本项目无需动态库故 Windows 用户建议直接用 WSL 或 MinGW避免 MSVC 兼容性黑洞。4. grammar.y 的三大避坑点解决 “编译器的堆空间不足” 和 “编译器未包含main类型”grammar.y是整个编译器的骨架90% 的崩溃源于此处。南开作业要求支持int main() { ... }但学生常因语法定义不严谨导致yyparse()在遇到main时直接 abort或生成 AST 时 malloc 失败报 “堆空间不足”。以下是三个高频翻车现场4.1 现象Segmentation fault (core dumped)或malloc(): corrupted unsorted chunks原因YYSTYPE定义为union时未为所有字段分配足够内存或在$$ $1赋值时发生野指针拷贝。例如// ❌ 危险写法str 字段未分配内存直接 strcpy %union { int ival; char* str; } %token str IDENTIFIER %% assignment: IDENTIFIER expression ; { strcpy($$, $1); // $1 是未 malloc 的栈地址 }解决所有字符串必须malloc strcpy且在free()时统一管理%union { int ival; char* str; } %destructor { free($$); } str %% assignment: IDENTIFIER expression ; { $$ malloc(strlen($1) 1); strcpy($$, $1); }destructor告诉 Bison当$1即IDENTIFIER的str被归约弹出栈时自动调用free($1)避免内存泄漏。4.2 现象syntax error, unexpected IDENTIFIER, expecting (原因main被识别为IDENTIFIER但语法规则未将其提升为FunctionDefinition。典型错误是function_definition规则缺少main特例// ❌ 错误只匹配 int IDENTIFIER但没处理 main 作为固定标识符 function_definition: TYPE IDENTIFIER ( parameter_list ) compound_statement // ✅ 正确显式支持 main 函数TYPE 可为 intIDENTIFIER 必须为 main function_definition: INT_KEYWORD MAIN_KEYWORD ( ) compound_statement | TYPE IDENTIFIER ( parameter_list ) compound_statement并在词法分析中将main单独设为MAIN_KEYWORDtokenmain { return MAIN_KEYWORD; } [a-zA-Z_][a-zA-Z0-9_]* { yylval.str strdup(yytext); return IDENTIFIER; }4.3 现象error: main undeclared (first use in this function)原因语义动作中未检查main函数是否存在或未在符号表中注册main为全局函数。即使语法接受int main(){}, 若语义分析阶段未标记该函数为程序入口后续代码生成会跳过它。解决在function_definition归约时插入检查function_definition: INT_KEYWORD MAIN_KEYWORD ( ) compound_statement { if (strcmp($2, main) ! 0) { yyerror(Only int main() is allowed as entry point); YYERROR; } // 注册 main 到符号表 insert_function(main, int, NULL); }玄学经验yyerror()不会自动终止解析必须跟YYERROR才真正抛异常。只写yyerror()会导致错误后继续归约产生更诡异的 secondary error。5. 从 C 源码到汇编输出中间代码生成与目标代码落地的实操路径南开作业要求“生成可执行代码”但未限定目标平台。实践中生成 ATT 语法 x86-64 汇编.s 文件是最稳妥的选择——它不依赖操作系统 ABI可用gcc -c直接转为目标文件且调试直观。本节聚焦如何在grammar.y的语义动作中嵌入代码生成逻辑绕过复杂的 LLVM 或手写机器码。5.1 设计三地址码TAC结构体为后续汇编生成铺路不直接生成汇编而是先生成易读、易优化的三地址码。定义struct tactypedef struct tac_ { char op[10]; // add, mov, call char arg1[32]; // 目标操作数 char arg2[32]; // 源操作数1 char arg3[32]; // 源操作数2可选 struct tac_* next; } TAC; TAC* tac_head NULL; TAC* tac_tail NULL; void emit(char* op, char* a1, char* a2, char* a3) { TAC* t malloc(sizeof(TAC)); strcpy(t-op, op); strcpy(t-arg1, a1); if (a2) strcpy(t-arg2, a2); else t-arg2[0] \0; if (a3) strcpy(t-arg3, a3); else t-arg3[0] \0; t-next NULL; if (!tac_head) tac_head tac_tail t; else { tac_tail-next t; tac_tail t; } }在assignment归约中调用assignment: IDENTIFIER expression ; { emit(mov, $1, $3, NULL); // mov %eax, var_name }5.2 将 TAC 转为 x86-64 汇编寄存器分配与栈帧管理汇编生成核心是寄存器映射和栈偏移计算。为简化采用“每个表达式结果存入 %rax”的策略void generate_asm(FILE* out) { fprintf(out, \t.text\n); fprintf(out, \t.globl main\n); fprintf(out, main:\n); fprintf(out, \tpushq\t%%rbp\n); fprintf(out, \tmovq\t%%rsp, %%rbp\n); TAC* t tac_head; while (t) { if (strcmp(t-op, mov) 0) { fprintf(out, \tmovq\t$%s, %%rax\n, t-arg2); // 假设 arg2 是立即数 fprintf(out, \tmovq\t%%rax, %s(%%rbp)\n, t-arg1); } else if (strcmp(t-op, add) 0) { fprintf(out, \tmovq\t%s(%%rbp), %%rax\n, t-arg2); fprintf(out, \taddq\t%s(%%rbp), %%rax\n, t-arg3); fprintf(out, \tmovq\t%%rax, %s(%%rbp)\n, t-arg1); } t t-next; } fprintf(out, \tmovq\t$0, %%rax\n); // return 0 fprintf(out, \tpopq\t%%rbp\n); fprintf(out, \tret\n); }关键细节%rbp作为帧指针所有局部变量以var_name(%rbp)形式寻址。movq $0, %rax是return 0的标准写法避免ret前未设置返回值。5.3 集成到主流程从 parser 到 .s 文件的完整链路修改main()函数使其接收输入文件、调用yyparse()、最后调用generate_asm()int main(int argc, char* argv[]) { if (argc ! 3) { fprintf(stderr, Usage: %s input.c output.s\n, argv[0]); return 1; } yyin fopen(argv[1], r); if (!yyin) { perror(fopen); return 1; } yyparse(); fclose(yyin); FILE* out fopen(argv[2], w); if (!out) { perror(fopen output); return 1; } generate_asm(out); fclose(out); printf(✅ Assembly written to %s\n, argv[2]); return 0; }然后更新Makefile支持make compilecompile: compiler ./compiler test.c test.s gcc -c test.s -o test.o gcc test.o -o test.out ./test.out # 应输出 0验证技巧用objdump -d test.o查看反汇编确认movq $0, %rax和ret存在用readelf -a test.o | grep -A5 Symbol table检查main符号是否为STB_GLOBAL。6. 文档说明怎么写才过关用 Sphinx Markdown 自动生成 API 与语法树图南开作业明确要求“文档说明”但多数学生交 PDF 截图或 Word 堆砌文字被扣分。真正被认可的文档必须满足三点可执行性命令能跑通、可追溯性代码行号对应文档段落、可视化AST 图/语法树。我用 Sphinx Graphviz MyST Markdown 实现一键生成比手写高效 10 倍。6.1 用 doxygen 提取代码注释生成 API 文档在grammar.y关键函数旁加 Doxygen 注释/** * brief Emit a three-address code instruction * param op Operation name (mov, add) * param a1 Destination operand * param a2 Source operand 1 (optional) * param a3 Source operand 2 (optional) * see generate_asm() */ void emit(char* op, char* a1, char* a2, char* a3) { // ... }安装 doxygen 后执行doxygen -g Doxyfile # 生成配置 sed -i s/GENERATE_HTML YES/GENERATE_HTML YES\nGENERATE_LATEX NO/g Doxyfile doxygen Doxyfile生成的html/index.html自动包含函数列表、调用图、文件依赖图。6.2 用 graphviz 可视化语法树从 .output 文件提取 DOTbison -v grammar.y生成的grammar.output包含状态转移图。用 Python 脚本提取并转为 DOT# gen_ast_graph.py import re with open(grammar.output) as f: content f.read() # 提取产生式规则如 function_definition - INT_KEYWORD MAIN_KEYWORD ( ) compound_statement rules re.findall(r^\s*\d\.\s(.?)\s*$, content, re.M) with open(ast.dot, w) as f: f.write(digraph G {\n) f.write( rankdirLR;\n) for rule in rules[:10]: # 取前 10 条核心规则 left, right rule.split(-, 1) left left.strip() for term in right.strip().split(): f.write(f {left} - {term};\n) f.write(})然后生成 PNGdot -Tpng ast.dot -o ast.png6.3 用 MyST Markdown 写用户手册支持数学公式与代码块嵌入docs/index.md示例# 南开编译原理作业C子集编译器文档 ## 语法支持范围 支持以下 C 语法符合 ISO/IEC 9899:1990 第 3.5 节 | 语法成分 | 示例 | 限制 | |----------|------|------| | 函数定义 | int main() { } | 仅允许 main 为入口 | | 赋值语句 | a b 1; | 不支持复合赋值 | | 整型字面量 | 42, 0xFF | 不支持浮点 | ## 生成汇编示例 输入 test.c c int main() { int a 1; int b 2; a a b; return 0; }输出test.s关键片段movq $1, %rax movq %rax, a(%rbp) movq $2, %rax movq %rax, b(%rbp) movq a(%rbp), %rax addq b(%rbp), %rax movq %rax, a(%rbp)提示a(%rbp)表示以%rbp为基址偏移a的内存位置这是 x86-64 栈帧标准寻址方式。最后用 pip install myst-parser sphinx make html 生成专业文档网站。 --- 我带过三届南开软院编译原理助教每年都有学生卡在 makefile:18 或 grammar.y 的 | 符号上熬通宵。后来我定下铁律**写完一行 Bison 规则立刻用 bison -v 看 grammar.output 里有没有 shift/reduce conflict改完 Makefile第一件事是 make -n 检查命令是否按预期展开生成 .s 文件后必用 gcc -c 验证能否过汇编阶段**。这些不是玄学是用 37 个失败的 core dumped 换来的肌肉记忆。希望帮到你。 p a hrefhttps://download.csdn.net/download/m0_73728511/88714219 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表