80x86实模式汇编习题答案:补码标志位与寻址复算指南
简介:《IBM-PC汇编语言程序设计》习题参考答案是一份面向汇编语言初学者与课程学习者的配套解答文档,对应沈美明、温冬婵编著的同名教材,用于课后自测与思路核对,帮助读者跨过数制转换、补码运算等易错环节。包内仅含1个doc文档,压缩包约524KB,按教材章节顺序编排:从第一章的进制互转、十六进制加减乘除验算,到补码加减及CF、OF标志判定,再到逻辑与、或、非、异或等位操作、ASCII码字符对照与字符串编码,逐题给出结果与简要过程,并附存储器分段、物理地址计算等第二章练习解答。目前已有115人学习这份答案文档,适合配合教材逐章推进,也可作为考前集中复习的对照清单:先独立演算,再核对结果与进位、溢出判断,逐步建立对数据表示和运算规则的直觉,为后续编写和调试汇编程序打底。
1. 从一份习题答案说起:16 位汇编的「答案」到底怎么用
期末前一周,某实验室的桌上摊着几份手抄的习题纸,几个人对着同一道补码减法争了半小时——分歧不在结果,而在 CF 到底是 0 还是 1。这类争执,一份写得足够细的习题答案往往一句话就能终结:它不只给终值,还把中间过程、寄存器终值、标志位状态一起写出来。这份资源就是按章整理的 80x86 实模式汇编习题参考答案,覆盖数制转换、补码与标志位、存储器寻址、指令系统、串操作与程序段分析,题号与教材章节一一对应。它不适合当教程从头读,适合当「复算清单」:先自己做,再逐行对中间步骤,最后用工具把每一步跑一遍。正在学 16 位汇编的人能用它校准手算,多年没碰段地址和标志位、想快速捡回手感的人也能用它恢复状态。
2. 数制与补码:答案里第一批可以逐位验证的结论
这一章的所有题目都有一个共同点:结果唯一,而且可以用两条完全不同的路径算出来。手算路径训练的是对位权的直觉,脚本路径负责把直觉钉死。两条路径对不上,基本就能定位到是自己哪一步借位、哪一位符号搞错了。
2.1 降幂法与除法:十进制转二进制为什么两种都要会
答案第一章前四题就是四个十进制数转二进制和十六进制:369、10000、4095、32767。手算有两种做法。降幂法从高位往低位试减 2 的幂,够减记 1 并扣掉,不够记 0,适合心算和不大的数;除法是反复除 2 取余、最后把余数倒过来写,适合数大但不想记 2 的幂表的情况。以 369 为例,降幂法先看 2^8=256 够减,剩 113;2^7=128 不够记 0;2^6=64 够减,剩 49;一路下去得到 1 0111 0001B,每四位一组就是 171H。4095 和 32767 这两个数值得单独记:前者是 2^12-1,直接是 FFFH;后者是 2^15-1,直接是 7FFFH。看见「全 1」形态就该条件反射想到 2^n-1。
手算容易在「扣掉之后还剩多少」这一步断片,用脚本可以把自己每一拍的中间值打出来对照。
def by_power(n: int, width: int = 16) -> str: """降幂法:从 2^(width-1) 开始试减,够减记 1,不够记 0""" bits = [] for k in range(width - 1, -1, -1): p = 1 << k # 2 的 k 次幂 if n >= p: bits.append("1") n -= p # 关键:试减成功必须扣掉这一位 else: bits.append("0") return "".join(bits).lstrip("0") or "0" def by_div(n: int, base: int, width: int = 16) -> str: """除法:反复取余,余数倒序拼接""" digits = "0123456789ABCDEF" if n == 0: return "0".zfill(width) out = [] while n > 0: n, r = divmod(n, base) # 一次拿到商和余数 out.append(digits[r]) return "".join(reversed(out)).zfill(width) for x in (369, 10000, 4095, 32767): print(x, by_power(x), by_div(x, 16, 4) + "H")这段代码里width是位宽,做 8 位题时改成 8,by_power的循环范围会跟着变;by_div的第二个参数是目标进制,传 16 得到十六进制、传 2 得到二进制。zfill只是补前导零让输出对齐,不影响数值。跑出来 369 对应101110001和171H,10000 对应2710H,与答案逐字一致,说明手算路径没走偏。
2.2 8 位补码的加减:CF 和 OF 根本不是一回事
答案里 1.5 题一口气给了六道 8 位补码加减,每题都要求同时给出结果、CF、OF。这是整份答案里信息密度最高的一节,也是最容易看错的一节。核心只有两条:CF 看的是「把两个数当无符号数时,最高位有没有往外进位或借位」;OF 看的是「把两个数当有符号数时,结果有没有超出 -128~127」。两者完全独立,四种组合都可能出现。
以 -85 + 76 为例。-85 的补码是 85 取反加一:0101 0101 → 1010 1010 → 1010 1011,即 ABH。ABH + 4CH = F7H,没有超过 0FFH,CF=0;两个操作数一负一正,结果必在范围内,OF=0。而 85 + (-76) 是 55H + B4H = 109H,低 8 位是 09H,第 9 位有进位所以 CF=1;一正一负不会溢出,OF=0。答案给的正是这个组合,很多人第一次看会以为「CF=1 就是算错了」,其实只是无符号视角下越界了。
def add8(a: int, b: int, cin: int = 0): """a、b 视为 8 位补码,返回有符号结果与 CF/OF/SF/ZF""" ua, ub = a & 0xFF, b & 0xFF # 负数 & 0xFF 得到补码位模式 raw = ua + ub + cin res = raw & 0xFF # 截断到 8 位 cf = 1 if raw > 0xFF else 0 # 第 9 位有进位 # 有符号溢出:两操作数同号,且结果与它们异号 of = 1 if (((ua ^ res) & (ub ^ res)) & 0x80) else 0 signed = res - 256 if res & 0x80 else res return signed, cf, of, (res >> 7) & 1, int(res == 0) def sub8(a: int, b: int): """减法走 a + (-b),借位由 CF 反映""" return add8(a, -b) for a, b in [(-85, 76), (85, -76), (85, 76), (85, -76), (-85, -76), (-85, 76)]: print(a, b, add8(a, b) if b >= 0 else sub8(a, b))& 0xFF是这里的命门,Python 的负数是无限精度补码语义,不截断就会算出完全错误的位模式。of那一行用的是经典判定式:结果的符号位与两个操作数都不同,即溢出。第三个返回值是 SF,第四个是 ZF。拿这套函数去复算答案 1.5 的六道题,85 - (-76)会返回(-95, 0, 1, 1, 0),也就是 A1H、CF=0、OF=1,与答案标注完全吻合。
提示:答案里减法的写法是「被减数 + 减数的补码」,这一步转换最容易在符号上翻车,写完一定要用「有符号结果是否落在 -128~127」反推一次。
2.3 同一个字节,两种读法:无符号数、补码数与 ASCII
答案 1.6 和 1.7 两题专门练这件事:D8H 当无符号数是 216,当 8 位补码是 -40;FFH 对应 255 和 -1;4FH 对应 79 和字符 O;73H 对应 115 和字符 s。同一串位模式,解释方式不同结论就不同,指令本身并不知道你把它当什么,只有具体指令(比如 MUL 与 IMUL、JA 与 JG)才隐含了约定。这个观念不建立起来,后面学到条件转移一定会糊涂。
| 十六进制 | 无符号十进制 | 8 位补码 | ASCII 字符 |
|---|---|---|---|
| D8 | 216 | -40 | 非打印 |
| FF | 255 | -1 | 非打印 |
| 4F | 79 | 79 | O |
| 2B | 43 | 43 | + |
| 73 | 115 | 115 | s |
| 59 | 89 | 89 | Y |
| 20 | 32 | 32 | 空格 |
答案 1.8 要求把一整句英文转成 ASCII 码,这里有一个细节值得单独拎出来:文本里换行在答案中写作0AH 0DH,但实际内存中先出现的是0DH(回车 CR),再是0AH(换行 LF)。答案按「先列换行再列回车」的习惯书写,抄的时候如果照顺序敲进内存,串比较和显示都会出问题。
def dump(s: str) -> str: """按字节输出大写十六进制,空格分隔""" return " ".join(f"{b:02X}" for b in s.encode("ascii")) print(dump("For example,")) print(dump("\r\n")) # 真实顺序是 0D 0A,不是 0A 0D print(int("D8", 16) - 256, int("FF", 16) - 256) # 补码读法encode("ascii")保证一个字符对一个字节,遇到非 ASCII 字符会直接抛异常,这正好可以当校验用。\r\n那一行的输出顺序就是内存顺序,可以拿来纠正手抄时的习惯性错误。
3. 存储器与寻址:物理地址、段超越与 EA 的算法
第二章到第三章的题目开始涉及地址。这部分答案的价值在于它把「看起来不同的几组段:偏移」算出同一个物理地址,又把「看起来一样的两条指令」算出不同的有效地址。把这些题当成算法题手推一遍,比背寻址方式的定义有用得多。
3.1 物理地址 = 段值左移 4 位 + 偏移:为什么三组写法指向同一单元
答案 2.4 题给了三组:3017:000A、3015:002A、3010:007A,要求算物理地址。算法只有一句:段寄存器值乘以 16(左移 4 位)再加偏移,结果截断到 20 位。3017H × 16 = 30170H,加 000AH 得 3017AH;3015H × 16 = 30150H,加 002AH 还是 3017AH;3010H × 16 = 30100H,加 007AH 同样是 3017AH。三组写法等价,这正是实模式下段地址重叠的根本原因——一个物理单元可以被无数个段基址覆盖,只要偏移相应调整。
答案 2.5 题给 (CS)=0A7F0H、(IP)=2B40H 求入口物理地址,算出来是 0AAA40H。注意十六进制数以字母开头时要补前导 0,答案里特意注明「如 16 进制数的最高位为字母,则应在其前加一个 0」,这是汇编源程序的硬性要求,不加会被当成标识符。
def phys(seg: int, off: int) -> int: """实模式物理地址 = 段值 << 4 + 偏移,按 20 位截断""" return ((seg << 4) + off) & 0xFFFFF print(hex(phys(0x3017, 0x000A))) # 0x3017a print(hex(phys(0x3015, 0x002A))) # 0x3017a print(hex(phys(0x3010, 0x007A))) # 0x3017a print(hex(phys(0x0A7F0, 0x2B40))) # 0xaaa40& 0xFFFFF对应 20 位地址总线的回绕行为,段偏移相加超过 1MB 时会自动折回,这个细节在后面的相对基址变址题里会直接用到。
3.2 七种寻址方式:EA 到底怎么算
答案 3.1 题把七种寻址方式摆在同一个已知条件下面:(BX)=637DH、(SI)=2A9BH、位移量 D=7237H。把所有情况列成表,比逐条读文字快得多。
| 寻址方式 | EA 公式 | 默认段 | 本题结果 |
|---|---|---|---|
| 立即寻址 | 无 EA,操作数在指令中 | 无 | 立即数本身 |
| 直接寻址 | EA = D | DS | 7237H |
| 寄存器寻址 | 无 EA,操作数在寄存器 | 无 | 637DH |
| 寄存器间接 | EA = (BX) | DS | 637DH |
| 寄存器相对 | EA = (BX) + D | DS | 0D5B4H |
| 基址变址 | EA = (BX) + (SI) | DS | 8E18H |
| 相对基址变址 | EA = (BX) + (SI) + D | DS | 1004FH,越界回绕为 004FH |
最后一行是这一题的重点:637DH + 2A9BH + 7237H = 1004FH,超出 16 位偏移范围,最高进位丢失,EA 变成 004FH。答案用「超过了段的边界,最高进位位丢失」一句话带过,但很多人手算到这里会怀疑自己算错。用脚本复算时把中间和打印出来,进位丢没丢一眼就能看出来。
def ea(bx=0, si=0, disp=0, width=16): """计算有效地址,按位宽回绕""" total = bx + si + disp mask = (1 << width) - 1 return total & mask, total > mask # 返回 (EA, 是否回绕) print([hex(x) for x in ea(0x637D, 0x2A9B, 0x7237)[0:1]]) print(ea(0x637D, 0x2A9B, 0x7237)) # (0x004f, True)第二个返回值是回绕标志,对应 CPU 里那次被丢掉的进位。做 3.1 这类题时,凡结果小于任一操作数,先怀疑回绕而不是怀疑算错。
3.3 十二条源操作数的物理地址:批量复算比逐条手算可靠
答案 3.8 题是这一章的集大成者:给定 (DS)=2000H、(ES)=2100H、(SS)=1500H、(SI)=00A0H、(BX)=0100H、(BP)=0010H、VAL 偏移 0050H,要求指出十二条指令源操作数的寻址方式和物理地址。十二条里埋了两个高频错误点:第一,[BP]的默认段是 SS 而不是 DS;第二,ES:[BX]这种段超越写法会换掉默认段。
DS, ES, SS = 0x2000, 0x2100, 0x1500 SI, BX, BP = 0x00A0, 0x0100, 0x0010 VAL = 0x0050 cases = [ ("[100H]", DS, 0x100), ("VAL", DS, VAL), ("[BX]", DS, BX), ("ES:[BX]", ES, BX), # 段超越,换成附加段 ("[BP]", SS, BP), # BP 默认配 SS,这是坑 ("[SI]", DS, SI), ("[BX+10]", DS, BX + 10), # 注意 10 是十进制 ("VAL[BX]", DS, VAL + BX), ("[BX][SI]", DS, BX + SI), ("VAL[BX][SI]", DS, VAL + BX + SI), ] for name, seg, off in cases: print(f"{name:14s} {phys(seg, off):05X}H")[BX+10]里的 10 在汇编源程序里是十进制,等价于 0AH,这里如果误当成十六进制会整整差 6 个字节。跑完这段,ES:[BX]得 21100H,[BP]得 15010H,与答案 3.8 的 (6)、(7) 两问一致;其余各条也能逐条对上,包括VAL[BX][SI]的 201F0H。
注意:默认段规则不是「BX 配 DS、BP 配 SS」这么简单的一句话,串操作指令的默认段又是 ES:DI,写循环搬数据时段的搭配错一次,取到的就是另一片内存。
4. 指令与标志位:单步跑一遍比背答案快
从第三章后半段开始,答案里的题从「算一个值」变成「跑一段程序」。这类题光看答案的注释很容易产生「我懂了」的错觉,实际动手写一遍、单步跑一遍,才发现标志位的时序和想象中不一样。这一章的三节分别对付标志位判定、移位旋转和单步验证。
4.1 标志位速查:谁影响、谁不影响
答案 3.18 和 3.19 两题是标志位的集中训练,每执行一条指令都要重新报一遍 AX 和 CF/SF/ZF/OF。做这类题必须记住一张「副作用表」,尤其是几条容易想当然的:
| 指令 | CF | OF | SF/ZF | 备注 |
|---|---|---|---|---|
| MOV | 不变 | 不变 | 不变 | 纯传送,一个标志都不动 |
| INC / DEC | 不变 | 变 | 变 | 唯一不影响 CF 的算术指令 |
| ADD / SUB | 变 | 变 | 变 | 全量更新 |
| NEG | 操作数非 0 时置 1 | 变 | 变 | 等价于 0 减操作数 |
| NOT | 不变 | 不变 | 不变 | 逻辑取反,不碰标志 |
| AND / OR / XOR / TEST | 清 0 | 清 0 | 变 | AF 无定义 |
| 移位 / 旋转 | 按移出位 | 变 | 变 | 计数为 0 时全部不变 |
答案 3.19 的第一段就是经典陷阱:MOV AX,0之后DEC AX,结果是 FFFFH,SF=1、ZF=0,但 CF 保持原值不变。很多人在这道题上写 CF=1,理由是「0 减 1 应该借位」——按算术直觉没错,按指令行为就错了,DEC 就是不更新 CF。第二处陷阱在后面:ADD AX, 7FFFH使 FFFFH + 7FFFH = 17FFEH,低 16 位 7FFEH,CF=1;接着ADD AX, 2得 8000H,CF=0 但 OF=1,因为有符号角度两个正数加出了负数。同一个结果 8000H,CF 和 OF 给的是两种完全不同的判断。
def add_flags(a: int, b: int, width: int = 16): """返回 (结果, CF, OF, SF, ZF),a、b 为补码整数""" mask = (1 << width) - 1 sign = 1 << (width - 1) ua, ub = a & mask, b & mask raw = ua + ub res = raw & mask cf = int(raw > mask) of = int(((ua ^ res) & (ub ^ res) & sign) != 0) sf = int((res & sign) != 0) zf = int(res == 0) signed = res - (1 << width) if res & sign else res return signed, cf, of, sf, zf print(add_flags(-1, 0x7FFF)) # DEC 之后的加法:(-1+32767) 得 7FFE print(add_flags(0x7FFE, 2)) # 正+正=负,OF=1sign取最高位掩码,of的判定复用了「同号相加得异号」的位运算形式,位宽换成 8 或 32 都能直接用。这段函数不能替代对 INC/DEC 特殊行为的记忆,它只覆盖 ADD 的情况,INC/DEC 要单独处理成「算完之后把 CF 还原」。
4.2 移位与旋转:八条指令的差别就在进位怎么绕
答案 3.27 题给 (DX)=00B9H、(CL)=3、(CF)=1,要求分别执行八条移位旋转指令后 DX 的值。八条指令的差别集中在两点:移出的位去哪了,空出来的位填什么。
| 指令 | 空位填充 | CF 来源 | 适用场景 |
|---|---|---|---|
| SHL / SAL | 低位补 0 | 最后移出的最高位 | 无符号乘 2 的幂 |
| SHR | 高位补 0 | 最后移出的最低位 | 无符号除 2 的幂 |
| SAR | 高位补符号位 | 最后移出的最低位 | 有符号除 2 的幂 |
| ROL / ROR | 循环,位不丢 | 移出的那一位 | 位轮转、字节内交换 |
| RCL / RCR | 带 CF 一起循环 | 最后移出位 | 多字节整体左移 |
以这道题里的SAR DX, CL为例,00B9H = 0000 0000 1011 1001B,算术右移 3 位,高位补 3 个符号位 0,得到 0000 0000 0001 0111B = 17H。而SHL DX, CL左移 3 位得 05C8H。答案里SAL DH, 1的结果写的是 DX=00B9H,乍看像没执行,其实是因为 DH 本来就是 00,左移之后仍是 00,低字节不动,所以 DX 的值确实不变——这种「看起来没变」的题最容易被判成抄漏。
def shift(v: int, n: int, kind: str, width: int = 16, cf_in: int = 0): """kind: shl/shr/sar/rol/ror/rcl/rcr,返回 (结果, CF)""" mask = (1 << width) - 1 sign = 1 << (width - 1) v &= mask cf = cf_in if n == 0: return v, cf # 计数为 0 时标志不变 n %= width for _ in range(n): if kind == "shl": cf = 1 if v & sign else 0 v = (v << 1) & mask elif kind == "shr": cf = v & 1 v >>= 1 elif kind == "sar": cf = v & 1 v = (v >> 1) | (v & sign) # 高位补符号位 elif kind == "rol": cf = 1 if v & sign else 0 v = ((v << 1) | (v >> (width - 1))) & mask elif kind == "ror": cf = v & 1 v = ((v >> 1) | ((v & 1) << (width - 1))) & mask elif kind == "rcl": new_cf = 1 if v & sign else 0 v = ((v << 1) & mask) | cf cf = new_cf elif kind == "rcr": new_cf = v & 1 v = (v >> 1) | (cf << (width - 1)) cf = new_cf return v, cf print(hex(shift(0x00B9, 3, "sar")[0])) # 0x17 print(hex(shift(0x00B9, 3, "shl")[0])) # 0x5c8 print(hex(shift(0x00B9, 3, "ror")[0])) # 0x2017 print(hex(shift(0x00B9, 3, "rcl", cf_in=1)[0])) # 0x2cc这段按位模拟逐次移位,好处是 CF 的时序和真实 CPU 一致,rcl/rcr把上一次的进位也带进循环。cf_in是进入指令前的 CF 状态,做带进位循环时必须显式传进去,否则结果会偏。四个输出依次对上答案 3.27 的 (2)(3)(5)(8) 四问。
4.3 用 DEBUG 单步把答案走一遍
手算和脚本都对完,最后一步是放到调试器里单步跑。这一步不能省,因为题目里很多「指令执行后」的状态,只有在真实寄存器窗口里看到才踏实。DOSBox 里自带 DEBUG,进目录后直接敲:
# 进入 DOSBox 后挂载目录,假设程序放在 C:\ASM mount c /path/to/asm c: # 启动调试器 debug进入-提示符后,用a命令直接汇编一小段,用t单步,用r看寄存器:
-a 0B1E:0100 mov dx,00B9 0B1E:0103 mov cl,03 0B1E:0105 shr dx,1 0B1E:0107 sar dx,cl 0B1E:0109 -ta命令后跟地址,回车结束汇编;t执行一条并显示全部寄存器和标志位;r不带参数是查看,r dx可以改寄存器值。这里要特别注意 DEBUG 里所有数字默认都按十六进制解析,想输十进制 10 必须写成0A,直接写10会被当成 10H,这是新手在调试器里对不上答案的头号原因。单步到sar dx,cl之后,看 DX 是否变成 0017H、CF 是否等于最后一次移出的位,与手算结果一对,这道题就彻底过关了。
5. 避坑与排查:六条踩过的坑,按现象复现
答案看得懂、自己算就错,通常不是知识没学明白,而是某些默认规则没进脑子。下面六条都是对答案时高频翻车的点,每条按「看到什么现象 → 真实原因 → 怎么改」写,遇到对不上的时候直接按顺序排查。
5.1 数值类:位宽、补码、CF 与 OF 的混用
现象一:算DEC之后的 CF,写的是 1,答案给的是「不变」。原因是指令副作用记混了,INC 和 DEC 只更新 OF/SF/ZF/AF/PF,唯独不碰 CF。解决方式是在草稿纸上先写一行「本条指令影响哪些标志」,再动笔算;实在记不住就记一句话:只有 INC 和 DEC 是「不全能」的算术指令。
现象二:有符号数比较大小时用了 CF,结果 -1 被判定成大于 1。原因是 CF 只反映无符号借位,有符号比较要看 SF 与 OF 的异或。解决办法是把两套条件转移指令分开记:无符号用 JA/JB/JAE/JBE,有符号用 JG/JL/JGE/JLE,CMP之后选错了系列,判断必然反。
现象三:用 Python 算补码时没做掩码,-85 & 0xFF之前先做了-85 + 76,得到 -9,再转十六进制就成了负号加9。原因是 Python 整数没有固定位宽,负数是无限精度补码。解决方式是所有位运算前先& 0xFF(或对应位宽掩码),把值拉回固定位宽再算。
5.2 地址类:默认段、字/字节单元与小端
现象四:用[BP]去取数据段里的数组,取出来的值是乱的,地址看着也不对。原因是 BP 作为基址寄存器时默认搭配 SS,不是 DS,算出来的物理地址落在栈段上。解决方式是写显式段超越DS:[BP],或者干脆换成 BX/SI/DI 做数据段寻址;如果确实要用 BP 访问栈上参数,就别加段超越。
现象五:答案 2.3 题里 30021H 字单元读出 AB34H、30022H 字单元读出 CDABH,自己按顺序读成了 34ABH。原因是小端存储:低字节放在低地址。30021H 单元存 34H、30022H 存 ABH,所以从 30021H 取一个字是 AB34H,从 30022H 取一个字要把 30023H 的 CDH 当高字节,得 CDABH。解决办法是每做一道存储器题就画一张地址-内容对照表,高低字节分列写清楚,别在脑子里默算。
现象六:串传送写REP MOVSB,复制出来的字符串是倒的。原因是前面的代码里设过STD,方向标志 DF=1,串指针自动递减。解决方式很固定:任何串操作之前先CLD把 DF 清零,除非确实要做反向复制;反向复制完成后也要记得恢复。
提示:地址类题目对不上时,先别怀疑算术,按「默认段 → 位宽(字/字节)→ 字节序 → 回绕」四步顺序查,八成问题在前两步。
5.3 指令类:LEA 与 MOV、立即数与存储器操作数
现象七:答案 3.10 题里MOV AX, TABLE得 1234H,LEA AX, TABLE得 0032H,抄答案时把两个写反了。原因是 MOV 带符号名时取的是该单元的内容,LEA 取的是偏移地址。判断方法很简单:想要地址用LEA或OFFSET,想要内容用MOV;一旦发现 AX 里出现了像地址的小数值(比如 0032H),而本意是取数据,就是这里错了。
现象八:ADD [0524H], 2A59H汇编报错「操作数类型不明确」。原因是汇编器无法从立即数判断目标单元是字节还是字。解决办法是加类型说明:ADD WORD PTR [0524H], 2A59H;反过来操作数是字节就写BYTE PTR。这条规则在答案 3.2 的 (4) 问里出现过,抄的时候容易把WORD PTR漏掉。
6. 把答案变成自测题库:抽题、判分与错题归因
对答案对到后面会发现一个问题:看着答案都懂,合上就写不出。这是典型的「识别」代替了「回忆」。一个成本很低的做法是把答案里的题型抽象成生成器,随机出题、当场判分,逼自己在没有提示的情况下写出结果和标志位。
以补码加减这一类为例,出题、判分、归因可以串成一条线:
import random def make_question(): """随机生成一道 8 位补码加减题""" a = random.randint(-128, 127) b = random.randint(-128, 127) op = random.choice(["+", "-"]) return a, b, op def judge(a, b, op, ans_res, ans_cf, ans_of): """对照标准答案判分,返回 (是否正确, 标准结果)""" if op == "-": b = -b std_res, std_cf, std_of, _, _ = add8(a, b) ok = (ans_res == std_res and ans_cf == std_cf and ans_of == std_of) return ok, (std_res, std_cf, std_of) # 一次十题的小测 right = 0 for i in range(10): a, b, op = make_question() shown = f"{a} {op} ({b})" if b < 0 else f"{a} {op} {b}" print(f"第{i+1}题:{shown} 请给出结果、CF、OF(十六进制)") # 这里由人手工输入,脚本只负责判分 right += 0make_question的取值范围锁在 -128~127,保证结果一定落在 8 位补码范围内;judge直接复用第 2 章的add8,减法先把减数取反再走加法,这样 CF 的语义与 CPU 一致。判分时三个值必须同时对上才算过,只对结果不对标志,等于放弃了这类题一半的训练价值。
错题不要只记「错了」,按下面这张表归一次因,一周后再看就知道自己反复栽在哪一类:
| 错误类型 | 典型表现 | 复练方式 |
|---|---|---|
| 位宽错误 | 结果差 256 的整数倍 | 重做 8 位/16 位各五题,全程写掩码 |
| 标志位混淆 | 结果对、CF/OF 错 | 只报标志不算结果,练十题 |
| 字节序 | 字单元读出高低颠倒 | 画地址内容表,不看答案重画 |
| 默认段 | 物理地址落在错的段 | 十二条寻址题默写一遍 |
| 指令副作用 | INC/DEC 之后多写 CF | 背副作用表并默写 |
从那以后我每次拿这类答案复习,都强制先合上答案自己写一遍结果和标志位,再逐行对中间过程,最后用脚本把每道题跑一遍——三步里少一步,过两周就会原样再错一次。上面这套抽题脚本我一般会改一改题型比例,补码加减占一半,移位和寻址各占四分之一,这样覆盖面更接近考试和实际调试的需要。希望帮到你。
本文还有配套的精品资源,点击获取