恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

北航编译课程设计代码拆解:从类C源码到汇编的完整编译器骨架

  • 首页
  • 资讯中心
  • /
  • 北航编译课程设计代码拆解:从类C源码到汇编的完整编译器骨架

相关资讯

Spring工厂方法循环依赖解析:三级缓存与BeanCurrentlyInCreationException实战排查 2026/10/10 9:35:32
Claude Code生产级代码规范:CLAUDE.md约束体系完全指南 2026/10/10 9:35:32
基于WebGIS的淮河水量水质监测系统Java Web源码解析与实战部署 2026/10/10 9:30:32

最新资讯

CAD学习避坑指南:从安装失败到字体缺失与Python批量改图
Dify离线安装包:断网/内网/高安全场景AI应用部署方案
91行代码撑起创意赛:约束下的编程取舍与贪吃蛇实战
归并排序详解:分治原理、稳定排序特性与工程应用
ASP本地数据库查询工具:Access/SQL Server离线调试方案
2026论文降重工具红黑榜:实测八类方法,避坑与组合打法

今日推荐

Codex 总用英文回答?从 AGENTS.md 到 config.toml 的中文输出调优指南
OpenClaw 自定义插件开发完整指南(2026最新版):从 TypeScript 到 npm 发布
基于Spark的电影推荐系统全链路实战:从爬虫到Web展示

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

北航编译课程设计代码拆解:从类C源码到汇编的完整编译器骨架

