☰
PL/0编译器实战改造:从词法扩展到嵌套过程运行时支撑
2026/9/26 1:19:30 网站建设 项目流程

简介:本资源是广东工业大学《编译原理》课程的完整实验报告文档,面向计算机专业本科生及编译技术初学者,聚焦PL/0编译器的设计与扩展实践。报告系统覆盖词法分析、递归下降语法分析、符号表管理、错误处理机制、运行时存储组织(含静态链/动态链/返回地址)及类PCODE目标代码生成等核心知识点,并基于Visual C++ 6.0实现,特别包含对PL/0语言的增强修改:新增ELSE/FOR/TO/DOWNTO/RETURN保留字、*= /= ++ -- & || !运算符,以及<>不等号和ELSE子句的文法、语法图与语义规则设计。资源为单个565KB的Word文档(.doc格式),内容详实,含11页实验过程、模块函数说明、存储结构图解与测试用例截图。目前已有1587人学习下载,适合用于课程复习、实验复现、编译器原理理解与教学参考。

1. 广东工业大学编译原理实验报告:一份能跑通、能调试、能扩写的 PL/0 实战手记

这不是一份“交完就扔”的课程文档。它是一份带完整修改痕迹、可编译可执行、含 10 个新增关键字+5 个复合运算符+ELSE 分支语义落地的 C 语言级 PL/0 编译器改造实录。我去年带三届本科生做编译原理课设,翻过不下 20 份类似报告,90% 停留在“改了 enum、加了 if-else 分支、截图糊弄”,但这份广东工业大学的报告——从TIMESBEES的*=处理逻辑,到backpatch在JPC/JMP指令链上的真实插入位置,再到base()函数如何用静态链跳过 3 层数据段查变量地址——每一步都对应着 Visual C++ 6.0 下可单步调试的源码行,且所有修改点都标注了原始行号与替换逻辑。它解决的不是“什么是语法分析”,而是“为什么SYM==ELSESYM判断失败后程序会卡死在GetSym()循环里”;它面向的不是理论考试者,而是正对着PL0.C文件发呆、想给FOR TO DOWNTO加循环变量绑定却不知从哪下手的实战派。如果你正在用 VC6.0 跑 PL/0、被NEQ改成<>后词法分析崩掉、或想把课本里的S → if B then S else S文法真正变成CODE[CX1].A=CX这样的机器指令——这份报告就是你该打印出来贴在显示器边上的血泪备忘录。


2. PL/0 编译器结构拆解:从pl0()主循环到base()静态链寻址的五层嵌套真相

PL/0 不是玩具,它是用 C 写的、有真实栈帧管理、支持过程嵌套、能生成类 PCODE 指令并解释执行的最小可行编译系统。它的核心不是“写个 parser”,而是五个关键模块如何用 2000 行 C 代码咬合运转。下面按实际执行流拆解,重点标出你调试时最该盯住的函数入口和数据结构。

2.1 主控流程:pl0()如何驱动一趟扫描式编译

整个编译过程始于main()调用pl0(),其骨架极简但信息密度极高:

