资讯详情

资讯详情

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^8256 够减剩 1132^7128 不够记 02^664 够减剩 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 位题时改成 8by_power的循环范围会跟着变by_div的第二个参数是目标进制传 16 得到十六进制、传 2 得到二进制。zfill只是补前导零让输出对齐不影响数值。跑出来 369 对应101110001和171H10000 对应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没有超过 0FFHCF0两个操作数一负一正结果必在范围内OF0。而 85 (-76) 是 55H B4H 109H低 8 位是 09H第 9 位有进位所以 CF1一正一负不会溢出OF0。答案给的正是这个组合很多人第一次看会以为「CF1 就是算错了」其实只是无符号视角下越界了。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、CF0、OF1与答案标注完全吻合。提示答案里减法的写法是「被减数 减数的补码」这一步转换最容易在符号上翻车写完一定要用「有符号结果是否落在 -128~127」反推一次。2.3 同一个字节两种读法无符号数、补码数与 ASCII答案 1.6 和 1.7 两题专门练这件事D8H 当无符号数是 216当 8 位补码是 -40FFH 对应 255 和 -14FH 对应 79 和字符 O73H 对应 115 和字符 s。同一串位模式解释方式不同结论就不同指令本身并不知道你把它当什么只有具体指令比如 MUL 与 IMUL、JA 与 JG才隐含了约定。这个观念不建立起来后面学到条件转移一定会糊涂。十六进制无符号十进制8 位补码ASCII 字符D8216-40非打印FF255-1非打印4F7979O2B434373115115s598989Y203232空格答案 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 得 3017AH3015H × 16 30150H加 002AH 还是 3017AH3010H × 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、位移量 D7237H。把所有情况列成表比逐条读文字快得多。寻址方式EA 公式默认段本题结果立即寻址无 EA操作数在指令中无立即数本身直接寻址EA DDS7237H寄存器寻址无 EA操作数在寄存器无637DH寄存器间接EA (BX)DS637DH寄存器相对EA (BX) DDS0D5B4H基址变址EA (BX) (SI)DS8E18H相对基址变址EA (BX) (SI) DDS1004FH越界回绕为 004FH最后一行是这一题的重点637DH 2A9BH 7237H 1004FH超出 16 位偏移范围最高进位丢失EA 变成 004FH。答案用「超过了段的边界最高进位位丢失」一句话带过但很多人手算到这里会怀疑自己算错。用脚本复算时把中间和打印出来进位丢没丢一眼就能看出来。def ea(bx0, si0, disp0, width16): 计算有效地址按位宽回绕 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), ([BX10], 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)[BX10]里的 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。做这类题必须记住一张「副作用表」尤其是几条容易想当然的指令CFOFSF/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结果是 FFFFHSF1、ZF0但 CF 保持原值不变。很多人在这道题上写 CF1理由是「0 减 1 应该借位」——按算术直觉没错按指令行为就错了DEC 就是不更新 CF。第二处陷阱在后面ADD AX, 7FFFH使 FFFFH 7FFFH 17FFEH低 16 位 7FFEHCF1接着ADD AX, 2得 8000HCF0 但 OF1因为有符号角度两个正数加出了负数。同一个结果 8000HCF 和 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 之后的加法(-132767) 得 7FFE print(add_flags(0x7FFE, 2)) # 正正负OF1sign取最高位掩码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的结果写的是 DX00B9H乍看像没执行其实是因为 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_in1)[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/JLECMP之后选错了系列判断必然反。现象三用 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方向标志 DF1串指针自动递减。解决方式很固定任何串操作之前先CLD把 DF 清零除非确实要做反向复制反向复制完成后也要记得恢复。提示地址类题目对不上时先别怀疑算术按「默认段 → 位宽字/字节→ 字节序 → 回绕」四步顺序查八成问题在前两步。5.3 指令类LEA 与 MOV、立即数与存储器操作数现象七答案 3.10 题里MOV AX, TABLE得 1234HLEA 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第{i1}题{shown} 请给出结果、CF、OF十六进制) # 这里由人手工输入脚本只负责判分 right 0make_question的取值范围锁在 -128~127保证结果一定落在 8 位补码范围内judge直接复用第 2 章的add8减法先把减数取反再走加法这样 CF 的语义与 CPU 一致。判分时三个值必须同时对上才算过只对结果不对标志等于放弃了这类题一半的训练价值。错题不要只记「错了」按下面这张表归一次因一周后再看就知道自己反复栽在哪一类错误类型典型表现复练方式位宽错误结果差 256 的整数倍重做 8 位/16 位各五题全程写掩码标志位混淆结果对、CF/OF 错只报标志不算结果练十题字节序字单元读出高低颠倒画地址内容表不看答案重画默认段物理地址落在错的段十二条寻址题默写一遍指令副作用INC/DEC 之后多写 CF背副作用表并默写从那以后我每次拿这类答案复习都强制先合上答案自己写一遍结果和标志位再逐行对中间过程最后用脚本把每道题跑一遍——三步里少一步过两周就会原样再错一次。上面这套抽题脚本我一般会改一改题型比例补码加减占一半移位和寻址各占四分之一这样覆盖面更接近考试和实际调试的需要。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →