汇编闰年程序Debug翻车:CH未清零与CX循环陷阱
简介:这份文档面向学习汇编语言与微机接口课程的高校学生,尤其适合正在做北京交通大学相关实验、需要掌握程序调试方法的学习者。内容围绕一个判断输入年份是否为闰年的 leapYear.exe 程序展开,覆盖编译链接与运行、代码整体逻辑与逐句逐段分析、Debug 调试过程中的闰年与非闰年两类输入演示、D、T、C、P 等常用指令的上机实践,以及发现逻辑错误后的代码修改与重新编译验证,形成从编写到排错的完整闭环。资源包共 1 个 docx 文档,约 78.94MB,以图文步骤记录实验过程,便于对照复现。目前已有 521 人学习。读者可据此理解寄存器与内存状态观察方法、断点设置技巧和条件判断错误的定位思路,也可作为实验报告撰写与上机复习的参照材料。
1. 同一个闰年程序,为什么双击运行正确、Debug 里跑就翻车
同一个 leapYear.exe,在命令行敲进 2020、2025、2100,闰年判断一次没错;搬到 Debug 里用 G 命令跑,输入 2021 却照样蹦出This is a leap year!。这不是玄学,程序没改,改的只是它被启动时寄存器里残留的那些值。
这份汇编与接口课程的调试实验,真正值钱的地方不在背下几条 Debug 命令,而在于它把一行MOV CL,[BUF+1]的后果完整摊开给人看:循环的边界是 CX 这整个 16 位寄存器,不是你以为的那个 8 位 CL。写高级语言时循环次数由编译器和运行时兜底,我们从来不关心高 8 位装了啥;一旦落到裸机寄存器上,CH 里那点来历不明的残留值,就足够让一个判闰年的程序对所有输入都回答"是"。
后面按顺序拆:编译链接阶段产物各自是干什么的、键盘输入怎么变成内存里的二进制数、三次 DIV 怎么拼出闰年判定、以及 Debug 里那几步单步为什么能让错误现形。每一层都落到具体的寄存器和机器码上,不讲抽象概念。
2. 从 .asm 到 .exe:masm 与 link 在背后补齐了哪些东西
2.1 一次编译链接留下的六个文件,各是什么角色
把 masm5 整个目录放到 C 盘根下,是为了让masm和link这两个可执行文件能被 PATH 或者当前目录找到,省掉每次敲绝对路径。进入目录后两条命令就够:
cd C:\masm5 masm leapYear.asm; # 汇编:源码 -> 目标文件,分号表示后面所有提问都用默认值回答 link leapYear.obj; # 链接:目标文件 -> 可执行文件,同样用分号一路默认分号这个写法很多人不知道它的含义:它让汇编器和链接器不再逐项询问"要不要生成清单文件""要不要生成映像文件",全部按默认答案走。一旦交互式地回答,多敲几个回车反而容易在某个提问上答错,产物不全会让人以为编译失败。链接成功后目录里会多出.obj、.lst、.ref、.map、.crf、.exe这一串文件,它们不是废料,各管一件事。
| 文件 | 产生阶段 | 内容 | 什么时候真用得上 |
|---|---|---|---|
.obj | 汇编 | 机器码加尚未确定的地址重定位项 | 链接器的输入,单独没法运行 |
.lst | 汇编 | 源码与机器码逐行对照,附段偏移 | 算某条指令在第几字节,定断点地址 |
.crf/.ref | 汇编/链接 | 标号被定义与被引用的行号清单 | 找某个 LAB 到底从哪儿跳过来 |
.map | 链接 | 各段的起始地址、段长、程序总大小 | 确认 DATA 段被放在哪个段基址 |
.exe | 链接 | 带重定位表的加载映像 | 交给 DOS 装载执行 |
.lst是这个实验里被严重低估的文件。Debug 里用 U 反汇编,看到的地址是0C59:0000、0C59:0003这样的偏移,想提前知道某条INT 21H落在哪个偏移上,就得回头翻清单文件:清单里每条指令前面的四位十六进制就是它在代码段内的偏移。没有它,你只能靠一条条 T 单步数着走,效率低还容易数错。
2.2 MOV AX,DATA 这一圈绕的到底是什么
程序开头永远是这个套路,两行代码,缺一不可:
START: MOV AX, DATA ; DATA 是段名,链接后才确定的段基址,先送通用寄存器过渡 MOV DS, AX ; 段寄存器只能从通用寄存器或内存装载,不能接立即数为什么不能直接MOV DS, DATA?因为 x86 的指令编码里,段寄存器没有"立即数到段寄存器"这种形式,只有"寄存器/内存到段寄存器"。而DATA在汇编阶段还不是一个确定数字——它只是段名,真正的段基址要等链接器把所有段排好位置才定下来,装载时再由 DOS 的重定位机制填进 exe 映像。所以汇编器生成的是B8 xx xx(MOV AX, imm16)后跟一个重定位项,MOV DS,AX则是8E D8两个字节。这两行的机器码加起来 5 个字节,一个都不能省。
这也是为什么MOV AX,DATA里的 AX 只是"过渡容器"——它在这一刻不承载任何业务含义,纯粹是因为段寄存器不接受立即数而临时借用的。很多初学者把MOV AX,DATA当成"把数据段装进 AX",然后在后面接着用 AX 算东西,结果 DS 是对了,AX 的初值却被这段样板代码污染了。我在别的程序里见过因此算错累加和的案例,排查半天,最后发现是这段几乎所有汇编课本都会抄的开头埋的雷。
2.3 三段骨架与 ASSUME 的边界
整个程序的结构是三个段,DATA 放字符串和变量,STACK 放栈空间,CODE 放指令:
DATA SEGMENT INFON DB 0DH,0AH,'Please input a year: $' W DW 0 BUF DB 8 DB ? DB 8 DUP(?) DATA ENDS STACK SEGMENT 'stack' DB 200 DUP(0) STACK ENDS CODE SEGMENT ASSUME DS:DATA, SS:STACK, CS:CODEASSUME是纯伪指令,不产生任何机器码。它的作用是在汇编阶段告诉汇编器:"遇到不带段前缀的内存操作数,默认按 DS 指向 DATA 段来解释"。比如后面MOV AL,[SI]这条指令,机器码里根本没有段信息,CPU 执行时一律用 DS 作为默认段。如果 ASSUME 写错但运行时 DS 装对了,程序照样跑;反过来 ASSUME 写对而 DS 没装载,取到的就是垃圾。这种"两边不一致也能跑/也会崩"的耦合,正是汇编调试里最需要建立的心智模型:伪指令只影响汇编器的判断,段寄存器才决定 CPU 的实际行为。
栈段那 200 字节也不是随便写的。STACK SEGMENT 'stack'里的'stack'是段类别名,链接器据此把这段标记成栈段,DOS 装载时把 SS:SP 指到这段的末尾,SP 初值大约是 200。后面 IFYEARS 里连续三次 PUSH 会消耗 6 个字节,200 字节的余量绰绰有余。如果这段忘了写或者太小,PUSH 就会一路压到别的数据上去,表现为"程序跑着跑着输出乱码",这种崩溃最难查,因为它不在出错的那一行报错。
3. 键盘到内存:INT 21H 的两条功能号和 DATACATE 的累加套路
3.1 AH=09H:$ 才是这段字符串的句号
LEA DX, INFON ; DX 指向字符串首地址,LEA 取的是偏移而不是内容 MOV AH, 9 ; 功能号 9:显示以 $ 结尾的字符串 INT 21H这里有个在高级语言里完全不会遇到的约定:字符串的结束标志是$(ASCII 码 24H),不是长度,也不是 0。DOS 的 09H 功能从 DX 指向的地址开始逐字节往屏幕送,一遇到$就停。所以字符串内容里绝对不能出现$,否则输出会被截断——这个坑在输出金额、符号表之类的内容时特别容易踩。
INFON的定义是0DH,0AH,'Please input...',前面两个字节是回车和换行:0DH是 CR,把光标送回行首;0AH是 LF,往下走一行。顺序不能反,写成0AH,0DH在很多终端上会看到光标斜着往下跑。把这两个控制字符直接编进字符串而不是单独输出,是因为 09H 一次调用就能把"换行 + 提示语"全部送出去,少一次 INT,这在一堆需要反复交互的程序里是实打实的性能考虑。
3.2 AH=0AH:BUF 头两个字节是一份小型协议
输入年份用的是 0AH 功能,它的缓冲区结构是三段式:
LEA DX, BUF ; DX 指向缓冲区结构首地址 MOV AH, 10 ; 功能号 0AH:带缓冲的键盘输入 INT 21HBUF定义的三个部分各司其职:BUF+0是调用前由程序填好的"最多接受几个字符(含回车)",这里是 8;BUF+1是调用返回后 DOS 写回的"实际读到了几个字符(不含回车)";BUF+2开始才是真正的字符内容,末尾会保留一个0DH。
| 偏移 | 谁写 | 含义 |
|---|---|---|
BUF+0 | 程序 | 缓冲区容量上限,含回车符 |
BUF+1 | DOS | 实际输入字符数,不含回车 |
BUF+2起 | DOS | 输入内容,末尾附0DH |
这个协议里最反直觉的一点是:容量包含回车,长度不包含回车。输入2020加回车一共 5 个字符,所以容量至少要给 5;返回值却是 4。如果按"容量就是数字位数"来理解,给 4 就会在输入四位年份时被拒——DOS 只会收下前 3 个字符,最后一个数字根本进不了缓冲区,后面的转换自然全错。这种 off-by-one 在缓冲区结构里是常态,血泪经验就是:看到"最大长度"这个词,先问一句含不含结束符。
3.3 DATACATE:把四个 ASCII 字节折叠成一个整数
年份在内存里是32H 30H 32H 30H(即 "2020"),要变成07E4H才能参与除法。这个转换由 DATACATE 完成:
DATACATE PROC NEAR LEA SI, BUF+2 ; SI 指向第一个数字字符 MOV BX, 0 ; BX 充当累加器 X3: MOV AL, [SI] ; 取出一个字符 SUB AL, 30H ; 减去 '0' 的 ASCII 码,得到 0~9 的数字 MOV AH, 0 ; 高位清零,把 AL 零扩展成 16 位 XCHG AX, BX ; 交换:AX 拿到累计值,BX 拿到当前数字 MOV DX, 10 ; 乘数 10 MUL DX ; DX:AX = AX * 10,16 位乘法结果放在 DX:AX XCHG AX, BX ; 再换回来:BX 拿到累计值*10,AX 拿回当前数字 ADD BX, AX ; BX = 累计值*10 + 当前数字 INC SI ; 指针后移一位 LOOP X3 ; CX 减 1,不为 0 就继续 MOV W, BX ; 结果存进变量 W RET DATACATE ENDP这段代码干的事用一句话概括就是 Horner 展开:n = ((d1*10 + d2)*10 + d3)*10 + d4。不用四次幂运算,只用四次"乘十再加",这是把十进制字符串转成二进制最省事的写法,任何进制转换的底层实现都是这个套路。
两次XCHG AX,BX是这段代码最绕的地方,根源在于MUL指令的硬性约束:16 位乘法只能对 AX 操作,结果固定放在 DX:AX 里,没有"乘以某个寄存器再存回原处"的形式。所以只能把累计值搬到 AX、算完再搬回去。反复交换看着低效,但省下了额外的内存访问,在 8086 这种寄存器稀少的架构上是很典型的取舍。
参数上要记清楚:入口时 SI 指向第一个数字字符,CX 是循环次数(也就是位数);出口时 BX 就是转换结果,同时已经写进了变量 W。这个过程会破坏 AX 和 DX,调用方不能指望它们保持原值。
3.4 一个位数被塞进 CL 的返回值
在调 DATACATE 之前,主程序先做了这么一件事:
MOV CL, [BUF+1] ; 把输入位数(4)送进 CL LEA DI, BUF+2 ; BUF+2 送进存储器指针 CALL DATACATECL是CX的低 8 位,MOV CL, x这种 8 位写操作在 x86 上只改低 8 位,高 8 位原样保留。这是 x86 寄存器"部分写入"特性的直接体现,也是整个 bug 的种子:如果此刻 CH 里恰好是 0,一切岁月静好;如果 CH 里躺着上一位主人留下的01H,那 CX 就是0104H,LOOP 会忠实地循环 260 次而不是 4 次。
为什么直接运行时 CH 总是 0?因为 DOS 装载一个 exe 时会初始化一批寄存器,CX 通常被置 0 或某个固定值。而 Debug 里用 G 命令运行时,程序不是被装载器重新初始化过的,寄存器里是上一次单步、上一次 INT 21H 调用后残留的状态。同一个 bin,两套初始条件,这就是"双击运行正确、Debug 里错"这种看似灵异现象的物理根源。
4. 三次 DIV 一条判定链:IFYEARS 怎么用 CF 把结果交回主程序
4.1 闰年规则落到三次 DIV 上,余数就是判据
闰年的完整定义是三段式:能被 4 整除但不能被 100 整除,或者能被 400 整除。这段逻辑被翻译成三条DIV加一串条件跳转:
IFYEARS PROC NEAR PUSH BX ; 保护三个将被破坏的寄存器 PUSH CX PUSH DX MOV AX, W ; AX = 年份 MOV CX, AX ; CX 留一份备份,后面每次除法前重取 MOV DX, 0 ; 除法前必须清零 DX,否则被当成 32 位被除数的高半部分 MOV BX, 4 ; 除数 4 DIV BX ; DX:AX / BX -> 商在 AX,余数在 DX CMP DX, 0 ; 余数为 0 说明能整除 JNZ LAB1 ; 不能被 4 整除,直接判非闰年 MOV AX, CX MOV BX, 100 ; 第二次:判 100 DIV BX CMP DX, 0 JNZ LAB2 ; 能被 4 整除但不能被 100 整除 -> 是闰年 MOV AX, CX MOV BX, 400 ; 第三次:判 400 DIV BX CMP DX, 0 JZ LAB2 ; 能被 400 整除 -> 是闰年 LAB1: CLC ; 清进位标志,表示"不是闰年" JMP LAB3 LAB2: STC ; 置进位标志,表示"是闰年" LAB3: POP DX ; 恢复现场 POP CX POP BX RET IFYEARS ENDPDIV的行为是这里最需要钉死的细节:16 位除法把 DX:AX 当成 32 位被除数,除以 BX 后,商放 AX、余数放 DX。也就是说 DX 在除法前是输入(被除数的高 16 位),除法后是输出(余数),一进一出,角色完全不同。如果第一次除法前忘了MOV DX,0,被除数就变成了DX:AX拼起来的几十亿,除出来的商极可能超过 16 位能表示的范围,CPU 直接触发除法溢出异常,在 DOS 下表现为程序毫无征兆地中断、连错误提示都没有。这种崩溃比逻辑错误更让人摸不着头脑。
三条 DIV 的顺序也是有讲究的:先判 4,是因为"不能被 4 整除"这一条直接排除率最高(四分之三的年份都能被这条干掉),先判能省两点后续计算。先判 100 再判 400,是因为 400 的情况更罕见,把它放到最后,绝大多数年份根本走不到第三条 DIV。这就是"最常见的情况先短路"的典型写法,虽然在这种一年跑一次的小程序里省不了什么,但在判定链动辄七八层的场景里,顺序几乎决定性能。
4.2 NEAR 与 PUSH/POP:裸机上最朴素的调用约定
PROC NEAR里的 NEAR 决定了两件事:调用方用CALL时只压入 IP(2 字节),不压 CS;子程序返回时用RET弹回 2 字节。如果用 FAR,压栈的就是 CS:IP 共 4 字节,两个字节的差别在高频调用里也是成本。这个实验里所有子程序都是 NEAR,因为它们和主程序分在同一个代码段。
PUSH BX / CX / DX和结尾对应的三个 POP,是 8086 时代最基本的现场保护方式。哪些寄存器该保护,没有硬性规定,全凭调用约定:DATACATE 破坏了 AX、DX 和 SI,而且没做任何保护,因为调用它之前主程序对这些寄存器没有依赖;IFYEARS 保护了 BX、CX、DX 但没保护 AX,因为主程序在拿到 CF 之后只需要判断标志位,AX 的内容已经不被关心。
这种"按需保护"的做法在今天看很不规范,但它揭示了一个事实:调用约定不是语言强加的,是调用双方商量的。没有编译器替你记账,你就得自己清楚每一处 CALL 之后哪个寄存器还能用。判断标准也很朴素——谁需要跨过 CALL 继续使用某个值,谁就负责保护它。
4.3 拿 CF 当返回值的省事与代价
这个程序最巧妙也最脆弱的设计是:闰年判定结果不是放在 AX 或 AL 里返回的,而是压在了进位标志 CF 上。主程序用一行JC A1拿走结果:
CALL IFYEARS JC A1 ; CF=1 表示是闰年,跳去输出"是闰年"这么写确实省事:CF 天生只有 0/1 两种状态,正好对应布尔量,判定过程中顺手用STC/CLC就写完了,不需要额外的指令去装 AL。但它同时引入了一个隐患——CF 是一个全局共享的状态位,任何一条算术、移位、逻辑运算指令都会改写它。如果哪天有人在STC和RET之间插一句MOV AX,CX(不影响标志位,没事)或者CMP AX,0(会改写 CF,出事),返回值就神不知鬼不觉地变了。
现代 ABI 之所以把返回值统一放在 AL/EAX 里,就是因为它不会被中间指令随手改掉。用 CF 传返回值是 8086 时代的实用主义,在那个寄存器比黄金还贵的年代是合理的,但今天再看它,更像是一个"能跑但不好维护"的典型样板。
4.4 DX 一路没清零却没出事的原因
这段代码里有个看起来像 bug 的地方:只有第一次除法前写了MOV DX,0,第二、第三次除法前都只有MOV AX,CX和MOV BX,xxx,DX 没有重置。按前文说的"DX 是 32 位被除数的高半部分",DX 若不是 0,除出来的结果就完全错了。可程序跑是对的。
原因藏在控制流的路径约束里。第一次 DIV 之后 DX 是"年份 mod 4"的余数;随后是一条CMP DX,0加JNZ LAB1——只要余数不为 0,程序就已经跳走了,根本走不到第二次除法。因此能走到第二次除法的那条路径上,DX 必然等于 0。同理,第二次 DIV 后 DX 是"年份 mod 100",JNZ LAB2会把非零的情况全部过滤掉,能走到第三次除法的,DX 又必然是 0。
这是典型的"路径约束保证不变量"的手法:不显式清零,靠前面的判断保证到达时一定干净。这么写省了一条指令,也把一个隐含假设(每次进入除法前 DX=0)散布在三个不同的位置,读代码的人必须把整条路径在脑子里跑一遍才能确认它安全。如果哪天有人把JNZ LAB1改成JMP LAB1之前先做点别的,或者调整了判定顺序,这里立刻翻车。
5. 排查:Debug 单步时最容易翻车的五个地方
这几个坑的共同点是:报告出来的现象和真正的原因往往隔了一层,不建立起"寄存器状态随着指令流转"的直觉,很容易在错误的地址上反复打转。
5.1 T 一头扎进 INT 21H 里出不来
现象:用 T 单步执行到INT 21H这一行,再按一次 T,屏幕开始滚动输出一堆看不懂的机器码反汇编,程序的执行流像是被劫持了;想继续往下走,怎么按都回不到自己的代码。
原因:T 指令的原理是给 CPU 置单步陷阱标志,让每条指令执行完都触发一次单步中断。而INT 21H本身也是一条指令,它执行完就跳进 DOS 内核的处理程序了,接下来 T 停下来的位置是 DOS 内部的第一条指令,不是你的下一条。也就是说你没有"进错地方",是这个工具就这么设计的——它把控制权交给了中断,并且中断里那条指令也是"一条指令",也会被单步陷阱抓住。
解决:遇到INT 21H、CALL、LOOP这类会改变执行流的指令,改用 P。P 的语义是"执行到下一条同层指令",它会把整个中断处理程序当作一条指令执行完,停在你代码的下一行。经验法则是:想看自己代码的逐步流转用 T,想跨过系统调用和子程序用 P,两者交替着用,比硬按 T 高效得多。
5.2 P 把整个子程序当一条指令吞了
现象:用 P 去执行CALL DATACATE,一按下去,BX 直接从 0 变成了07E4H,中间四次循环、四次乘十加全都看不见,想观察累加过程的愿望落空。
原因:P 对 CALL 的处理就是"跑到子程序返回为止",它在子程序末尾的 RET 上设了临时断点,等于把整个子程序当成一个黑匣子。对 LOOP 指令也是如此,P 会一直执行到 CX 归零、循环真正结束,你想看每次循环后的 BX,一次都看不到。
解决:观察子程序内部必须用 T,而且要提前算好入口的偏移地址,从LEA SI,BUF+2那条开始单步。反过来,如果只想验证"调用后结果对不对"而不关心过程,P 更省事。两种指令不是替代关系,是互补关系:外层用 P 快速跳过已确认无误的部分,可疑的那段用 T 逐条啃。
5.3 只改 CL 不改 CH,循环次数变成 256 倍
现象:输入 2020,用 T 单步进入 DATACATE,走到MOV CL,[BUF+1]之后看寄存器,CX 显示0004,看起来一切正常。改用 P 跑完整个循环,BX 却变成了71B0H,而不是应有的07E4H。
原因:MOV CL,[BUF+1]只写了低 8 位。在这个具体的运行环境里,CH 恰好残留着01H,于是 CX 实际是0104H,LOOP 老老实实地循环了 260 次。260 次和 4 次之间差的不是次数,是每一次都在BX = BX*10 + d上继续累积,早就溢出了 16 位能表达的范围。这个时候如果只看 CL 的值,永远发现不了问题——必须在 R 命令里看完整的 CX 16 位显示,而不是只看低 8 位的那个数字。
解决:在MOV CL,[BUF+1]后面补一句MOV CH,0,或者干脆先用MOV CX,0再取位数。更稳妥的写法是MOVZX CX,BYTE PTR [BUF+1],一条指令完成零扩展。这个问题的隐蔽性在于它取决于运行环境:直接双击运行 CX 初值是干净的,只有调试器里才会暴露,属于典型的"环境敏感型 bug"。
5.4 溢出截断把结果抹平成一个常数
现象:在 Debug 里不管输入 2020、2021 还是别的什么年份,最终都输出This is a leap year!,而且每次跑完看到的 BX 都是同一个71B0H。看起来像是"输入根本没被读进去",但用 D 命令查缓冲区,32 30 32 31明明躺在那里。
原因:这轮循环次数是 260,每一轮都在做BX = BX*10 + d,结果被截断在 16 位以内。把它写成模运算就是BX_k ≡ (BX_{k-1}*10 + d_k) mod 65536,展开后BX_k ≡ Σ d_j · 10^(k-j) mod 65536。关键在于 10 的幂:10^m = 2^m · 5^m,只要m ≥ 16,这个乘积就含有 2 的 16 次方因子,模 65536 之后恒为 0。换句话说,第 k 轮之后,只有最后 16 个输入数字还能影响结果,前面输入的四位年份被彻底"忘掉"了。后面那 256 次循环读的是缓冲区之外的内存,内容与年份无关,于是所有输入都收敛到同一个常数。
解决:先修 5.3 那个循环次数的问题,再谈结果对不对。这一步的价值在于它解释了"为什么错误会表现得如此规整"——不是随机乱码,是一个稳定的错误值。凡是看到"输入千变万化、输出恒定不变"的现象,第一反应就该是查溢出截断,而不是怀疑输入没读到。
5.5 U 反汇编里对不上的行数
现象:用 U 命令反汇编,屏幕上列出来的指令条数明显比源文件的行数少,SEGMENT、ENDS、ASSUME、PROC这些行一个都找不到,导致没法按源码行号去对应地址。
原因:这些全是伪指令,只在汇编阶段起作用,不生成任何机器码。U 命令是拿内存里的机器码往回翻译,机器码里没有的东西它自然显示不出来。所以反汇编看到的永远只是代码段里真实存在的字节,而伪指令留下的唯一痕迹是它们造成的段布局和偏移变化。
解决:把.lst清单文件打开对照,左边是源码、右边是机器码和偏移,这才是源码与地址之间的权威对照表。另一种办法是用 U 的起止地址分段反汇编,比如只反汇编DATACATE那一段,把范围缩小到能一眼看完。养成看清单文件的习惯之后,定断点、算跳转目标都会快很多。
6. 一个 CH 没清零暴露的取舍:现场保护、返回值与环境依赖
修这个 bug 只要一行:
MOV CL, [BUF+1] MOV CH, 0 ; 补这一行,把循环计数器的高 8 位清干净 LEA DI, BUF+2 CALL DATACATE但更值得琢磨的是三种修法各自的取舍:补MOV CH,0最省事,前提是调用方记得 CX 的语义;MOV CX,4直接硬编码位数,绕开了部分写入问题,但把"年份必须是四位"这件事焊死在代码里;MOVZX CX,BYTE PTR [BUF+1]一条指令到位,是现代汇编里更常见的零扩展写法,代价是需要 386 以上的指令集。这个实验的机器和教材以 8086 为准,所以第一种最合适。
| 修法 | 指令数 | 前提 |
本文还有配套的精品资源,点击获取