ARTICLE DETAIL

资讯详情

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

南开软院C语言编译器课设:Flex+Bison实现四阶段编译链路

南开软院C语言编译器课设:Flex+Bison实现四阶段编译链路 简介本资源是南开大学软件学院编译原理课程的高分课程设计成果——一个可运行的简易C语言编译器配套完整源码与技术文档面向计算机、人工智能、电子信息等专业的在校学生及初学者助力理解词法分析、语法解析、中间代码生成等核心编译流程。压缩包共50个文件含18个头文件.h定义数据结构与接口、16个C源文件.cpp实现核心模块、6个C文件.c支撑底层功能另有Yacc/Bison语法文件.y、Lex词法文件.l、Makefile构建脚本及README.md使用指南整体仅54KB轻量易读。已有213人学习下载项目经实际编译测试验证支持多个典型C示例如fibo.c、array.c、struct.c等答辩平均分96分附带IR中间表示说明与符号表设计思路可直接用于课程设计、毕设参考或编译原理实践进阶。1. 这不是玩具编译器南开软院96分课设C语言编译器能跑fibonacci、数组、结构体、循环还能生成中间代码IR你手头那份“编译原理作业.zip”解压后看到grammar.y、lexer.l、Makefile和一堆.c文件时第一反应可能是——这玩意儿真能跑别急着删它真能。这不是教学演示用的伪代码而是南开大学软件学院学生实打实跑通的完整编译器原型输入fibo.c斐波那契递归输出可执行二进制输入struct.c含嵌套结构体指针访问能正确解析域偏移输入basic_loop.cwhilebreakcontinue生成带跳转标签的三地址码IR甚至array.c里二维数组a[3][4]的下标计算和内存布局都经得起反汇编验证。它不支持#include、不处理浮点、没有优化但词法→语法→语义→IR生成四阶段全链路打通且所有测试用例test/下共7个在 Ubuntu 20.04 flex/bison 2.7.4 gcc 9.4 环境下make make test全部通过。适合刚学完《编译原理》第三版第二章文法与推导的学生上手调试也适合人工智能方向想理解“程序如何被机器读懂”的同学拆解底层逻辑——毕竟LLM再强也得靠编译器把Python代码喂给CPU。2. 从零构建FlexBisonGCC三件套驱动的编译流水线这个编译器不是黑匣子它的构建流程完全暴露在 Makefile 和源码结构里。核心是三个工具链的协同flex负责词法分析.l文件bison负责语法分析.y文件gcc负责最终目标码生成.c文件。整个流程不依赖IDE或高级框架纯命令行驱动对理解编译器本质极其友好。下面带你一步步走通从源码到可执行的全过程重点讲清每个环节的输入/输出、关键参数含义以及为什么必须按这个顺序执行。2.1 词法分析器lexer.l 如何把字符流切分成 token词法分析是编译的第一道关卡lexer.l文件定义了所有合法 token 的正则模式。打开该文件你会看到类似这样的规则%{ #include trees.h #include symbol.h %} %% int|char|void|return|if|else|while|for|break|continue { yylval.str strdup(yytext); return TYPE; } [0-9] { yylval.num atoi(yytext); return NUMBER; } [a-zA-Z_][a-zA-Z0-9_]* { yylval.str strdup(yytext); return IDENTIFIER; } |!|||||| { yylval.str strdup(yytext); return RELOP; } |-|*|/|%|||-- { yylval.str strdup(yytext); return OP; } [\t\n ] { /* 忽略空白 */ } . { return yytext[0]; } /* 其他单字符直接返回ASCII码 */ %% int yywrap() { return 1; }提示yylval是 Bison 与 Flex 之间传递语义值的全局联合体其定义在trees.h中见typedef union { int num; char *str; struct node *node; } YYSTYPE;。yytext指向当前匹配的字符串yywrap()是 Flex 的 EOF 处理钩子必须返回1否则会无限循环。这段代码的关键在于TYPE、NUMBER、IDENTIFIER等 token 类型需与grammar.y中的%token声明严格一致strdup(yytext)是必须操作因为yytext指向的缓冲区在下一次yylex()调用时会被覆盖空白符[ \t\n]的处理不能写成[[:space:]]某些旧版 flex 不支持 POSIX 字符类会导致make报错undefined symbol: yywrap。运行flex lexer.l后生成lex.yy.c这是一个标准 C 文件可直接用gcc -c lex.yy.c编译。但注意不要手动编译它Makefile 已将其作为依赖项自动处理。2.2 语法分析器grammar.y 如何用 LALR(1) 解析 C 子集grammar.y是整个编译器的骨架定义了 C 语言子集的上下文无关文法CFG。它采用经典的 LALR(1) 分析方式由 Bison 自动生成分析表。打开该文件你会看到%{ #include stdio.h #include stdlib.h #include trees.h #include symbol.h extern int yylex(); extern int yyparse(); extern FILE *yyin; extern char *yytext; void yyerror(const char *s); %} %union { int num; char *str; struct node *node; } %token str IDENTIFIER TYPE %token num NUMBER %token IF ELSE WHILE FOR RETURN BREAK CONTINUE SEMI COMMA LP RP LB RB LC RC ASSIGN PLUS MINUS TIMES DIV MOD EQ NEQ LT GT LE GE AND OR NOT %type node program ext_def_list ext_def opt_semi_list se_list se opt_semi statement compound_stmt declaration_list declaration specifier init_declarator_list init_declarator declarator direct_declarator pointer type_specifier abstract_declarator parameter_list parameter_declaration argument_expression_list assignment_expression expression logical_or_expression logical_and_expression inclusive_or_expression exclusive_or_expression and_expression equality_expression relational_expression shift_expression additive_expression multiplicative_expression cast_expression unary_expression postfix_expression primary_expression %start program %% program : ext_def_list { /* 根节点构造 */ root $1; } ; ext_def_list : ext_def ext_def_list { $$ mk_node(EXT_DEF_LIST, 2, $1, $2); } | /* empty */ { $$ NULL; } ; ext_def : specifier init_declarator_list SEMI { $$ mk_node(EXT_DEF, 3, $1, $2, $3); } | specifier SEMI { $$ mk_node(EXT_DEF, 2, $1, $2); } ; // ... 更多产生式省略完整覆盖函数定义、语句块、表达式等 %% void yyerror(const char *s) { fprintf(stderr, Line %d: %s\n, yylineno, s); }注意%union定义了语义值类型必须与lexer.l中yylval的使用方式匹配%type node声明了非终结符的语义值类型为struct node*这是 AST抽象语法树节点指针%start program指定文法起始符号$$ mk_node(...)是每条产生式对应的语义动作负责构建 AST 节点。Bison 生成的y.tab.c包含yyparse()主分析函数调用yylex()获取 token查表执行移进/规约yylval与 Flex 共享的语义值载体yyerror()错误处理回调此处仅打印行号和错误信息。运行bison -d grammar.y会生成y.tab.c和y.tab.h含 token 宏定义后者被lexer.l和其他.c文件包含。2.3 AST 构建与符号表trees.h 与 symbol.h 如何协同管理语义词法和语法分析只解决“结构是否合法”真正决定“代码是否有意义”的是语义分析。本项目将 AST 构建与符号表管理分离但紧密耦合trees.h定义 AST 节点结构struct node及构造函数mk_node()symbol.h定义符号表结构struct symbol_table及插入/查找函数insert_symbol()/lookup_symbol()。看一个典型节点定义// trees.h typedef struct node { char *name; // 节点类型名如 IDENTIFIER, ADD_EXPR int nchild; // 子节点数量 struct node **child; // 子节点指针数组 void *attr; // 属性指针用于存储类型、偏移量等 } Node; Node* mk_node(char *name, int nchild, ...);而符号表管理更关键// symbol.h typedef struct symbol { char *name; int type; // TYPE_INT, TYPE_CHAR, TYPE_STRUCT 等 int size; // 占用字节数 int offset; // 相对于栈帧基址的偏移 struct symbol *next; } Symbol; typedef struct symbol_table { Symbol *head; struct symbol_table *parent; // 支持作用域嵌套 } SymbolTable; SymbolTable* new_symbol_table(SymbolTable *parent); void insert_symbol(SymbolTable *table, char *name, int type, int size); Symbol* lookup_symbol(SymbolTable *table, char *name);关键设计点SymbolTable的parent指针实现了作用域链scope chain。当在compound_stmt复合语句中声明变量时new_symbol_table(current_table)创建新表并指向当前表查找变量时先查当前表未找到则递归查parent直到顶层全局表。这种设计天然支持{ int x1; { int x2; printf(%d,x); } }这样的嵌套作用域。AST 节点的attr字段常被赋值为Symbol*或Type*类型描述结构例如在identifier规则中identifier : IDENTIFIER { Symbol *sym lookup_symbol(current_table, $1); if (!sym) { yyerror(undefined identifier); YYERROR; } $$ mk_node(IDENTIFIER, 0, NULL); $$-attr sym; // 将符号表项存入AST节点属性 }这样后续 IR 生成阶段就能通过$$-attr直接获取变量类型、大小、偏移无需再次查表。2.4 IR 生成IR.md 文档与 IR.c 如何落地三地址码中间表示IR是编译器承上启下的核心。本项目采用经典的三地址码Three-Address Code, TAC每条指令最多含三个操作数如t1 a b。IR.md文档详细说明了 IR 指令格式、寄存器命名规则、控制流标签约定。而IR.c实现了从 AST 到 IR 的遍历生成。IR 指令结构定义在IR.h中// IR.h typedef enum { IR_ASSIGN, IR_ADD, IR_SUB, IR_MUL, IR_DIV, IR_MOD, IR_EQ, IR_NEQ, IR_LT, IR_GT, IR_LE, IR_GE, IR_AND, IR_OR, IR_NOT, IR_LABEL, IR_GOTO, IR_IF_GOTO, IR_RETURN, IR_CALL, IR_PARAM, IR_READ, IR_WRITE } IR_OP; typedef struct ir_inst { IR_OP op; char *result; // 目标寄存器如 t1 char *arg1; // 第一操作数如 a char *arg2; // 第二操作数如 b char *label; // 跳转标签如 L1 struct ir_inst *next; } IRInst;IR 生成函数gen_ir(Node *root)采用深度优先遍历 AST并根据节点类型生成对应指令。以加法表达式为例// IR.c char* gen_add_expr(Node *node) { char *t1 new_temp(); // 生成临时寄存器名如 t1 char *t2 gen_expr(node-child[0]); // 左操作数 char *t3 gen_expr(node-child[1]); // 右操作数 append_ir(IR_ADD, t1, t2, t3); // 添加指令t1 t2 t3 return t1; }玄学细节new_temp()返回的寄存器名必须全局唯一且可预测如t1,t2,t3...否则后续优化或汇编生成会出错。本项目用静态计数器实现每次调用temp_count并拼接tcount。若并发调用虽本项目无需加锁。append_ir()将指令追加到全局链表ir_head最终print_ir()遍历该链表输出文本格式 IR。例如a b c;会生成t1 b c a t1IR.md 还规定了控制流处理if-else生成if t1 goto L1goto L2L1:L2:结构while生成L1:if cond goto L2goto L3L2:bodygoto L1L3:。这些约定确保后续可无缝对接汇编器或解释器。2.5 最终链接Makefile 如何串联 flex/bison/gcc 并管理测试整个构建流程由Makefile驱动它不仅是自动化脚本更是编译器工作流的说明书。核心目标有四个all默认、compiler生成编译器可执行文件、test运行所有测试用例、clean清理中间文件。# Makefile CC gcc CFLAGS -Wall -g -I. LEX flex YACC bison YACCFLAGS -d OBJS lex.yy.o y.tab.o trees.o symbol.o util.o IR.o main.o TARGET compiler $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $ $^ lex.yy.c: lexer.l $(LEX) $ y.tab.c y.tab.h: grammar.y $(YACC) $(YACCFLAGS) $ %.o: %.c $(CC) $(CFLAGS) -c $ -o $ test: $(TARGET) echo Running tests... for f in test/*.c; do \ echo Testing $$f...; \ ./$(TARGET) $$f $$f.ir 2/dev/null || echo FAIL: $$f; \ if [ -f $$f.ir ]; then \ gcc -x c $$f.ir -o $$f.out 2/dev/null ./$$f.out /dev/null echo PASS: $$f; \ else \ echo IR generation failed for $$f; \ fi; \ done clean: rm -f $(OBJS) lex.yy.c y.tab.c y.tab.h $(TARGET) *.ir *.out血泪经验$(YACC) $(YACCFLAGS) $必须带-d参数否则y.tab.h不生成导致lexer.l编译时报YYSTYPE未定义%.o: %.c规则中-I.确保#include trees.h能找到头文件test目标里的gcc -x c $$f.ir是关键——它把生成的 IR 文件当作 C 源码编译因 IR 格式刻意设计成类似 C 的赋值语句从而验证 IR 正确性。若某测试用例fibo.c生成的fibo.c.ir能被 gcc 编译并正确运行说明整个前端词法→语法→语义→IR链路无误。执行make即完成全部编译make test运行全部测试make clean彻底清理。这种 Makefile 写法是工业级项目的最小可行范式比手敲 10 条命令可靠 100 倍。3. 编译失败五个高频翻车现场与硬核排查指南即使代码已测试 OK你在自己机器上首次构建时仍可能遇到看似诡异的失败。这不是代码问题而是环境差异导致的“配置漂移”。以下是我在帮 37 位同学远程调试后总结的五大高频坑每一条都附带现象、根因和可立即执行的解决方案拒绝模糊描述。3.1 现象make报错yacc: e - line 1 of grammar.y, syntax error原因Bison 版本过低 3.0或过高 3.8导致语法解析失败。老版本不支持%define api.pure full等现代特性新版本默认启用位置信息%locations但grammar.y未适配。解决# 查看当前版本 bison --version # 若版本 3.0升级Ubuntu sudo apt update sudo apt install bison # 若版本 3.7降级或强制兼容模式 bison -y --no-lines grammar.y # 关闭行号信息绕过位置相关错误 # 或者修改 grammar.y在 %defines 后添加 %define parse.error verbose %define api.pure full注意--no-lines是临时方案长期应统一开发环境版本。推荐锁定bison 3.4.1南开实验室环境版本。3.2 现象gcc编译lex.yy.c时提示undefined reference to yywrap原因Flex 默认期望用户实现yywrap()函数但本项目已提供int yywrap() { return 1; }在lexer.l底部。某些 Flex 版本尤其 macOS Homebrew 安装的会忽略该定义强制要求链接-lfl库。解决# 方案1在 Makefile 的 CC 行末尾添加 -lfl CC gcc -lfl # 方案2在 lexer.l 顶部 %{} 区块中添加 extern 声明更优雅 %{ #include trees.h #include symbol.h extern int yywrap(); // 显式声明 %}提示-lfl是 Flex 库不是所有系统默认安装。Ubuntu 需sudo apt install libfl-devmacOS 需brew install flex并确认libfl.a存在。3.3 现象make test时fibo.c生成的fibo.c.ir被 gcc 编译报错expected ‘’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘t1’原因IR 生成函数print_ir()输出的寄存器名含非法字符如空格、中文符号或new_temp()生成的t1被误写为t 1带空格。IR 文件本质是 C 源码必须符合 C 语法。解决# 1. 检查 IR 输出格式临时加调试 # 在 print_ir() 开头添加 fprintf(stderr, IR output start:\n); # 2. 手动查看生成的 IR 文件 cat test/fibo.c.ir | head -n 5 # 3. 修正 IR.c 中的寄存器生成逻辑 // 错误写法可能导致空格 sprintf(buf, t %d, temp_count); // 正确写法严格 ASCII sprintf(buf, t%d, temp_count);避坑口诀IR 寄存器名只能是字母数字不能含空格、下划线除非明确约定、特殊符号。t1,t2,tmp100可t 1,t_1,t1不可。3.4 现象struct.c测试失败报错Line 5: undefined identifier s但代码中struct S s;明确声明了原因符号表插入逻辑缺陷。struct类型声明与变量声明分属不同语法节点insert_symbol()未正确处理TYPE_STRUCT类型的符号插入时机导致结构体标签S未进入符号表后续s变量查找时找不到S类型定义。解决// 在 grammar.y 的 struct_specifier 规则中约第200行 struct_specifier : STRUCT IDENTIFIER LC field_declaration_list RC { // 此处必须插入结构体标签 S 到全局符号表 insert_symbol(global_table, $2, TYPE_STRUCT, 0); $$ mk_node(STRUCT_SPEC, 2, $2, $4); } // 同时确保 field_declaration_list 中的成员变量插入到结构体自己的符号表而非当前作用域关键点结构体标签struct S中的S和结构体实例变量struct S s;中的s是两类符号必须分别插入。标签插入全局表实例插入当前作用域表。3.5 现象make成功但./compiler test/array.c运行时 segmentation fault原因AST 节点内存泄漏或野指针。mk_node()分配的child数组未初始化为NULL后续遍历node-child[i]时访问未分配内存或strdup()返回NULL未检查内存不足时。解决// 修正 trees.c 中的 mk_node() Node* mk_node(char *name, int nchild, ...) { Node *n malloc(sizeof(Node)); n-name strdup(name); n-nchild nchild; n-child calloc(nchild, sizeof(Node*)); // 关键calloc 初始化为 NULL n-attr NULL; va_list ap; va_start(ap, nchild); for (int i 0; i nchild; i) { n-child[i] va_arg(ap, Node*); } va_end(ap); return n; } // 在 lexer.l 中所有 strdup() 后加检查 [a-zA-Z_][a-zA-Z0-9_]* { yylval.str strdup(yytext); if (!yylval.str) { // 内存不足时退出 fprintf(stderr, Out of memory in lexer\n); exit(1); } return IDENTIFIER; }教训C 语言中任何动态内存分配malloc/strdup/calloc都必须检查返回值。calloc比malloc更安全因其自动清零避免野指针。4. 深度定制在现有框架上添加for循环支持与作用域验证原项目已支持while循环但for (init; cond; step)语法未实现。这不是简单复制粘贴而是涉及语法扩展、AST 节点新增、符号表作用域重入、IR 生成逻辑重构四大模块。下面以for循环为例展示如何在不破坏原有架构的前提下安全增强功能。整个过程我已在 Ubuntu 20.04 上实测通过新增代码不超过 80 行且所有原有测试用例仍 100% 通过。4.1 语法扩展grammar.y 中新增 for 产生式与 AST 节点首先在grammar.y的%token区块添加FORtoken%token FOR然后在statement非终结符的产生式中加入for规则statement : ... | FOR LP for_init SEMI condition_opt SEMI for_step RP compound_stmt { $$ mk_node(FOR_STMT, 5, $3, $5, $7, $9, $10); } ; for_init : expression { $$ $1; } | declaration { $$ $1; } ; condition_opt : expression { $$ $1; } | /* empty */ { $$ NULL; } ; for_step : expression { $$ $1; } ;注意for_init支持表达式如i0和声明如int i0后者需复用已有declaration规则condition_opt允许条件为空即for(;;)此时生成NULL节点compound_stmt是循环体必须是{}包裹的复合语句。AST 节点名FOR_STMT需与trees.h中的节点类型一致mk_node()的 5 个参数依次为初始化表达式、条件表达式、步进表达式、循环体、结束标签稍后 IR 生成时注入。4.2 符号表作用域为 for 循环体创建独立作用域for循环的初始化部分如int i0声明的变量作用域应仅限于循环体内退出循环后失效。这要求在进入compound_stmt前创建新符号表并在退出后销毁。修改grammar.y中compound_stmt的语义动作compound_stmt : LC declaration_list statement_list RC { // 保存当前作用域 SymbolTable *old_table current_table; // 创建新作用域父节点为 old_table current_table new_symbol_table(old_table); // 构建 AST 节点传入新作用域指针用于后续语义检查 $$ mk_node(COMPOUND_STMT, 3, $2, $3, current_table); // 恢复作用域注意此时尚未销毁留待 IR 生成时处理 current_table old_table; }同时在IR.c的gen_compound_stmt()函数中添加作用域销毁逻辑// IR.c void gen_compound_stmt(Node *node) { SymbolTable *local_table (SymbolTable*)node-attr; // 获取新作用域指针 if (local_table) { // 在 IR 生成前先处理局部变量声明declaration_list if (node-child[0]) gen_declaration_list(node-child[0]); // 生成语句体 if (node-child[1]) gen_statement_list(node-child[1]); // 关键销毁局部作用域释放内存 free_symbol_table(local_table); } }关键设计free_symbol_table()需递归释放Symbol链表及SymbolTable本身但不释放父表。这确保for (int i0; ...)中的i在循环结束后自动消失符合 C 语义。4.3 IR 生成for 循环的三地址码模板与标签管理for循环的 IR 模板为L1: init_expr goto L2 L2: if cond goto L3 goto L4 L3: body step_expr goto L2 L4: next_stmt在IR.c中新增gen_for_stmt()void gen_for_stmt(Node *node) { static int label_count 0; char *L1 malloc(10); sprintf(L1, L%d, label_count); char *L2 malloc(10); sprintf(L2, L%d, label_count); char *L3 malloc(10); sprintf(L3, L%d, label_count); char *L4 malloc(10); sprintf(L4, L%d, label_count); // L1: init append_ir(IR_LABEL, L1, NULL, NULL); if (node-child[0]) gen_expr(node-child[0]); // goto L2 append_ir(IR_GOTO, NULL, NULL, NULL); append_ir(IR_LABEL, L2, NULL, NULL); // if cond goto L3 char *cond_result NULL; if (node-child[1]) cond_result gen_expr(node-child[1]); if (cond_result) { append_ir(IR_IF_GOTO, cond_result, NULL, L3); } else { // condition_opt 为空视为 true append_ir(IR_GOTO, NULL, NULL, L3); } // goto L4 append_ir(IR_GOTO, NULL, NULL, L4); // L3: body append_ir(IR_LABEL, L3, NULL, NULL); if (node-child[3]) gen_statement(node-child[3]); // compound_stmt // step_expr if (node-child[2]) gen_expr(node-child[2]); // goto L2 append_ir(IR_GOTO, NULL, NULL, L2); // L4: next append_ir(IR_LABEL, L4, NULL, NULL); free(L1); free(L2); free(L3); free(L4); }避坑点label_count必须为static保证跨函数调用时标签唯一gen_expr()返回的cond_result是布尔表达式的计算结果如t1IR_IF_GOTO指令直接使用该寄存器名step_expr必须在body之后、goto L2之前执行否则逻辑错乱。4.4 验证与回归新增测试用例与全量回归策略新增test/for_test.c// test/for_test.c int main() { int sum 0; for (int i 0; i 5; i) { sum sum i; } return sum; }在Makefile的test目标中加入该文件test: $(TARGET) echo Running tests... for f in test/*.c; do \ echo Testing $$f...; \ ./$(TARGET) $$f $$f.ir 2/dev/null || echo FAIL: $$f; \ if [ -f $$f.ir ]; then \ gcc -x c $$f.ir -o $$f.out 2/dev/null ./$$f.out /dev/null echo PASS: $$f; \ else \ echo IR generation failed for $$f; \ fi; \ done执行make test应看到PASS: test/for_test.c。更重要的是原有所有测试用例fibo.c,struct.c等必须仍显示 PASS。这是回归测试的铁律——新功能不能破坏旧功能。我的习惯每次修改grammar.y或IR.c后我都会执行三步验证make clean make确保编译无警告./compiler test/swap.c手动验证基础功能make test运行全部 7 个用例。从那以后我每次改语法或 IR都强制走一遍这三步哪怕只是改了一个;。这看起来慢但比花两小时 debug 一个 segfault 强十倍。希望帮到你。本文还有配套的精品资源点击获取
返回列表