发布时间:2026/10/10 9:35:32
北航编译课程设计代码拆解:从类C源码到汇编的完整编译器骨架 简介这是一份面向北航编译技术课程设计的代码与文档合集适合计算机专业本科生或对编译器内部实现感兴趣的开发者作为参考。资源围绕编译器构建的核心流程展开涉及有限自动机驱动的词法分析、递归下降语法分析、中间表示IR构建、寄存器分配以及目标代码生成等模块既包含可直接阅读修改的Java工程源码也附带了实验记录、Markdown笔记与说明文档有助于对照课程要求梳理每个阶段的实现方案。压缩包共2000个文件其中Java源文件1437个、class字节码235个另有Markdown笔记149个、txt文本111个、XML配置47个以及少量Python/C辅助脚本整体体积约10.64MB文件类型覆盖源代码、编译产物和文档结构便于按模块检索。目前已有135人学习下载。通过该资源读者可获得完整课设代码与配套文档重点参考IR构建、MIPS指令生成与寄存器分配等关键代码并借助文档中的设计思路和排错经验为独立完成类似编译器课程设计或扩展研究提供实用支撑。1. 北航编译技术课程设计代码2022.zip能把类 C 源文件变成汇编的最小编译器骨架拿到一个名为“北航编译技术课程设计代码2022.zip”的压缩包不少人的第一反应是解压、找 README然后对着几十个源文件发呆。这门课的大作业核心不是写个能打印 Hello World 的小工具而是把一段类 C 源码依次送过词法分析、语法分析、语义分析、中间代码生成和目标代码生成最终输出一份可以链接运行的汇编文件。对正在做课程设计的学生、接手旧代码的组员以及想从样例代码反推编译器实现的初学者来说这份 zip 是离“完整编译器”最近的脚手架。我按理解、运行、验证三个动作来拆它后面的命令和参数可以直接抄。2. 解压后的第一步认清目录、入口和运行环境课程设计代码包和公司项目最大的差别在于它通常不是按“可维护性”组织的而是按“老师看得见工作量”组织的。我见过把 lexer、parser、codegen 塞进一个 main.cpp 的也见过用 flex/bison 生成一堆 .c 文件后不自量力地全部提交的先保证能编译是正事。所以解压之后不要急着打开编辑器先做三件事看清文件布局、找到主入口、确认构建脚本。三件事做完再花十分钟跑一个最小用例整条链路才算在你手上转起来。2.1 用一条命令看清压缩包的真实结构拿到 zip 后我一般会在终端里走下面这几步把代码放到独立目录避免污染工作区mkdir -p ~/compiler-course cd ~/compiler-course unzip 北航编译技术课程设计代码2022.zip -d src cd src find . -maxdepth 2 -type f | sortunzip -d src是指定解压目录find -maxdepth 2限制只看两层避免被.git或者tests/output下的临时文件淹没。如果解压后中文文件名变成乱码说明这个 zip 是在 Windows 下用默认编码打包的可以先记录后面避坑章再展开。我更愿意在服务器上解压还有一个原因grep 和 find 比图形界面顺手隐藏文件也容易看见图形解压工具常常把.gitignore、.vscode这类文件藏起来而这些文件偶尔会影响目录结构判断。常见的目录布局大概是下面这张表路径常见内容读法src/或根目录.cpp/.h源码、lexer.l、parser.y主逻辑include/公共头文件、AST 节点定义先看这里tests/输入用例、期望输出、回归脚本验证用docs/或report/课程设计报告、测试说明先看需求Makefile或CMakeLists.txt构建脚本决定环境README.md编译和运行说明不一定有如果根目录只有main.cpp和一个Makefile说明代码是单文件结构读起来更省事但扩展的时候要小心全局变量互相纠缠。如果看到lexer.l、parser.y说明语法分析用了 flex/bison这是另一套排错思路——你改的优先级文件会被重新生成 C 代码还得检查生成器版本。先判断结构再决定用 debugger 还是 dump 输出。不要一上来就期待代码规范课程设计代码里经常出现拼音变量名和全大写宏但这不影响读懂主干。2.2 入口文件与命令行参数先跑通一个最小 case找入口文件最直接的方法是在源码里找maingrep -rn int main --include*.cpp --include*.c . # 如果项目是 Java 写的换成查找 public static void main grep -rn public static void main --include*.java .命令行通常是课程设计老师统一的约定从输入文件读出源码输出.s汇编文件。入口参数解析一般不超过四个输入路径、输出路径、优化级别、debug 开关。常见写法像下面这样// 课程设计编译器入口约定用法compiler [options] input.c int main(int argc, char* argv[]) { string srcFile test.c; // 默认输入方便在 IDE 里直接调试 string outFile out.s; // 默认输出汇编文件 int optLevel 0; // 优化级别默认不开优化 bool dumpTokens false; // 是否打印 token 序列 for (int i 1; i argc; i) { string arg argv[i]; if (arg -o i 1 argc) { outFile argv[i]; } else if (arg -O i 1 argc) { optLevel atoi(argv[i]); } else if (arg -dump-tokens) { dumpTokens true; } else if (arg.substr(arg.size() - 2) .c || arg.substr(arg.size() - 5) .mini) { srcFile arg; } } return compile(srcFile, outFile, optLevel, dumpTokens); }这个例子覆盖了大多数课程设计的约定-o指定输出-O0/-O1/-O2指定优化级别-dump-tokens是调试口把输入文件判断写在最后是为了兼容“输入文件是最后一个位置参数”的调用习惯。如果你拿到的代码没有参数解析而是硬编码一个文件路径那就直接改默认字符串先跑通再说。跑最小用例前确认构建脚本。Linux/macOS 下用makeWindows 下用 Visual Studio 的工程文件或者 MinGW 的mingw32-make。如果找不到 Makefile 或者它已经过时手动编译命令一般是g -stdc17 -Iinclude src/*.cpp -o compiler-Iinclude把include/加进头文件搜索路径src/*.cpp把源码一次编完。这里要注意 C 标准如果代码用了 C17 特性而 GCC 默认是 C11会在std::optional、std::filesystem上报错。Makefile 里如果写了-stdc17先确认你的 GCC 版本在 9 以上否则编译后端那部分会翻车。2.3 第一次端到端运行从源码到汇编拿到一个可以编译的程序后我习惯先准备一个最小输入文件例如只放一个返回常量的函数然后再跑编译命令。把输入输出分开放在/tmp这样翻车时不会把锅甩到测试用例上。printf int main(){return 0;}\n /tmp/mini.c ./compiler /tmp/mini.c -o /tmp/mini.s head -30 /tmp/mini.shead -30看汇编头确认生成的是 64 位还是 32 位汇编、有没有冒出来奇怪的.section。如果命令报错先确认输入文件能读、输出目录能写。这一步不追求正确性只追求“编译器完整执行完”。端到端跑通后再回到源码里一个一个模块读如果跑都没跑通后面所有 AST 可视化都是空中楼阁。3. 把词法分析和语法分析拆开先弄懂 token 表和语法树词法分析和语法分析是编译器前半段的主干。与其把整包源码当一个黑匣子我更建议你按一条主线读把它当成一份示例代码讲解先看 token 类型定义再看语法树节点定义最后看 Parser 如何把 token 变成树。这条线走通后面语义分析就只是在这棵树上加属性。3.1 词法分析模块的读法关键字表、最长匹配和状态机词法分析在课程设计里通常有两种实现手写词法分析器或者用 flex 生成。怎么区分看目录里有没有lexer.l、lex.yy.c。有 flex 的关键字表在.l文件里手写的一般在Lexer.cpp或token.h里。两种实现都要重点看“最长匹配”和“注释/字符串”两个边界。给一个手写词法分析核心循环的示意// 词法分析核心循环跳过空白和注释再按开头字符分类 Token Lexer::nextToken() { while (true) { if (isspace(peek())) { advance(); continue; } if (peek() /) { advance(); if (peek() /) { // 行注释 while (peek() ! \n !eof()) advance(); continue; } if (peek() *) { // 块注释 advance(); while (!(peek() * peekNext() /) !eof()) advance(); advance(); advance(); // 消费掉尾部的 */ continue; } return makeToken(Slash); } break; } if (isalpha(peek()) || peek() _) { string id; while (isalnum(peek()) || peek() _) id advance(); return makeToken(checkKeyword(id), id); } if (isdigit(peek())) { string num; while (isdigit(peek())) num advance(); // 只处理十进制定点 return makeToken(Number, num); } // 单字符运算符 char op advance(); switch (op) { case : return makeToken(Plus); case -: return makeToken(Minus); default: throw LexError(unknown char); } }这段代码的逻辑是先处理空白和注释然后按当前字符的类型分派字母或下划线开头走标识符数字开头走整数剩下的单字符运算符直接查符号表。读别人代码时重点看三件事关键字表是否覆盖了课程要求的全部关键字注释和字符串的结束条件是否正确数字按几进制解析。词法层的坑集中在注释结束符、字符串转义和\r回车符上。Windows 换行符进入 Linux 环境时\r会被当成非法字符这是非常典型的踩坑点。如果你看到checkKeyword(id)这个函数它背后通常会有一个查找表// 把标识符和关键字区分开 const unordered_setstring keywords {if, else, while, return, int, void}; Token checkKeyword(const string id) { if (keywords.count(id)) { Token t(Keyword); t.lexeme id; return t; } Token t(Identifier); t.lexeme id; return t; }这里有一个读者容易忽略的点关键字表必须和语法分析器里用到的 token 类型对齐。如果语法分析要求支持for但关键字表里没有forfor会被当作普通标识符解析后面语法分析报错时你根本想不到是词法层先放错了。所以读代码时可以先搜课程设计题目要求支持哪些语法再对着关键字表打勾。3.2 语法分析递归下降还是 yacc/lex错误恢复藏在哪语法分析器决定整套代码的主干。手写递归下降的代码读起来最顺畅每个函数对应一个语法单元函数名基本就是产生式的名字。如果你的包里出现Parser类、parseExpression()这种命名说明是手写递归下降。如果出现parser.y、y.tab.c说明是 Bison 生成。递归下降里优先级是靠函数调用层级解决的一个典型表达式解析函数是这样// 表达式解析先调用 parseTerm表示乘除优先级更高 Node* Parser::parseExpr() { Node* node parseTerm(); // 先吃掉乘除 while (peekToken() Plus || peekToken() Minus) { Token op nextToken(); Node* right parseTerm(); node new BinaryOpNode(op, node, right); // 左结合 } return node; } Node* Parser::parseTerm() { Node* node parseFactor(); while (peekToken() Star || peekToken() Slash) { Token op nextToken(); Node* right parseFactor(); node new BinaryOpNode(op, node, right); } return node; }这里的parseExpr只处理加减parseTerm处理乘除先调用parseTerm()表示乘除优先级更高。读代码时顺着parseExpr - parseTerm - parseFactor这条线走就能看到完整的优先级和结合性。如果代码里出现大量if (token x) consume(); else throw说明错误恢复策略是“遇到错误立即抛异常”。更好的课程设计会在错误点插入“同步 token”比如分号或右花括号然后继续解析。你可以在error函数里看到这些同步 token 列表。我一般会先看这个错误恢复函数因为 Parser 的翻车通常不是在正确路径上而是在错误路径上。如果错误信息只有parse error后面调试语义分析时很痛苦。测试时故意漏一个分号看编译器会不会给出“第几行”而不是一个退出码这也是验收时会考的。从选型上说课程设计我更推荐手写递归下降flex/bison 需要额外安装和生成步骤带进评测环境容易翻车生成的 C 代码也不好读递归下降体积小、错误信息友好在报告里也能写清楚“我如何表达优先级”。3.3 从最小用例反推 AST先找 BinaryOpNode 和 FuncDeclNode读完词法再看一眼 AST 节点定义。找到BinaryOpNode、NumberNode、FuncDeclNode这几种节点你就知道语法分析要在哪一步结束。用12*3;做一个最小用例跟着 Parser 走一遍比背半天产生式更有效。如果代码里有dumpAST或者toString方法优先用它们输出一棵树然后对比你自己的推导。这步做完语法分析这块就过关了。4. 语义分析、中间代码和目标代码从语法树到可执行文件的三个关口语法树只是骨架语义分析负责给它加血肉。课程设计里这一阶段往往被分成两个文件SemanticAnalyzer.cpp和CodeGen.cpp。前者管作用域、类型、return 路径后者负责把树翻译成三地址码或汇编。读这一章的时候不要一上来就钻汇编细节先找“这个编译器自己定义的中间表示”。4.1 符号表与类型检查先看作用域嵌套和 return 路径符号表在课程设计中可能叫SymbolTable、Scope或Table。常见结构是一个栈进入块作用域时 push 新层退出时 pop。读代码时注意三个必查项变量是否先声明后使用同一作用域下是否重复声明函数返回值类型是否一致。// 符号表条目变量和函数统一用一条记录 struct Symbol { string name; // 变量名/函数名 string type; // int / void / pointer bool isFunction; // 区分变量和函数 int paramCount; // 函数参数个数变量为 0 int blockDepth; // 所在作用域深度用于查重和释放 }; class SymbolStack { vectorunordered_mapstring, Symbol scopes; int depth() const { return scopes.size(); } void push() { scopes.push_back({}); } void pop() { scopes.pop_back(); } bool insert(const Symbol s) { auto cur scopes.back(); if (cur.count(s.name)) return false; // 重复声明 cur[s.name] s; return true; } const Symbol* find(const string name) const { for (int i (int)scopes.size() - 1; i 0; --i) { auto it scopes[i].find(name); if (it ! scopes[i].end()) return it-second; } return nullptr; } };这个find从当前层向上逐层找对应“内层变量遮蔽外层变量”的语义。读代码时要确认push/pop是否和{}配对很多翻车点是作用域栈没有在 return 语句或异常路径上 pop导致后面的声明被误判成重名。如果课程设计还要求布尔短路那类型检查还要处理非零即真这类 C 语言惯例。类型检查还有一个隐蔽问题隐式转换。只支持int的编译器不会遇到但一旦加入char或pointer就必须定义“赋值兼容”的规则。课程设计里常见做法是只在赋值和函数调用参数上做类型检查在算术表达式里不做完整推导因为推导要写类型格。4.2 中间代码生成与汇编输出三地址码是救命稻草有些课程设计要求先生成三地址码有些直接生成 X86 汇编。前者调试起来舒服很多因为你可以先确认指令序列对不对再怀疑汇编模板。如果你看到Temp、Label、Quad这样的类基本就是三地址码。示例输出长这样L0: t0 10 t1 a t2 t0 t1 return t2在代码里如何打开这类输出常有一个-emit-llvm、-dump-ir或-d参数。读后端代码时先找“指令选择”函数。如果只有push/pop模板而没有寄存器分配函数说明用的是最简单的“每个临时变量对应一个栈槽位”这对课程设计完全够用。不要在这里纠结优化先把功能跑对。给一个汇编生成模板的示意// 将三地址码加法 t2 t0 t1 转成 x86 汇编 void CodeGen::emitAdd(const Quad q) { // 假定临时变量已经映射到 32 位寄存器或栈槽 fprintf(out, movl %s, %%eax\n, tempName(q.arg1)); fprintf(out, addl %s, %%eax\n, tempName(q.arg2)); fprintf(out, movl %%eax, %s\n, tempName(q.result)); }这种写法的优点是每一条中间代码都能在汇编里找到对应行出了错可以直接从t2 t0 t1反查是寄存器分配的问题还是模板的问题。如果要支持复杂的表达式可以用栈式机器push/pop避免处理寄存器分配代价是生成的汇编更长但正确性更容易保证。中间代码为什么是后悔药因为它是前端和后端的唯一契约。如果生成汇编结果不对先 dump 三地址码看错误是前端的 IR 错了还是后端的翻译错了。没有这层 IR排错就只能靠调试器单步两条路。课程设计代码里如果没有 IR我会在 Parser 之后加一个-dump-ast先用文本形式确认 AST再改后端这样至少不会把两个问题叠加在一起。指令选择和寄存器分配是后端最容易翻车的地方。课程设计的常见要求是“能生成汇编并正确执行”所以很多样例代码会给每个临时变量分配一个内存地址而不是做真正的寄存器分配。你读到的代码如果有alloc之类函数并且每次临时变量都调用它说明用的是栈槽方案。这种方案在 32 位汇编下很好写但要注意栈对齐否则运行时会出现莫名其妙的数据错乱。5. 跑通测试用例的 3 个必调参数与 5 条避坑记录当你可以编译出一个compiler可执行文件后接着就要面对所有课程设计代码最常出问题的环节跑测试。这一章把三个必调参数和五条避坑记录写在一起照做能少走不少弯路。5.1 第一个必调参数输入输出文件路径与编码很多课程设计代码默认把输入文件写死在主函数里比如test.c你在项目根目录跑没问题换个目录就报No such file。改法有两个改动主函数里的默认路径或者调用时用绝对路径。更重要的是中文路径和空格编译器里的文件读取一般用 C 的fopenWindows 下路径里的中文和空格经常导致打开失败。我一般会把测试用例全放到tests/cases这种纯 ASCII 路径下避免在词法分析之前就倒在文件系统层。如果程序里有-dump-tokens参数先在最小输入上跑一遍确认 token 序列能正常输出。这一步能同时验证路径解析和词法层都通了。5.2 第二个必调参数链接运行时库的方式编译器生成的.s文件并不能直接变成可执行文件。课程设计里通常会提供一个runtime.c或lib.c里面实现了print_int、read_int之类供程序调用的辅助函数。生成汇编后需要把这个运行时库一起编译链接gcc out.s runtime.c -o out如果链接时报undefined reference to printf或main先检查生成汇编里的调用名是不是和你 runtime.c 里写的一致。很多课程设计会在代码生成里硬编码调用_printf而 C 库实际符号是printf。这时候要么改生成模板要么在 runtime.c 里加一个同名的包装函数。不要直接去禁用优化问题往往在符号名。5.3 第三个必调参数优化开关与 dump 开关课程设计一般要求至少支持-O0有些代码也实现了-O1或简单常数折叠。调试阶段保持-O0最重要因为一旦开优化临时变量可能被合并你在 dump 里看到的 IR 和实际汇编对不上。-dump-tokens、-dump-ast、-dump-ir是三个不同层级的 dump 开关建议全部打开并输出到文件不要只打到 stdout。因为编译器前端报错时stdout 和 stderr 混在一起拿到文件里好定位。5.4 五条避坑记录从这套代码里学到的血泪教训第一条解压后文件名乱码。现象zip 里中文目录变成麦之类原因Windows 默认用 GBK 编码文件名Linux/macOS 用 UTF-8解决在 Linux 下用unzip -O gbk 北航编译技术课程设计代码2022.zip或者用 Python 的zipfile做一次转码重命名。不要手动一个个改名文件多了会疯。第二条make 编译失败头文件找不到。现象fatal error: ast.h: No such file or directory原因代码用#include include/ast.h或者根本没加-Iinclude解决看 Makefile 的 CFLAGS手动补-Iinclude。如果代码是从别处粘的可能还缺-stdc17一并加上。第三条一跑就段错误。现象输入一个合法测试用例编译器直接Segmentation fault原因AST 节点构造时子节点指针没有初始化或者符号表scopes.back()在空栈上调用解决用 gdb 跑到段错误处看调用栈在哪个构造函数把所有成员放到初始化列表里。也可以开 AddressSanitizerg -fsanitizeaddress -g重新编译它会把野指针定位到具体行。第四条汇编链接时找不到printf。现象gcc out.s -o out报undefined reference to printf原因生成代码里输出的是_printf或printfPLT而运行库环境不匹配解决检查汇编模板的符号名统一改成printf并在生成文件头部声明.global main。注意 32 位汇编下符号名可能带下划线64 位则不带。第五条测试用例全部超时。现象一个 10 行的输入程序能让编译器卡死原因词法分析或语法分析进入了死循环通常是advance()没有真正消费字符或者注释匹配*/的循环在 EOF 处停了下来解决在关键循环里加一个消费计数器限定最多等于输入长度加一或者在读字符时把peek和advance配对检查尤其注意while (!(peek() * peekNext() /) !eof())这种写法在 EOF 时会先peekNext()导致越界。这五条里前两条是环境问题后三条是代码本身的问题。按照“先环境后源码”的顺序排查大部分卡点都在编译阶段而不是运行阶段。6. 验证正确性给编译器加一棵能可视化的语法树6.1 加一个 -dump-ast 参数让语法树变成文本在 Parser 里加一个 AST 输出函数比猜结构靠谱void dumpAST(Node* n, int depth) { for (int i 0; i depth; i) cerr ; cerr nodeTypeName(n-type); if (!n-lexeme.empty()) cerr ( n-lexeme ); cerr \n; for (auto child : n-children) dumpAST(child, depth 1); }depth把嵌套层级显示成缩进lexeme是节点对应的原文这样一眼能看到运算符优先级被 parser 建成了什么样。判断优先级对不对看12*3的树是( 1 (* 2 3))还是(* ( 1 2) 3)就行。6.2 用一份回归脚本把全部测试用例拉起来跑临时目录里准备tests/cases和tests/expect然后跑一个简单脚本对比返回值或者输出文件#!/usr/bin/env python3 import glob, subprocess, os for c in sorted(glob.glob(tests/cases/*.c)): res subprocess.run([./compiler, c, -o, /tmp/t.s], capture_outputTrue) if res.returncode ! 0: print(fFAIL compile {c}: rc{res.returncode}) continue res2 subprocess.run([gcc, /tmp/t.s, -o, /tmp/t], capture_outputTrue) if res2.returncode ! 0: print(fFAIL link {c}: {res2.stderr.decode()}) continue r subprocess.run([/tmp/t], capture_outputTrue) print(f{os.path.basename(c)}: exit{r.returncode})这里编译器和 gcc 的返回值都被检查任一步失败都会打印对应用例名比跑一百个用例再回头找哪个挂了要快得多。我自己的习惯是拿到任何编译课程设计代码第一件事不是改功能而是把测试用例脚本先搭好没有回归脚本你改了后面就会毁掉前面。有一次我把的右操作数求值顺序改错了单独测试看不出来回归脚本跑完才发现第三个用例挂了。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号