我有一些关于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)的寄存器太小而不适合商,仍然没有硬件异常。所以我不明白为什么这里没有引起硬件故障。
答案 0 :(得分:13)
在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
。即在汇编语言实验中,您将+32768
除以-1
。结果是-32768
,应该如此。这里没什么不寻常的。
如果您想在-32768
对中代表DX:AX
,则必须对其进行签名扩展,即您必须使用全位模式填充DX
,而不是零。您应该使用xor DX, DX
初始化AX
,然后完成-32768
,而不是cwd
。这会将AX
符号扩展为DX
。
例如,在我的实验(不是GCC)中这段代码
__asm {
mov AX, -32768
cwd
mov BX, -1
idiv BX
}
导致预期的异常,因为它确实试图将-32768
除以-1
。
答案 1 :(得分:2)
当你得到一个带整数2的补码加/减/乘的整数溢出时,你仍然有一个有效的结果 - 它只是缺少一些高阶位。这种行为通常很有用,因此不适合为此生成异常。
对于整数除法,除以零的结果是无用的(因为,与浮点不同,2的补码整数没有INF表示)。
答案 2 :(得分:1)
与本文所说的相反,上述内容并未导致硬件异常
文章没有说。是说
...如果源操作数(除数)为零或者商指定对于指定的寄存器
,则会产生除法错误
寄存器大小肯定大于16位(32 || 64)
答案 3 :(得分:1)
From the relevant section on integer overflow:
与add,mul和imul不同 说明,英特尔部门 指令div和idiv没有设置 溢出旗帜;他们生成一个 如果源操作数,则除错 (除数)为零或商 对于指定的太大了 注册
寄存器的大小在32或64位的现代平台上; 32768将适合其中一个寄存器。但是,下面的代码很可能抛出一个整数溢出execption(它在VC8上我的核心Duo笔记本电脑上):
int x= INT_MIN;
int y= -1;
int z= x/y;
答案 4 :(得分:0)
您的示例未生成硬件异常的原因是由于C的整数提升规则。小于int
的操作数会在执行操作之前自动提升为ints
。
至于为什么不同类型的溢出处理方式不同,请考虑在x86机器级别,实际上没有乘法溢出。当您将AX乘以其他寄存器时,结果将进入DX:AX对,因此总是有结果空间,因此没有机会发出溢出异常的信号。但是,在C和其他语言中,两个ints
的乘积应该适合int
,所以在C级有溢出这样的东西。 x86有时在MUL
s上设置OF(溢出标志),但它只是意味着结果的高部分是非零的。
答案 5 :(得分:0)
在具有32位int
的实现上,您的示例不会导致除数溢出。它产生一个完全可表示的int
,32768,然后在进行赋值时以实现定义的方式转换为int16_t
。这是由于C语言指定的默认促销,因此,在此处引发异常的实现将不符合。
如果您想尝试引发异常(实际上可能会或可能不会发生异常,则取决于实现),请尝试:
int a = INT_MIN, b = -1, c = a/b;
您可能需要做一些技巧来阻止编译器在编译时对其进行优化。
答案 6 :(得分:0)
我猜想在某些旧计算机上,尝试除以零会导致一些严重的问题(例如,将硬件置于无穷无尽的循环中,试图减去足够的余量,以便剩余部分小于股息,直到操作员出现为了解决问题,这开始了一种传统,即除了溢出被认为是比整数溢出更严重的错误。
从编程的角度来看,没有理由认为意外的除法溢出应该比意外的整数溢出(有符号或无符号)更严重或更严重。考虑到划分的成本,之后检查溢出标志的边际成本将非常小。传统是我能看到硬件陷阱的唯一原因。