一、引子:为什么除法是汇编里的 “定时炸弹”?
想象你在编写一个文件复制程序,需要计算文件大小除以块大小的块数。
- 若文件大小是
0xFFFFFFFF
(无符号数,4294967295),块大小是2
,用DIV
指令能得到正确块数吗? - 若你在处理温度数据,遇到
-5℃ ÷ 2
,用普通除法指令会得到-3
还是-2
?余数该为正还是负?
这两个场景揭示了 x86 除法的核心难点:无符号与有符号的天差地别,以及位数不匹配时的隐藏陷阱。本文将通过 “解剖级” 讲解,让你彻底掌控DIV
/IDIV
的每一个字节行为。
二、DIV 与 IDIV 的本质区别:数据类型决定一切
2.1 一个案例看懂两种除法的鸿沟
场景:计算-5 ÷ 2
-
无符号除法(DIV):
将-5
视为无符号数0xFFFFFFFB
(4294967291),除以2
得商0x7FFFFFFD
(2147483645),余数1
。
结果毫无意义,因为无符号数不理解 “负数”。 -
有符号除法(IDIV):
按补码规则计算,商为-2
,余数为-1
(数学上-5 = 2×(-2) + (-1)
)。
结果符合预期,因为 IDIV 会处理符号。
为什么 ADD 不需要区分符号?
加法的二进制运算对有符号 / 无符号数共享同一套电路(仅标志位解读不同),而除法的商和余数在两种类型下逻辑完全不同,必须分开指令。
三、无符号除法 DIV
:CPU 里的 “分糖果机”
1. 功能:把数据 “平均分堆”
- 8 位除法:把
AX
里的数(16 位)除以源
(8 位),结果拆成两部分:- 商:放到
AL
里(8 位,范围 0~255)。 - 余数:放到
AH
里(8 位,范围 0~255)。
- 商:放到
- 16 位除法:把
AX
里的数(16 位)除以源
(16 位),结果拆成两部分:- 商:放到
AX
里(16 位,范围 0~65535)。 - 余数:放到
DX
里(16 位,范围 0~65535)。
- 商:放到
- 32 位除法:把
DX:AX
组合起来的数(32 位)除以源
(16 位或 32 位),结果拆成两部分:- 商:放到
AX
里(16 位)或EAX
里(32 位)。 - 余数:放到
DX
里(16 位)或EDX
里(32 位)。
- 商:放到
- 64 位除法(x86-64):把
RDX:RAX
组合起来的数(64 位)除以源
(32 位或 64 位),结果拆成两部分:- 商:放到
RAX
里(64 位)。 - 余数:放到
RDX
里(64 位)。
- 商:放到
被乘数的位置固定,由乘数宽度决定
乘数宽度 | 被乘数位置(隐含) | 乘数来源(指令操作数) | 乘积存储位置 | 乘积宽度 |
---|---|---|---|---|
8 位(字节) | al | 寄存器(如 bl )或内存字节 | ax (16 位) | 2 倍于乘数 |
16 位(字) | ax | 寄存器(如 bx )或内存字 | dx:ax (32 位,高 16 位在dx ) | 2 倍于乘数 |
32 位(双字) | eax | 寄存器(如 ecx )或内存双字 | edx:eax (64 位,高 32 位在edx ) | 2 倍于乘数 |
64 位(四字) | rax | 寄存器(如 rbx )或内存四字 | rdx:rax (128 位,高 64 位在rdx ) | 2 倍于乘数 |
关键说明:
- 被乘数不可变:无论乘数是寄存器还是内存,被乘数必须提前存入对应的累加器(
al
/ax
/eax
/rax
),不能用其他寄存器替代。
2. 用法:就像用计算器
; 8位除法:100 ÷ 3
MOV AX, 100 ; AX = 100 (0x64)
MOV BL, 3 ; BL = 3
DIV BL ; 计算 100 ÷ 3
; 结果:AL = 33 (0x21), AH = 1 (0x01)
; 16位除法:10000 ÷ 50
MOV AX, 10000 ; AX = 10000 (0x2710)
MOV BX, 50 ; BX = 50 (0x32)
DIV BX ; 计算 10000 ÷ 50
; 结果:AX = 200 (0xC8), DX = 0
; 32位除法:0x12345678 ÷ 0x100
MOV EDX, 0x1234 ; EDX = 0x1234
MOV EAX, 0x5678 ; EAX = 0x5678
; 合起来是 0x12345678
MOV EBX, 0x100 ; EBX = 0x100
DIV EBX ; 计算 0x12345678 ÷ 0x100
; 结果:EAX = 0x123456, EDX = 0x78
; 64位除法(x86-64):0x123456789ABCDEF ÷ 0x1000
MOV RDX, 0x12345678 ; RDX = 高64位
MOV RAX, 0x9ABCDEF ; RAX = 低64位
; 合起来是 0x123456789ABCDEF
MOV RBX, 0x1000 ; RBX = 0x1000
DIV RBX ; 计算 0x123456789ABCDEF ÷ 0x1000
; 结果:RAX = 商, RDX = 余数
3. 避坑指南:别让 CPU 罢工
- 除数不能是 0:
- 任何情况下,只要除数是 0,CPU 会直接触发
#DE
异常(除法错误),程序崩溃!
- 任何情况下,只要除数是 0,CPU 会直接触发
- 商不能太大:
- 8 位除法:商必须 ≤ 255(因为
AL
只有 8 位)。 - 16 位除法:商必须 ≤ 65535(因为
AX
只有 16 位)。 - 32 位除法:商必须 ≤ 4294967295(因为
EAX
只有 32 位)。 - 64 位除法:商必须 ≤ 2^64-1(因为
RAX
有 64 位,基本不会溢出)。
- 8 位除法:商必须 ≤ 255(因为
4. 生活类比:分糖果的规则
- 8 位除法:
- 总糖果数 ≤ 65535(因为
AX
是 16 位)。 - 分给 ≤ 255 个人(因为
源
是 8 位)。 - 每人最多拿 255 颗(因为
AL
是 8 位)。
- 总糖果数 ≤ 65535(因为
- 16 位除法:
- 总糖果数 ≤ 65535(因为
AX
是 16 位)。 - 分给 ≤ 65535 个人(因为
源
是 16 位)。 - 每人最多拿 65535 颗(因为
AX
是 16 位)。
- 总糖果数 ≤ 65535(因为
- 32 位除法:
- 总糖果数 ≤ 4294967295(因为
EDX:EAX
是 32 位)。 - 分给 ≤ 4294967295 个人(因为
源
是 32 位)。 - 每人最多拿 4294967295 颗(因为
EAX
是 32 位)。
- 总糖果数 ≤ 4294967295(因为
5. 常见错误案例
; 错误1:8位除法商溢出
MOV AX, 10000 ; AX = 10000
MOV BL, 2 ; BL = 2
DIV BL ; 计算 10000 ÷ 2 = 500
; 错误!500 超过 AL 的最大值 255
; 错误2:16位除法商溢出
MOV AX, 0xFFFF ; AX = 65535
MOV BX, 1 ; BX = 1
DIV BX ; 计算 65535 ÷ 1 = 65535
; 正确!但如果结果是 65536,就会溢出
; 错误3:除数为 0
MOV EAX, 1000
MOV EBX, 0
DIV EBX ; 错误!除数为 0
6. 总结:记住这三点
- 位数匹配:被除数、除数、商、余数的位数要匹配(比如 8 位除法用
AX
、AL
、AH
)。 - 防溢出:计算前先估算商的范围,避免超过目标寄存器的最大值。
- 除数≠0:任何情况下都不能除以 0!
一句话记忆:DIV
就像按不同规格分糖果,要根据口袋大小(寄存器位数)来分,否则糖果会撒出来(溢出),或者根本没法分(除以 0)!
四、有符号除法 IDIV
:CPU 里的 “正负数分糖果机”
1. 功能:处理带符号数的除法
- 与
DIV
的区别:IDIV
认为操作数是有符号数(可正可负),而DIV
认为是无符号数(只能是正数)。 - 商的正负规则:
由被除数和除数的符号共同决定(同号得正,异号得负):- 同号(+/+ 或 -/-)→ 商为正
- 异号(+/- 或 -/+)→ 商为负
- 余数的符号:始终与被除数相同(例如
-10 ÷ 3 = -3 余 -1
,而不是-3 余 2
)。
2. 用法:和 DIV
类似,但更 “智能”
; 8位有符号除法:-10 ÷ 3 = -3 余 -1
MOV AX, -10 ; AX = 0xFFF6(-10 的补码)
MOV BL, 3 ; BL = 3
IDIV BL ; 计算 -10 ÷ 3
; 结果:AL = -3 (0xFD), AH = -1 (0xFF)
; 16位有符号除法:10000 ÷ -50 = -200 余 0
MOV AX, 10000 ; AX = 0x2710(10000 的补码)
MOV BX, -50 ; BX = 0xFFCE(-50 的补码)
IDIV BX ; 计算 10000 ÷ -50
; 结果:AX = -200 (0xF938), DX = 0
; 32位有符号除法:-0x12345678 ÷ 0x100
MOV EDX, 0xFFFFFFFF ; EDX = -1(高32位)
MOV EAX, 0xEDCBA988 ; EAX = -0x12345678 的补码
MOV EBX, 0x100 ; EBX = 0x100
IDIV EBX ; 计算 -0x12345678 ÷ 0x100
; 结果:EAX = -0x123456, EDX = -0x78
3. 避坑指南:负数的 “陷阱”
- 商的溢出规则更严格:
商必须在目标寄存器的有符号范围内(例如 16 位时为-32768 ~ 32767
)。
典型溢出场景:-32768 ÷ -1 = 32768
,但 32768 超出 16 位有符号范围,会触发溢出! - 余数的符号易错:
余数符号由被除数决定,与除数无关。例如-10 ÷ 3
的余数是-1
,而不是2
。
4. 生活类比:借钱和还钱
- 被除数:欠款(负数)或存款(正数)。
- 除数:要分给几个人(必须是非零数,正负均可)。
- 商:每个人分到的欠款(负数)或存款(正数)。
- 例子:
-10 元欠款 ÷ 3人 = 每人欠 -3 元,还剩 -1 元欠款
。10 元存款 ÷ -2人(理解为“还2次钱”)= 每次还 -5 元(即收 5 元)
。
- 例子:
- 余数:剩下的欠款或存款(符号与被除数相同)。
5. 常见错误案例
; 错误1:商溢出(-32768 ÷ -1 = 32768 超出范围)
MOV AX, -32768 ; AX = -32768 (0x8000)
MOV BX, -1 ; BX = -1 (0xFFFF)
IDIV BX ; 错误!商 32768 超出 16 位有符号范围
; 错误2:余数符号误解
MOV AX, -10 ; AX = -10
MOV BL, 3 ; BL = 3
IDIV BL ; 结果:AL = -3, AH = -1(余数为负!)
; 错误3:除数为 0
MOV EAX, -1000
MOV EBX, 0
IDIV EBX ; 错误!除数为 0
6. 总结:记住这三点
- 符号规则:
- 商的正负:由被除数和除数的符号共同决定(同号得正,异号得负)。
- 余数的符号:始终与被除数相同。
- 防溢出:计算前估算商的范围,避免超出目标寄存器的有符号最大值。
- 除数≠0:任何情况下都不能除以 0!
一句话记忆:IDIV
是带 “正负号” 的分糖果机,商的正负由 “借钱方” 和 “被借方” 共同决定,余数符号永远和 “借钱方” 一致!
五,AAD(ASCII Adjust for Division):除法前的 ASCII 调整指令
1. 功能:将两位 BCD 码转换为二进制数
- 应用场景:在进行除法运算前,将 非压缩 BCD 码(Unpacked BCD)调整为二进制数,以便得到正确的商和余数。
- BCD 码:用 4 位二进制表示一个十进制数字(0-9),非压缩 BCD 码中每个十进制数字占一个字节的低 4 位,高 4 位为 0。
2. 调整规则
- 数学公式:
AX = AH × 10 + AL
- 具体操作:
- 将
AH
中的值乘以 10(相当于十进制的十位)。 - 加上
AL
中的值(相当于十进制的个位)。 - 结果存入
AX
,同时将AH
清零。
- 将
3. 用法示例
; 场景:计算 56 ÷ 8 = 7
MOV AH, 5 ; AH = 5(十位)
MOV AL, 6 ; AL = 6(个位)
; AX 现在表示非压缩 BCD 码的 56
AAD ; 执行调整:AX = 5×10 + 6 = 56(二进制)
; 结果:AH = 0, AL = 56(0x38)
MOV BL, 8 ; BL = 8(除数)
DIV BL ; 执行除法:56 ÷ 8 = 7
; 结果:AL = 7(商), AH = 0(余数)
4. 为什么需要 AAD?
- 直接除法的问题:如果不使用
AAD
,直接用DIV
计算AH=5, AL=6
:AX = 0x0506
(十进制 1286)。1286 ÷ 8 = 160 余 6
,结果完全错误!
- AAD 的作用:将非压缩 BCD 的
56
正确转换为二进制的56
(0x38),再进行除法,得到正确结果7
。
5. 标志位影响
AAD
会影响以下标志位:- SF(符号标志):根据
AL
的结果设置。 - ZF(零标志):如果
AL
为 0,则 ZF=1。 - PF(奇偶标志):根据
AL
中 1 的个数的奇偶性设置。
- SF(符号标志):根据
- 其他标志位(CF、OF、AF)状态未定义。
6. 注意事项
- 仅用于非压缩 BCD:
AAD
假设AH
和AL
中存储的是非压缩 BCD 码(即 0-9)。 - 必须在 DIV 前使用:
AAD
是为除法运算专门设计的调整指令,需紧跟在DIV
之前。 - 调整后 AH 被清零:执行
AAD
后,AH
会被自动清零,仅AL
保留结果。
7. 生活类比
- 场景:你有 5 张 10 元纸币(
AH=5
)和 6 张 1 元纸币(AL=6
),想平均分给 8 个人。 - 错误做法:直接用
506
除以 8(把纸币张数当成数值)。 - 正确做法:
- 先计算总金额:
5×10 + 6 = 56
元(AAD
的作用)。 - 再分钱:
56 ÷ 8 = 7
元 / 人(DIV
的作用)。
- 先计算总金额:
8. 总结
- 一句话功能:
AAD
是除法前的 “BCD 码转二进制” 工具,确保非压缩 BCD 码的除法结果正确。 - 使用公式:
AAD → DIV
(先调整,再除法)。
记忆口诀:AAD
像个 “BCD 翻译官”,把十位和个位的 BCD 码翻译成真正的二进制数,让除法不会算错!
六,未被展开的「暗线」:除法背后的性能与安全
本文聚焦于指令行为,但现实编程中,除法的「陷阱」远不止指令本身:
- 性能黑洞:DIV/IDIV 是 x86 中最慢的指令之一(尤其早期 CPU),执行周期可达数十甚至上百时钟周期。例如在高频循环中使用除法,可能成为性能瓶颈。优化技巧如:
- 用移位替代乘除(如无符号数除以 2^n 可直接右移);
- 利用数学变换将除法转化为乘法(如使用倒数近似值,需结合浮点指令)。
- 编译器的「隐身术」:现代编译器会对除法做激进优化。例如:
- 无符号除法
x/2
可能被编译为(x >> 1)
; - 有符号负数除法
x/-2
可能需额外处理符号(如(x + 1) >> 1
),但手工汇编需自行实现逻辑。
- 无符号除法
- 安全漏洞温床:未校验的除法可能导致缓冲区溢出(如用户可控的除数为 0 触发异常),或逻辑错误(如商溢出导致数组越界)。这在系统级代码(如驱动程序)中尤为危险。
七,未被触及的「边界」:更多除法场景
- 浮点除法的「另一宇宙」:
- x87 浮点指令(如 FDIV)处理浮点数除法,遵循 IEEE 754 标准,需关注 NaN、无穷大等特殊值;
- 与整数除法不同,浮点除法的舍入模式(向零、向无穷等)需显式控制。
- 大整数除法的「assembly 艺术」:
- 当处理超过 64 位的整数除法时,需手动实现多精度除法算法(如试商法),通过多次 DIV/IDIV 组合完成,这考验对余数传递和符号扩展的理解。
- 除法与数论的「隐秘联系」:
- 模运算(余数)在密码学(如 RSA 的模幂运算)、哈希算法(如取模分组)中是核心操作;
- 带符号数的余数性质(如
a = b*q + r, |r| < |b|
)可用于算法优化(如快速求余替代除法)。
八,全文总结:掌控除法的「三重境界」
- 知其然:
- DIV 与 IDIV 的本质区别在于对「符号」的理解 —— 前者视操作数为无符号数(纯二进制值),后者按补码规则处理正负;
- 位数匹配是使用除法指令的核心(如 8 位除法用 AX/AL/AH,32 位用 EDX:EAX),溢出和除零是致命错误。
- 知其所以然:
- 余数的符号由被除数决定(IDIV 特性),这与数学定义一致(如
-5 = 2*(-2) + (-1)
); - AAD 指令是 BCD 码除法的「翻译官」,解决十进制数位与二进制运算的语义鸿沟。
- 余数的符号由被除数决定(IDIV 特性),这与数学定义一致(如
- 知其所以不然:
- 避免在关键路径使用除法,优先用位运算或查表替代;
- 对用户输入的除数必须做严格校验(除数≠0,商在目标寄存器范围内);
- 理解汇编除法是理解高级语言编译器行为的「显微镜」—— 例如 C 语言的
(-1)/2
在 x86 上会编译为 IDIV,得到 - 1(因余数符号与被除数一致),而某些语言可能定义为向零取整(得 - 0.5 取整为 0),需注意语义差异。
九,写给读者的「最后一公里」
除法指令是汇编中「简单而危险」的存在:它的操作数看似直白,却暗藏符号、位数、性能的多重陷阱。掌握它不仅需要记住指令格式,更要理解二进制世界对「数」的独特诠释 —— 无符号数的「纯粹」与有符号数的「补码智慧」,本质是计算机用有限位宽模拟无限数学概念的妥协。
下次当你在 C 代码中写下count = size / block;
时,不妨在调试器中查看汇编,观察编译器如何用 DIV/IDIV 实现逻辑,或是用移位偷偷优化。这一行代码的背后,是 CPU 用数十个时钟周期完成的精密计算,也是人类智慧与硬件限制的又一次和解。
愿你在汇编的「01 世界」中,既能驾驭除法的精准,也能避开它的暗礁,让每一个商和余数都成为程序稳定运行的基石。毕竟,计算机科学的魅力,往往藏在这些「简单指令」的复杂细节里。