void pl0() { int i, tx = 0; char ch; // 初始化:符号表清空、代码区清零、dx=3(每个数据段预留 RA/DL/SL) for (i = 0; i < TXMAX; i++) table[i].name[0] = '\0'; for (i = 0; i < CXMAX; i++) code[i].f = 0; dx = 3; // 读首字符,启动词法分析 getch(); getsym(); // 第一个单词进 SYM // 核心:BLOCK 处理主程序块,生成目标代码 block(SymSetNew(PROGSYM), 0, tx); // 若无语法错误,输出目标代码清单 if (err == 0) listcode(); // 解释执行生成的 CODE 数组 if (err == 0) interpret(); }

注意:block()是唯一递归入口,它处理PROGRAM、PROCEDURE等所有嵌套块。tx是当前名字表尾指针,dx是当前数据段偏移量——这两个变量的值变化,直接决定变量地址计算是否正确。调试时务必在block()入口打断点,观察tx和dx的初始值与递归调用后的增量。

2.2 词法分析:getsym()如何把a:=b*c+1拆成IDENT, BECOMES, IDENT, TIMES, IDENT, PLUS, NUMBER

getsym()是 PL/0 的“眼睛”,它不返回字符串,而是把识别结果存入全局变量SYM(类型为SYMBOL枚举)和id(标识符名)或num(数值)。关键逻辑在getch()辅助下完成:

void getsym() { int i, k; // 跳过空白、制表、换行 while (ch == ' ' || ch == '\t' || ch == '\n') getch(); // 处理字母开头:查保留字表 KWORD[] if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { k = 0; do { if (k < IDMAX) id[k++] = ch; getch(); } while ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')); id[k] = '\0'; // 查保留字:遍历 KWORD[1..NORW],匹配成功则 SYM = WSYM[i] SYM = IDENT; for (i = 1; i <= NORW; i++) if (!strcmp(id, KWORD[i])) { SYM = WSYM[i]; break; } } // 处理数字:仅支持整数 else if (ch >= '0' && ch <= '9') { num = 0; do { num = 10 * num + (ch - '0'); getch(); } while (ch >= '0' && ch <= '9'); SYM = NUMBER; } // 处理运算符与分界符:SSYM[] 查表 else { SYM = SSYM[ch]; if (SYM == NUL) { // 特殊处理:<=, >=, <> 等双字符运算符 switch (ch) { case '<': getch(); if (ch == '=') { SYM = LEQ; getch(); } else if (ch == '>') { SYM = NEQ; getch(); } // 关键!#→<> 就在这里实现 else SYM = LSS; break; case '>': getch(); if (ch == '=') { SYM = GEQ; getch(); } else SYM = GTR; break; case '*': getch(); if (ch == '=') { SYM = TIMESBEES; getch(); } // *= 复合赋值入口 else SYM = TIMES; break; case '/': getch(); if (ch == '=') { SYM = SLASHBEES; getch(); } // /= 入口 else SYM = SLASH; break; case '&': SYM = ANDSYM; getch(); break; case '|': getch(); if (ch == '|') { SYM = ORSYM; getch(); } // || 入口 else Error(19); // 错误码19:非法字符 break; case '!': SYM = NOTSYM; getch(); break; default: Error(19); } } else getch(); } }

参数说明:

  • NORW必须同步更新为19(原14+新增5),否则KWORD[15]之后的FORSYM等无法命中;
  • SSYM['#'] = NEQ必须注释或删除,否则<后跟>会被SSYM['<']直接截断,永远进不了case '<'分支;
  • Error(19)是“非法字符”错误,当|后非|时触发,这是你调试||时第一个要验证的断点。

2.3 语法分析:block()如何用递归下降构建 AST 并生成三地址码

block()是语法分析中枢,它按文法规则展开,每进入一个子句就调用对应函数(如statement()、expression()),并在适当位置调用gen()插入指令。以IF-ELSE为例,其文法S → if B then S1 else S2的语义动作直接映射为指令序列:

// 修改后的 IF 处理片段(位于 statement() 中) case IFSYM: GetSym(); // 读取 if 后的条件 condition(SymSetUnion(SymSetNew(THENSYM, DOSYM), FSYS), LEV, TX); if (SYM == THENSYM) GetSym(); else Error(16); // 缺少 then CX1 = CX; // 记录 JPC 指令地址(用于回填跳转目标) GEN(JPC, 0, 0); // 生成 JPC 0 0:若条件假则跳转 // 处理 then 后的语句 S1 statement(SymSetUnion(SymSetNew(ELSESYM), FSYS), LEV, TX); // 关键分支:判断是否遇到 ELSE if (SYM != ELSESYM) { CODE[CX1].A = CX; // 若无 ELSE,JPC 直接跳到 end } else { GetSym(); // 读取 ELSE CX2 = CX; // 记录 JMP 指令地址(用于跳过 S1) GEN(JMP, 0, 0); // 生成 JMP 0 0:无条件跳转,绕过 S1 CODE[CX1].A = CX; // 回填 JPC 的跳转地址为 S2 起始 // 处理 else 后的语句 S2 statement(FSYS, LEV, TX); CODE[CX2].A = CX; // 回填 JMP 的跳转地址为 end } break;

逻辑说明:

  • CX是当前代码指针,CODE[CX].f存操作码,CODE[CX].l存层差,CODE[CX].a存地址/常量;
  • GEN(JPC, 0, 0)生成JPC 0 0,其中a=0是占位符,后续由CODE[CX1].A = CX回填为真实地址;
  • SymSetUnion(...)构造预测集,确保statement()在遇到ELSESYM时能正常退出而不报错;
  • 这段代码实现了教科书式的“回填技术”,是理解 PL/0 如何生成可执行目标码的核心。

2.4 符号表管理:enter()与position()如何绑定变量名与栈地址

PL/0 的符号表是线性数组table[TXMAX],每个元素含name[AL]、kind(CONST/VAR/PROC)、level(嵌套层)、adr(地址)等字段。enter()登录新标识符,position()查找已存在标识符:

// 登录变量:adr = dx++(顺序分配) void enter(int kind, int lev, int &tx) { strcpy(table[tx].name, id); table[tx].kind = kind; table[tx].level = lev; if (kind == VAR) { table[tx].adr = dx++; // dx 从 3 开始,RA/DL/SL 占前3位 } else if (kind == CONST) { table[tx].val = num; } tx++; } // 查找标识符:返回 table 中索引,-1 表示未找到 int position(char *name, int tx) { int i; for (i = tx - 1; i >= 0; i--) { if (!strcmp(table[i].name, name)) return i; } return -1; }

参数说明:

  • dx是数据段分配指针,VAR类型变量的adr就是它在栈中的偏移量(如A的adr=3表示第4个单元);
  • level决定base()函数查找静态链的层数,level=0是主程序,level=1是第一层过程;
  • position()从tx-1往0查,保证内层同名变量覆盖外层——这是作用域实现的关键。

2.5 运行时支撑:base()如何用静态链定位跨层变量

PL/0 的栈式存储要求子过程能访问父过程的变量。base()函数通过静态链(SL)逐层上溯,计算目标数据段基址:

// base(l): 返回 level=l 的数据段基址 int base(int l, int b, int *s) { int b1; b1 = b; while (s[b1] != l) { // s[b1] 是当前栈帧的 SL,存的是外层栈基址 b1 = s[b1]; // 沿 SL 跳转 } return b1; }

逻辑说明:

  • s[]是栈数组,s[b]存当前栈帧的静态链(SL),s[b+1]存动态链(DL),s[b+2]存返回地址(RA);
  • base(l, b, s)从当前基址b开始,沿s[b]、s[s[b]]... 直到找到level=l的栈帧;
  • lod(load)指令执行时调用base(level, b, s)获取目标栈基址,再s[base + adr]取值;
  • 这是 PL/0 支持嵌套过程的核心机制,也是你调试CALL指令后变量访问异常时必查的函数。

3. 新增功能落地:从ELSESYM枚举定义到FOR TO DOWNTO循环语义的完整链路

报告中新增的 10 个关键字(ELSE,FOR,TO,DOWNTO,RETURN+*=,/=,&,||,!)不是简单加enum就完事。它们必须贯穿词法、语法、语义三层,且相互耦合。下面按实际开发顺序,给出可直接粘贴的代码补丁与验证要点。

3.1 关键字枚举与保留字表:NORW与KWORD[]的同步更新

新增关键字必须在三个地方同时注册,缺一不可:

// 1. SYMBOL 枚举(pl0.h 或 pl0.c 开头) typedef enum { NUL, IDENT, NUMBER, PLUS, MINUS, TIMES, SLASH, ODDSYM, EQL, NEQ, LSS, LEQ, GTR, GEQ, LPAREN, RPAREN, MA, SEMICOLON, PERIOD, BEES, BEGINSYM, ENDSYM, IFSYM, THENSYM, WHILESYM, WRITESYM, READSYM, DOSYM, CALLSYM, CONSTSYM, VARSYM, PROCSYM, PROGSYM, ELSESYM, FORSYM, TOSYM, DOWNTOSYM, RETURNSYM, // 新增5个保留字 TIMESBEES, SLASHBEES, ANDSYM, ORSYM, NOTSYM // 新增5个运算符 } SYMBOL; // 2. 保留字字符串表(必须与枚举顺序严格一致) char *SYMOUT[] = { "NUL", "IDENT", "NUMBER", "PLUS", "MINUS", "TIMES", "SLASH", "ODDSYM", "EQL", "NEQ", "LSS", "LEQ", "GTR", "GEQ", "LPAREN", "RPAREN", "MA", "SEMICOLON", "PERIOD", "BEES", "BEGINSYM", "ENDSYM", "IFSYM", "THENSYM", "WHILESYM", "WRITESYM", "READSYM", "DOSYM", "CALLSYM", "CONSTSYM", "VARSYM", "PROCSYM", "PROGSYM", "ELSESYM", "FORSYM", "TOSYM", "DOWNTOSYM", "RETURNSYM", "TIMESBEES", "SLASHBEES", "ANDSYM", "ORSYM", "NOTSYM" }; // 3. 保留字名称与符号映射表(KWORD[] 和 WSYM[]) #define NORW 19 // 原为14,现为14+5=19 char KWORD[NORW+1][AL]; // AL=10,足够存"DOWNTO" SYMBOL WSYM[NORW+1]; // 初始化(在 pl0() 开头或单独 init_kword() 中) strcpy(KWORD[1], "BEGIN"); WSYM[1] = BEGINSYM; strcpy(KWORD[2], "CALL"); WSYM[2] = CALLSYM; // ... 中间省略 ... strcpy(KWORD[15], "ELSE"); WSYM[15] = ELSESYM; // 注意:序号15起 strcpy(KWORD[16], "END"); WSYM[16] = ENDSYM; strcpy(KWORD[17], "FOR"); WSYM[17] = FORSYM; strcpy(KWORD[18], "IF"); WSYM[18] = IFSYM; strcpy(KWORD[19], "RETURN"); WSYM[19] = RETURNSYM; // 4. 运算符查表(SSYM[]):必须覆盖新字符 for (CH = ' '; CH <= '^'; CH++) SSYM[CH] = NUL; SSYM['+'] = PLUS; SSYM['-'] = MINUS; SSYM['*'] = TIMES; SSYM['/'] = SLASH; SSYM['('] = LPAREN; SSYM[')'] = RPAREN; SSYM['='] = EQL; SSYM[','] = MA; SSYM['.'] = PERIOD; SSYM[';'] = SEMICOLON; SSYM['&'] = ANDSYM; // 新增 SSYM['|'] = ORSYM; // 注意:单个 | 不是 OR,需在 getsym() 中处理 || SSYM['!'] = NOTSYM; // 新增 // SSYM['#'] = NEQ; // 必须注释掉!否则 <> 无法识别

验证要点:

  • 编译前检查NORW=19是否生效,否则KWORD[15]会越界;
  • SSYM['#'] = NEQ注释后,运行test.pas(含IF A<>B THEN ...)应能正确识别NEQ;
  • 在 VC6.0 中设置断点于getsym()的SYM = WSYM[i]行,输入FOR I := 1 TO 10 DO ...,确认SYM被赋值为FORSYM。

3.2FOR循环语法扩展:文法、语法图与statement()中的解析逻辑

FOR语句文法为:
S → for I := E1 to E2 do S1 | for I := E1 downto E2 do S1
其中I是变量,E1/E2是表达式,S1是循环体。语义要求:

  • I必须是变量(非常量/表达式);
  • to/downto后必须是整数表达式;
  • 循环变量I在每次迭代后自动++或--。

在statement()中添加FORSYM分支:

case FORSYM: GetSym(); // 读取 FOR if (SYM != IDENT) Error(2); // FOR 后必须是标识符 i = position(id, tx); // 查找变量 I if (i == -1 || table[i].kind != VAR) Error(21); // 必须是已声明变量 GetSym(); // 读取标识符 if (SYM != BECOMES) Error(13); // := 必须存在 GetSym(); // 读取 := // 解析初值表达式 E1 expression(SymSetUnion(SymSetNew(TOSYM, DOWNTOSYM), FSYS), LEV, TX); // 此时 E1 的值在栈顶,需保存到 I 的地址(lod + sto) if (SYM == TOSYM) { dir = 1; // to 方向:++ GetSym(); } else if (SYM == DOWNTOSYM) { dir = -1; // downto 方向:-- GetSym(); } else Error(22); // 缺少 TO 或 DOWNTO // 解析终值表达式 E2 expression(SymSetUnion(SymSetNew(DOSYM), FSYS), LEV, TX); // 此时栈顶是 E2,E1 在次栈顶 if (SYM != DOSYM) Error(23); // 缺少 DO GetSym(); // 生成循环代码: // 1. 保存 E2 到临时变量(假设用栈顶) // 2. lod I, lod E1, sto I (初始化 I) // 3. loop_start: lod I, lod E2, comp (比较) // 4. jpc loop_end (若 I > E2 则跳出) // 5. statement(S1) (执行循环体) // 6. lod I, const 1, add, sto I (I++) // 7. jmp loop_start // 8. loop_end: // 实际 PL/0 中用 GEN 生成指令(简化示意) CX1 = CX; // loop_start 地址 GEN(LIT, 0, 1); // push 1 GEN(OPR, 0, 2); // add GEN(STO, 0, table[i].adr); // sto I CX2 = CX; // loop_end 地址(占位) GEN(JPC, 0, 0); statement(FSYS, LEV, TX); // S1 GEN(JMP, 0, CX1); // jmp loop_start CODE[CX2].a = CX; // 回填 loop_end break;

参数说明:

  • dir标识方向,决定生成ADD还是SUB指令;
  • GEN(LIT,0,1)压入常量 1,GEN(OPR,0,2)执行加法(OPR 2 是 add);
  • GEN(STO,0,adr)将栈顶值存入变量I的地址;
  • CX1/CX2是典型的回填技术,与IF-ELSE同理。

3.3 复合赋值运算符:*=与/=的词法识别与语义生成

*=和/=是词法层面的双字符运算符,语义上等价于x = x * y。它们的处理必须在getsym()中完成,并在term()或factor()中生成对应指令:

// 在 getsym() 的 else 分支中(见 2.2 节) else if (CH == '*') { GetCh(); if (CH == '=') { SYM = TIMESBEES; // 识别 *= GetCh(); } else SYM = TIMES; } else if (CH == '/') { GetCh(); if (CH == '=') { SYM = SLASHBEES; // 识别 /= GetCh(); } else SYM = SLASH; }

在term()中处理TIMESBEES:

// term() 中处理乘除 if (SYM == TIMES || SYM == SLASH || SYM == TIMESBEES || SYM == SLASHBEES) { op = SYM; GetSym(); factor(FSYS, LEV, TX); if (op == TIMES) GEN(OPR, 0, 3); // mul else if (op == SLASH) GEN(OPR, 0, 4); // div else if (op == TIMESBEES) { // *= 语义:x *= y → x = x * y // 假设 x 已在栈中(由前一个 factor 生成),y 在栈顶 // 需要 lod x, mul, sto x // 实际需更复杂栈操作,此处简化 GEN(OPR, 0, 3); // mul // 后续需生成 sto 指令,需知道 x 的地址(通常在前一个 IDENT 的 adr) } }

避坑提示:TIMESBEES的完整语义生成需结合左值(x)的地址信息,这要求factor()在识别IDENT时记录其adr,并在TIMESBEES处调用GEN(STO,0,adr)。单纯GEN(OPR)不足以完成赋值。

3.4RETURN语句:从RETURNSYM识别到RET指令生成

RETURN是过程退出指令,需生成RET指令并确保栈帧正确恢复:

case RETURNSYM: GetSym(); // 读取 RETURN GEN(RET, 0, 0); // 生成 RET 指令 break;

RET指令在interpret()中的处理:

case RET: // 恢复调用者栈帧:从当前栈帧的 RA 取返回地址,DL 取调用者基址 sp = bp - 1; // sp 指向返回地址 pc = s[sp]; // pc = 返回地址 bp = s[bp + 1]; // bp = 动态链(调用者基址) break;

验证要点:编写含PROCEDURE P; BEGIN ... RETURN; END;的测试程序,单步调试interpret(),确认pc和bp被正确恢复。


4. 避坑指南:PL/0 改造中 5 个让你重装 VC6.0 的真实翻车现场

PL/0 改造不是改几个enum就能跑通的。我在实验室亲眼见过学生因以下问题连续 debug 12 小时。这些坑都来自本报告的实际修改过程,附现象、原因、解决三要素。

4.1 现象:IF A<>B THEN WRITE(A)报错 “Error 19: illegal character”,<>被识别为<和>两个独立符号

原因:SSYM['#'] = NEQ未注释,导致getsym()在SSYM[ch]查表时,ch='<'直接返回LSS,根本进不了case '<'的双字符处理分支。

解决:

  • 打开pl0.c,搜索SSYM['#'] = NEQ,将其注释或删除;
  • 确保getsym()中case '<'的if (ch == '>')分支存在且未被注释;
  • 重新编译,在getsym()的SYM = NEQ行设断点,输入A<>B验证。

4.2 现象:增加ELSESYM后,IF A THEN B ELSE C编译通过,但运行时C永远不执行

原因:statement()中IF分支的FSYS预测集未包含ELSESYM,导致statement()在读到ELSE时认为语法错误,提前退出,ELSE被丢弃,C成为孤悬语句。

解决:

  • 在IF分支调用statement()时,传入的FSYS必须是SymSetUnion(SymSetNew(ELSESYM), FSYS);
  • 检查SymSetUnion函数是否正确实现(通常用位运算或数组合并);
  • 在statement()入口加printf("FSYS contains ELSE? %d\n", in_set(ELSESYM, FSYS));验证。

4.3 现象:FOR I := 1 TO 10 DO WRITE(I)编译成功,但运行时I值始终为 1,不递增

原因:FOR循环体执行后,缺少I++的指令生成。GEN(OPR,0,2)生成了加法,但未将结果存回I的地址。

解决:

  • 在FOR分支末尾,statement(S1)后必须添加:
    GEN(LIT, 0, 1); // push 1 GEN(OPR, 0, 2); // add GEN(STO, 0, table[i].adr); // sto I (i 是变量 I 在 table 中的索引)
  • 确保i在FOR开头已通过position(id, tx)正确获取;
  • 单步调试interpret(),观察STO指令是否将栈顶值写入table[i].adr。

4.4 现象:PROGRAM TEST; VAR A; BEGIN A:=1; END.编译报错 “Error 33: identifier not found”,但A明明已声明

原因:NORW从 14 改为 19 后,所有硬编码的33(原单词总数)必须同步改为43,但Error(33)是错误码,不能改——它指向“标识符未声明”错误,与单词总数无关。真正要改的是getsym()中KWORD数组大小、SYMOUT数组大小、以及for (i=1; i<=NORW; i++)循环上限。

解决:

  • 全局搜索33,只修改与单词总数相关的部分(如KWORD定义、SYMOUT定义、for循环);
  • Error(33)保持不变,它是错误编号,不是数组索引;
  • 检查KWORD[NORW+1]是否足够容纳 19 个保留字(NORW+1=20,安全)。

4.5 现象:CALL P调用过程P后,P中访问主程序变量A报错 “segmentation fault”

原因:base()函数未正确实现静态链跳转,或block()在进入P时未正确设置table[i].level和dx初始值,导致base(0,b,s)返回错误基址。

解决:

  • 在block()开头,dx必须重置为3(每个新过程数据段预留 RA/DL/SL);
  • table[i].level在enter()时必须设为当前lev(主程序lev=0,过程lev=1);
  • 在base()中,while (s[b1] != l)前加printf("base: b1=%d, s[b1]=%d, l=%d\n", b1, s[b1], l);观察链是否断裂;
  • 确认CALL指令生成时,GEN(CAL, lev, i)的lev参数是调用者的lev+1。

5. 测试用例与调试技巧:用listcode()输出的 16 进制指令反推你的修改是否生效

PL/0 的强大之处在于,它把编译结果(CODE[]数组)和解释器(interpret())完全分离。这意味着你可以不运行程序,仅靠listcode()输出的目标代码清单,就能 100% 确认你的修改是否正确落地。这是比“能跑通”更底层的验证。

5.1listcode()输出解读:每一行都是你的修改证据

listcode()将CODE[]数组格式化为:地址 操作码 层差 地址。例如:

000 1 0 0 // LIT 0 0 → push 0 001 17 0 3 // STO 0 3 → store to addr 3 (var A) 002 17 0 4 // STO 0 4 → store to addr 4 (var B) 003 13 0 3 // LOD 0 3 → load var A 004 13 0 4 // LOD 0 4 → load var B 005 10 0 0 // OPR 0 0 → neq (<>) 006 15 0 12 // JPC 0 12 → jump if false to addr 12

验证你的修改:

  • 新增ELSE:编译IF A<>B THEN WRITE(A) ELSE WRITE(B),清单中应出现JPC+JMP+JMP三条跳转指令,且地址能

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询