导致除法溢出错误 (x86)
Posted
技术标签:
【中文标题】导致除法溢出错误 (x86)【英文标题】:Causing a divide overflow error (x86) 【发布时间】:2010-10-08 16:14:38 【问题描述】:我有几个关于 x86 或 x86_64 架构上的除法溢出错误的问题。最近我一直在阅读有关整数溢出的文章。通常,当算术运算导致整数溢出时,FLAGS 寄存器中的进位位或溢出位被置位。但显然,根据this article,除法运算导致的溢出不会设置溢出位,而是触发硬件异常,类似于除以零时。
现在,除法导致的整数溢出比乘法少得多。只有几种方法可以触发除法溢出。一种方法是执行以下操作:
int16_t a = -32768;
int16_t b = -1;
int16_t c = a / b;
在这种情况下,由于有符号整数的二进制补码表示,不能用带符号的16位整数表示正32768,所以除法运算溢出,导致-32768的错误值。
几个问题:
1) 与本文所说的相反,以上并没有导致硬件异常。我正在使用运行 Linux 的 x86_64 机器,当我除以零时,程序以 Floating point exception
终止。但是当我导致除法溢出时,程序照常继续,默默地忽略错误的商。那么为什么这不会导致硬件异常呢?
2) 为什么硬件对除法错误的处理如此严重,而不是其他算术溢出?为什么硬件会默默地忽略乘法溢出( 更容易发生意外),而除法溢出却应该触发致命中断?
=========== 编辑 ==============
好的,谢谢大家的回复。我收到的回复基本上说上述 16 位整数除法不应该导致硬件故障,因为商仍然小于寄存器大小。我不明白这一点。在这种情况下,存储商的寄存器是 16 位的 - 太小无法存储带符号的正数 32768。那么为什么不引发硬件异常?
好的,让我们直接在 GCC 内联汇编中执行此操作,看看会发生什么:
int16_t a = -32768;
int16_t b = -1;
__asm__
(
"xorw %%dx, %%dx;" // Clear the DX register (upper-bits of dividend)
"movw %1, %%ax;" // Load lower bits of dividend into AX
"movw %2, %%bx;" // Load the divisor into BX
"idivw %%bx;" // Divide a / b (quotient is stored in AX)
"movw %%ax, %0;" // Copy the quotient into 'b'
: "=rm"(b) // Output list
:"ir"(a), "rm"(b) // Input list
:"%ax", "%dx", "%bx" // Clobbered registers
);
printf("%d\n", b);
这只会输出一个错误的值:-32768
。仍然没有硬件异常,即使存储商 (AX) 的寄存器太小而无法容纳商。所以我不明白为什么这里没有引发硬件故障。
【问题讨论】:
您在谈论特定于硬件的机器语言级别的行为,但您正在使用 C 代码对其进行测试。那是没有意义的。您为什么要尝试将汇编指令规范应用于 C 语言除法运算符?有些东西让您认为您的 C 部门实际上生成了您引用的规范的除法指令?您确认是这种情况吗? @AndreyT,好的,我更新了问题,直接用汇编语言尝试了 您在DX:AX
对中错误地表示了 -32768
。实际上,您将+32768
除以-1
,这就是为什么得到-32768
。看我的回答。
在 32 位 int
的实现上,您的示例不会导致除法溢出。它产生一个完美可表示的int
, 32768,然后在您进行分配时以实现定义的方式转换为int16_t
。 (提示:查找默认促销)
【参考方案1】:
在 C 语言中,算术运算永远不会在小于 int
的类型中执行。每当您尝试对较小的操作数进行算术运算时,它们首先会受到整数提升,将它们转换为int
。如果在您的平台上int
是 32 位宽,那么就无法强制 C 程序执行 16 位除法。编译器将改为生成 32 位除法。这可能就是为什么您的 C 实验不会产生预期的除法溢出。如果您的平台确实有 32 位 int
,那么您最好的办法是使用 32 位操作数尝试相同的操作(即,将 INT_MIN
除以 -1
)。我敢肯定,即使在 C 代码中,您最终也能重现溢出异常。
在您的汇编代码中,您使用的是 16 位除法,因为您将 BX
指定为 idiv
的操作数。 x86 上的 16 位除法将存储在 DX:AX
对中的 32 位被除数除以 idiv
操作数。这就是您在代码中所做的。 DX:AX
对被解释为一个复合 32 位寄存器,这意味着这对中的符号位现在实际上是 DX
的最高位。 AX
的最高位不再是符号位了。
你对DX
做了什么?你只是清除它。您将其设置为 0。但将 DX
设置为 0,您的股息将被解释为 正!从机器的角度来看,这样的DX:AX
对实际上代表了一个正值+32768
。 IE。在您的汇编语言实验中,您将+32768
除以-1
。结果是-32768
,应该是这样。这里没有什么不寻常的。
如果您想在DX:AX
对中表示-32768
,您必须对其进行符号扩展,即您必须用全一位模式填充DX
,而不是零。而不是做xor DX, DX
,你应该用你的-32768
初始化AX
,然后做cwd
。这会将AX
符号扩展为DX
。
例如,在我的实验中(不是 GCC)这段代码
__asm
mov AX, -32768
cwd
mov BX, -1
idiv BX
导致预期的异常,因为它确实尝试将-32768
除以-1
。
【讨论】:
好的,我明白了。谢谢。这触发了预期的硬件故障。 很好地了解汇编语言示例!【参考方案2】:当你得到整数 2 的补码加/减/乘的整数溢出时,你仍然有一个有效的结果 - 它只是缺少一些高位。这种行为通常很有用,因此不适合为此生成异常。
但是对于整数除法,除以零的结果是无用的(因为与浮点不同,2 的补码整数没有 INF 表示)。
【讨论】:
【参考方案3】:与本文所说的相反,以上并未导致硬件异常
文章没有这么说。是说
...如果源操作数(除数)为零或商对于指定的寄存器
来说太大,它们会产生除法错误
寄存器大小肯定大于16位(32 || 64)
【讨论】:
但是在这种情况下,存储商的寄存器只有16位,那么为什么没有引发硬件异常呢?请参阅我编辑的问题以获得进一步说明。 @Channel72,存储商的类型只有16位。寄存器本身是 32 位的。 @MSN:在 x86 中,寄存器大小是可变的。ax
即使在 64 位机器上也是一个 16 位寄存器,这是解释结果如何在这里被截断的唯一方法。
@MSN,我很困惑。 AX寄存器存放商,AX寄存器是16位的
@Potatoswatter,啊,好吧,编辑帖子中的程序集是错误的(请参阅 andreyT 的评论),但我真诚地怀疑 gcc 是否会使用 16 位操作码来进行 int16_t 划分。并且该标准并未规定除法运算的实际实施方式,仅规定其结果。无论如何,你仍然可以得到整数溢出进行除法,但你不应该尝试使用小于 int
的类型(因为你只能用 INT_MIN/-1 来做)。【参考方案4】:
From the relevant section on integer overflow:
与 add、mul 和 imul 不同 指令,英特尔部门 说明 div 和 idiv 不设置 溢出标志;他们产生一个 如果源操作数出现除法错误 (除数)为零或如果商 对指定的来说太大了 注册。
寄存器的大小在现代平台上是 32 位或 64 位; 32768 将适合这些寄存器之一。但是,以下代码很可能会引发整数溢出执行(它在我的核心 Duo 笔记本电脑上的 VC8 上确实如此):
int x= INT_MIN;
int y= -1;
int z= x/y;
【讨论】:
【参考方案5】:您的示例未生成硬件异常的原因是由于 C 的整数提升规则。在执行操作之前,小于int
的操作数会自动提升为ints
。
至于为什么不同类型的溢出处理方式不同,请考虑在 x86 机器级别,实际上不存在乘法溢出这样的事情。当您将 AX 乘以某个其他寄存器时,结果进入 DX:AX 对,因此结果总是有空间,因此没有机会发出溢出异常信号。但是,在 C 和其他语言中,两个ints
的乘积应该适合一个int
,因此在 C 级别存在溢出这样的事情。 x86 有时会在 MUL
s 上设置 OF(溢出标志),但这只是意味着结果的高位部分不为零。
【讨论】:
80386 乘以常数指令使用与源相同大小的单个目标寄存器。他们设置标志以防溢出,代码可能会或可能不会忽略。【参考方案6】:在使用 32 位 int
的实现中,您的示例不会导致除法溢出。它产生一个完美可表示的int
, 32768,然后在您进行分配时以实现定义的方式转换为int16_t
。这是由于 C 语言指定的默认提升,因此,在此处引发异常的实现将不符合要求。
如果您想尝试引发异常(这仍然可能会或可能不会实际发生,这取决于实现),请尝试:
int a = INT_MIN, b = -1, c = a/b;
您可能需要采取一些技巧来防止编译器在编译时对其进行优化。
【讨论】:
【参考方案7】:我猜想在一些旧计算机上,尝试除以零会导致一些严重的问题(例如,将硬件置于无限循环中,尝试减去足够的数,以便在操作员出现之前,余数会小于被除数来解决问题),这开始了除法溢出被视为比整数溢出更严重的错误的传统。
从编程的角度来看,意外的除法溢出没有理由比意外的整数溢出(有符号或无符号)更严重或更严重。考虑到除法的成本,之后检查溢出标志的边际成本将非常小。传统是我认为存在硬件陷阱的唯一原因。
【讨论】:
以上是关于导致除法溢出错误 (x86)的主要内容,如果未能解决你的问题,请参考以下文章