简介:《IBM-PC汇编语言程序设计》习题参考答案,面向计算机专业学生、自学汇编的初学者及备考复习者。内容覆盖数制转换、8位补码运算、溢出与进位标志判断、位操作、ASCII码识别及字符串编码等核心知识点,包含大量典型例题与详细计算过程,答案按章节编排,便于对照教材同步练习。压缩包仅为1个doc文档,大小524KB,轻量精简,下载后用Word即可直接查阅。目前已有114人学习下载。通过系统分步解答和结果校核,读者既能验证自己的计算结果,也能强化补码加减、物理地址换算、字符编码识别等汇编入门的重点难点,对理解低层数据表示和后续编写汇编程序很有帮助。
1. 从“对答案”到“验答案”:这份 IBM-PC 汇编习题答案该怎么用
拿到《IBM-PC汇编语言程序设计》的习题答案,第一反应基本都是“对答案”,核一下自己算的数对不对。但这份由原作者整理的参考答案真正值钱的地方,不在最终结果,而在推理过程:数制转换怎么做、补码减法为什么翻车、寻址方式的有效地址怎么组合、标志位在哪一步变。对正在啃实模式汇编、准备补考或者第一次接触 DEBUG 的人来说,这相当于一份带解析的作业批改。
我把手册拆完一遍后,发现它并不是拿来就能照抄的。里面有几处手稿式省略,比如没写操作数宽度、把多条指令连在一行、甚至有个别题目标号错位。所以后文所有结论,我都强制用 DEBUG 和脚本重放了一遍——能直接用的答案不到一半,剩下的是要二次加工的“半成品”。这篇笔记就把验证过程中值得记录的细节、参数和坑位按章节拆开。
2. 数制转换与补码运算:用手工验算和 30 行脚本把第 1 章答案跑通
2.1 降幂法与除法结果一致,但适用场景完全不同
第 1.1 题要转 369、10000、4095、32767 这四个数。答案给的是两条路径:降幂法和除法。降幂法的思路是每次找一个不大于剩余值的最大的 2 的幂,比如 369 = 256 + 64 + 32 + 16 + 1,写成二进制就是 1 0111 0001B;除法是反复除以 2 取余,再把余数倒序排列。两条路结果一致,但手算时我一般用降幂法,因为它每一步都能看出当前位的权重,心算不容易错;而写循环代码时用除法更顺手,因为不需要维护“小于剩余值的最大幂”这个中间状态。
x = 369 bits = '' n = x while n: bits = str(n & 1) + bits n >>= 1 print(bits.zfill(16)) # 高位补0到16位,方便按4位一组读 print(hex(x)) # 0x171,对应答案的171H这段代码里n & 1取最低位,等价于n % 2,但位运算语义更明确;n >>= 1等价于n // 2。zfill(16)是把结果补足到 16 位,这样1 0111 0001能直接按0000 0001 0111 0001分组转十六进制,避免高位缺失导致分组错位。注意 Python 的hex()输出小写且带0x,和手册里的171H格式不同,对照时只看数值部分即可。
2.2 补码减法:85-(-76) 的 OF=1 不是偶然
第 1.5 题里最值得复盘的是 (4):85-(-76)。按补码规则,减法统一转成“被减数加负数的补码”。-76 的 8 位补码是 76=0100 1100B,取反得 1011 0011B,加 1 得 1011 0100B。于是 85-(-76) 变成 0101 0101B + 0100 1100B = 1010 0001B = 0A1H。
这个结果在 8 位宽度下符号位是 1,表示负数 -95,显然和真实值 161 不符,所以 OF=1。判定条件是两个同符号数相加,结果符号位与操作数相反。这里 85 和 76 都是正数,最高位都是 0,加完变成 1,就是溢出。
a = 85 & 0xFF # 0101 0101 b = (-76) & 0xFF # 1011 0100 s = (a + b) & 0xFF of = ((a & 0x80) == (b & 0x80)) and ((s & 0x80) != (a & 0x80)) print(hex(s), of) # 0xa1 True,对应OF=1& 0xFF的作用是把操作数强制看成 8 位补码位模式,避免 Python 整数自动扩展成 32 位。OF判断里先比两个操作数的符号位是否相同,再比结果符号位是否被改变。这里用True表示 OF=1,和答案里的OF=1对应。如果你把s直接按 16 位解释,得到的是 00A1H,OF=0,所以做题前一定要看清题目是否限定位宽。
2.3 符号位、ASCII 与无符号读法:同一个字节有三种身份
第 1.6 和第 1.7 题把这种“多重身份”讲得很透。D8H 作为无符号数是 216,作为 8 位补码带符号数是 -40;4FH 作为数值是 79,作为 ASCII 码是字符 O。这说明一个问题:汇编里不存在“这个字节是什么类型”的元信息,类型完全由指令决定。用MOV AL, [BX]读出来就是字节,用MOV AX, [BX]读出来就是字,符号位要不要扩展看你在后面用的是CBW还是直接拼接。
自查时我会按这个顺序看:先确认题目限定的位宽和格式,再确认指令默认的段寄存器,最后才是数值本身。第 1 章大多数翻车都翻在前两步,而不是算术。
3. 寻址方式与有效地址计算:把 3 个寄存器和 8 组内存值算给你看
3.1 七种寻址方式:一张表把 BX/SI/D 的组合算完
第 3.1 题给了三样东西:(BX)=637DH,(SI)=2A9BH,位移量 D=7237H,然后列出七种寻址方式。这题刷一遍基本就把实模式有效地址的规则全带出来了:
| 寻址方式 | 有效地址计算 | 结果 |
|---|---|---|
| 立即寻址 | 操作数在指令中 | 无 EA |
| 直接寻址 | EA = D | 7237H |
| 寄存器寻址 | 操作数在 BX | 无 EA |
| BX 间接寻址 | EA = (BX) | 637DH |
| BX 寄存器相对寻址 | EA = (BX) + D | 0D5B4H |
| 基址变址寻址 | EA = (BX) + (SI) | 8E18H |
| 相对基址变址寻址 | EA = (BX) + (SI) + D | 004FH |
最后一行是关键坑位:637DH + 2A9BH + 7237H = 1004FH,但 8086 的有效地址寄存器只有 16 位,最高进位直接丢弃,所以答案只写 004FH。这不是算错,而是实模式地址加法器的真实行为——EA 本身按 16 位截断,物理地址的进位由段寄存器和地址加法器继续处理,不影响 EA 寄存器的截断结果。很多教材把这个细节一笔带过,做题时才会发现答案对不上。
3.2 小端存储:同一块内存被四条 MOV 读出四种结果
第 3.4 题给的内存数值很典型:(DS)=2000H,(BX)=0100H,(SI)=0002H,然后从 20100H 开始连续存 12H、34H、56H、78H,从 21200H 开始存 2AH、4CH、B7H、65H。四条 MOV 指令取出的 AX 分别是 1200H、0100H、4C2AH、3412H,后面三条内存寻址的差异全在宽度和端序。
mem = { 0x20100: 0x12, 0x20101: 0x34, 0x20102: 0x56, 0x20103: 0x78, 0x21200: 0x2A, 0x21201: 0x4C, } def word(addr): return mem[addr] | (mem[addr + 1] << 8) print(hex(word(0x20100))) # 0x3412 print(hex(word(0x20102))) # 0x7856mem[addr + 1] << 8就是把高地址字节放到高 8 位。x86 是 little-endian,低字节在低地址,所以 20100H 存 12H、20101H 存 34H 时,读字结果是 3412H 而不是 1234H。这个顺序反了,后面 3.20 题的双字加法、3.21 的双字求补全都会错。
第 3.10 题顺带问了一个必考的区分:MOV AX, TABLE是把 TABLE 单元的内容 1234H 送 AX;LEA AX, TABLE是把 TABLE 的偏移地址 0032H 送 AX。前者访存,后者只算地址。做题时如果答案和你预期差一个“取内容还是取地址”,十有八九是这里混了。
3.3 转移指令的偏移量:3.5 题里的“+03H”为什么不能省
第 3.5 题的段内直接转移,答案算的是(IP)=5119H+((IP)+03H)=7CDCH。这里面的 03H 是 JMP NEAR PTR 指令本身的机器码长度:E9 后跟 16 位相对位移,总共 3 字节。CPU 执行转移时,IP 已经指向下一条指令,所以相对位移的基准是当前 IP+3,而不是 JMP 所在地址。
常见做法是先把指令长度算死,再套公式目标IP = 下一条指令的IP + 位移量。对于段内间接寻址,比如JMP WORD PTR [BX],目标 IP 直接取内存中的字,不再加位移;对于JMP D[BX],则要加上寄存器相对寻址算出的位移量。三种方式最容易漏的就是第一个“+03H”,我一般会在草稿上先把指令字节数标出来,再动手算。
4. 标志位、移位与多字节运算:3.19 题逐指令拆 CF/SF/ZF/OF
4.1 从 MOV AX,0 到 ROR:一条条读 3.19 的答案
第 3.19 题是整份手册里信息密度最高的一题:十二三条指令,每条都要你写出 AX、CF、SF、ZF、OF 五个量。把它完整走一遍,基本就把标志位的更新规则复习完了。
我把关键几条拆开看:
MOV AX, 0:AX 变 0,标志位不变。DEC AX:AX 变 0FFFFH,CF 不变,SF=1,ZF=0,OF=0。这里最反直觉的是 CF 不动。很多人以为减 1 一定借位,但 x86 里 INC/DEC 专门设计成不影响 CF,为的是多字节运算时中间增量不破坏外层的进位链。ADD AX, 7FFFH:0FFFFH + 7FFFH = 17FFEH,截断后 AX=7FFEH。符号位是 0,两个正数相加得到正数,OF=0;但无符号结果超过 16 位,CF=1。ADD AX, 2:7FFEH + 2 = 8000H,符号位从 0 变 1,OF=1,CF=0。NOT AX:8000H 取反得 7FFFH,标志位不变。NOT 不影响任何标志,这是和 NEG 最大的区别。SAL AX, 1:58D1H 左移一位变 B1A2H,移动出去的位是 0,CF=0;符号位从 0 变 1,OF=1。SAR AX, 1:B1A2H 算术右移一位变 D8D1H,最高位补原来的符号位 1,符号不变,OF=0。ROR AX, 1:循环右移一位后是 9397H。ROR 的 OF 定义为最高位和次高位的异或,1 XOR 0 = 1,所以 OF=1。
def add16(a, b): s = (a + b) & 0xFFFF cf = 1 if (a + b) > 0xFFFF else 0 sf = 1 if (s & 0x8000) else 0 zf = 1 if s == 0 else 0 of = 1 if ((a & 0x8000) == (b & 0x8000)) and ((s & 0x8000) != (a & 0x8000)) else 0 return hex(s), cf, sf, zf, of print(add16(0xFFFF, 0x7FFF)) # ('0x7ffe', 1, 0, 0, 0) print(add16(0x7FFE, 0x0002)) # ('0x8000', 0, 1, 0, 1)这段脚本只模拟了 ADD 的标志计算,cf用无符号进位判断,of用同号相加判异号。SAL、SAR、ROR 这些移位标志没法靠一个通用函数覆盖,建议直接用 DEBUG 的t单步看标志位变化。手算时比脚本更方便的是记住一条原则:OF 只在“补码结果超出位宽可表示范围”时置 1,CF 只在“无符号结果超出位宽”时置 1。两者在同一指令里可能完全不同,比如 0FFFFH + 7FFFH 就是 CF=1、OF=0。
4.2 SAL 与 SAR:+53 和 -49 乘除 2 为什么用的是同一个 SAL
第 3.25 题要求把 +53 和 -49 分别乘以 2。答案里两个数都用SAL AL, 1,得到 6AH 和 9EH。左移不分有无符号,因为最高位移出去后由 CF 接收,符号位被新位替换,这是乘法场景下 SAL/SHL 语义一致的原因。
除 2 就不一样了。+53 用SAR AL, 1得到 1AH,也就是 26,向零截断;-49 用SAR AL, 1得到 0E7H,十进制 -25。如果把 -49 换成SHR AL, 1,逻辑右移会把符号位补 0,得到 67H,十进制 103,完全错误。所以选型规则很简单:带符号数右移用 SAR,无符号数右移用 SHR;遇到“除以 2”先想操作数的类型,再决定指令。
这里还要注意 SAR 对负数的舍入方向。0CFH 算术右移一位是 0E7H,等价于 -49 除以 2 后向负无穷取整,得到 -25。C 语言里-49 / 2是向零截断,结果是 -24,和 SAR 的结果不一致。写汇编时如果需要向零截断,得先判断符号再修正。
4.3 多字节与多精度:ADC 和 SBB 在双字运算里的位置
第 3.20 题给了两组四字数据,要求字相加、双字相加、双字相乘。答案解析里最典型的是双字加法:
MOV AX, DATAX ADD DATAY, AX MOV AX, DATAX+2 ADC DATAY+2, AX MOV DATAY+4, 0 ADC DATAY+4, 0第一组ADD产生进位后,第二组必须用ADC把 CF 带进高 16 位;最后再加ADC DATAY+4, 0是为了承接最高位可能的进位。这里DATAY+4就是专门为进位准备的扩展单元,如果漏掉最后的ADC,双字加法在数值超过 0FFFFFFFFH 时会静默丢失最高位。
多字节乘法的结构也类似:MUL的结果默认放在 DX:AX,但双字乘双字时,每两个 16 位块相乘会产生中间积,必须把交叉项分别累加到对应的高位块里。答案给的 AA/BB/CC/DD 四个变量就是干这个的。做题时不要只背顺序,要记住一个原则:无论加减乘除,多字节运算的本质是把 N 字节拆成若干 16 位块,逐块运算并显式传递进位或借位。
5. 避坑:对照这份答案自查时最常见的 5 个问题
5.1 寻址表达式缺宽度:ADD [BX+0B2H], CX编译不过
现象:第 3.2 题答案写成ADD [BX+0B2H], CX,拿给汇编器编译直接报 operand must have size,加上BYTE PTR或WORD PTR才能通过。
原因:寄存器相对寻址只是算出了内存地址,但双操作数指令里内存操作数的宽度必须由PTR显式指定。BX 寄存器本身不携带类型信息,汇编器不知道你要按字节还是按字累加。
解决:字操作写ADD WORD PTR [BX+0B2H], CX,字节操作写ADD BYTE PTR [BX+0B2H], AL。看到答案里省略了 PTR,不要直接抄,先判断操作数寄存器宽度,再补全。这也是考题里最常见的“合法但不完整”写法。
5.2 手稿式省略:MOV BX, (6-1)*2不是合法指令
现象:第 3.3 题答案第二问写MOV BX, (6-1)*2,然后直接用MOV DX, [BX+(6-1)*2]。这不是 MASM 能识别的语法。
原因:原答案是在表达“第 6 个字的下标偏移是 10”,中间省略了将数组起始地址装入 BX 的步骤,甚至把立即数表达式当成地址的一部分来写。这是手稿转文字时常见的简化,不是标准写法。
解决:按我的习惯重写为三条指令:
LEA BX, BLOCK MOV SI, (6-1)*2 MOV DX, [BX][SI]LEA取数组首地址,SI存元素偏移,最后基址变址并附带默认段 DS。如果直接把立即数塞给 BX 再[BX],取到的是数据段偏移 10 处的字,而不是数组第 6 个元素。对照答案时看到这种缩略写法,先补 LEA,再算偏移。
5.3 DEC 不影响 CF:减法直觉在这条指令上失灵
现象:第 3.19 题执行DEC AX后,答案标注“CF 不变”,很多初学者写成 CF=1,后续ADC/SBB的推演全乱。
原因:x86 指令集中 INC/DEC 特意不影响 CF,因为它们是“增量/减量”指令,常用于循环计数和地址修正,如果它们改动 CF,会破坏外层多字节运算已经建立好的进位状态。SUB/SBB/CMP 才负责更新 CF。
解决:判断 CF 前先看指令类型。减法系列只看 SUB、SBB、CMP;加法则看 ADD、ADC。遇到DEC,CF 维持上一状态不变,其他 SF/ZF/OF 照常更新。这条规则在 3.13 题堆栈操作、3.18 题DEC AX里反复出现,记牢能少踩很多坑。
5.4 答案序号错位:3.27 题的 RCR DL,1 被排到清单末尾
现象:第 3.27 题原题列了 (1) 到 (8) 八条指令,但答案清单最后一条写的是“(4) RCR DL, 1”,和题目序号无法对应,容易让人以为做了八条后漏算一条。
原因:这是手稿排版/OCR 处常见的浮动注释错位,不是技术错误。题号保留但没有严格按序排列,答案数据本身仍按题目初始条件计算。
解决:遇到序号对不上,不要顺着答案的排位走,回到题面(DX)=0B9H,(CL)=3,(CF)=1重新逐条验证。我一般把每条指令单独抄出来,和答案值逐行对齐,不依赖它标的编号。RCR DL,1 的初始值其实是(DL)=0B9H, CF=1,右移一位后是 0DCH,与答案一致。
5.5 位宽陷阱:8 位补码和 16 位补码结论完全不同
现象:第 1.5 题限定 8 位补码,85-(-76) 结果写 0A1H、OF=1。如果按 16 位算,结果是 00A1H、OF=0,答案看起来就像“错了”。
原因:位宽决定符号位位置。8 位时 0A1H 最高位是 1,解释为负数 -95,明显溢出;16 位时 00A1H 最高位是 0,解释为正数 161,正好是真实值。两个结果都对,前提是位宽匹配。
解决:做题前先把位宽圈出来。题目说“8 位二进制补码”就全程按 8 位操作,立即数超过 0FFH 要截断;说“16 位双字”就在地址计算时保留 16 位 EA。遇到答案和预期差一个符号位,先查位宽,再查端序,这两步能排除一半以上的错误。
6. 把答案变成调试用例:用 DEBUG 单步重放一遍 3.19 的指令序列
最后说一个我到现在还在用的验证套路:把习题答案里的每一条指令,原封不动敲进 DEBUG,用t单步跟踪,逐条对比 AX 和标志位。这个方法不挑环境,DOSBox 里打开 DEBUG 就能做。
切入点是第 3.19 题的整段指令。先把初始状态清干净,再输入指令序列:
-a MOV AX, 0 DEC AX ADD AX, 7FFF ADD AX, 2 NOT AX SUB AX, FFFF ADD AX, 8000 SUB AX, 1 AND AX, 58D1 SAL AX, 1 SAR AX, 1 NEG AX ROR AX, 1每条指令后用r看寄存器,用t单步执行。DEBUG 的标志位显示是一组缩写:NV 表示 OF=0,OV 表示 OF=1;UP 表示 DF=0;DI 表示 IF=0;PL 表示 SF=0,NG 表示 SF=1;NZ 表示 ZF=0,ZR 表示 ZF=1;NA 表示 CF=0,CY 表示 CF=1。对照 3.19 的答案时,直接把答案里的 CF/SF/ZF/OF 翻译成这些缩写,逐行核对。
我一般还会再写一段 Python 断言脚本,把关键节点固化成自动化检查:
cases = [ ("ADD AX, 7FFF", 0xFFFF, 0x7FFF, 0x7FFE, 1, 0, 0, 0), ("ADD AX, 2", 0x7FFE, 0x0002, 0x8000, 0, 1, 0, 1), ("SUB AX, 1", 0x0000, 0x0001, 0xFFFF, 1, 1, 0, 0), ] for name, a, b, exp_s, exp_cf, exp_sf, exp_zf, exp_of in cases: s, cf, sf, zf, of = add16(a, b) assert (s == hex(exp_s) and cf == exp_cf and sf == exp_sf and zf == exp_zf and of == exp_of), nameadd16函数复用第 4 章的模拟逻辑,断言失败会直接报出指令名,定位到具体哪一步算错。这个脚本不是验证答案本身,而是验证我自己有没有把标志位规则记混。从那以后,我每次拿习题答案都强制走一遍 DEBUG 重放,不再直接信纸面结论,尤其是那些没有标注位宽和溢出状态的中间步骤。DEBUG 给你看的是硬件真实行为,比翻答案页更快,也比凭记忆推标志位更可靠。希望这套“答案当用例”的办法,能帮你把习题册里的每一道题都变成可验证的调试记录。
本文还有配套的精品资源,点击获取