关于C ++:导致除法溢出错误(x86)

Causing a divide overflow error (x86)

我有一些关于x86或x86_64体系结构上的划分溢出错误的问题。最近,我一直在阅读有关整数溢出的知识。通常,当算术运算导致整数溢出时,将置位FLAGS寄存器中的进位或溢出位。但是很显然,根据本文所述,除法运算导致的溢出不会设置溢出位,而是会触发硬件异常,类似于将其除以零时的情况。

现在,除法导致的整数溢出比乘法要少得多。只有几种方法可以触发除法溢出。一种方法是做类似的事情:

1
2
3
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内联汇编中执行此操作,看看会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
"
, b);

这只是输出一个错误值:-32768。即使存储商数(AX)的寄存器太小而无法容纳商数,仍然没有硬件异常。所以我不明白为什么这里没有出现硬件故障。


在C语言中,永远不会在小于int的类型内执行算术运算。每当您尝试对较小的操作数进行算术运算时,它们都会首先进行积分提升,然后将其转换为int。如果您的平台上的int是32位宽,则无法强制C程序执行16位除法。编译器将改为生成32位除法。这可能是为什么您的C实验未在除法上产生预期的溢出的原因。如果您的平台确实有32位int,那么最好的选择是尝试使用32位操作数进行相同的操作(即将INT_MIN除以-1)。我非常确定,即使在C代码中,您最终也将能够重现溢出异常。

在汇编代码中,由于将BX指定为idiv的操作数,因此您使用16位除法。 x86上的16位除法将DX:AX对中存储的32位除数除以idiv操作数。这就是您在代码中所做的事情。 DX:AX对被解释为一个32位复合寄存器,这意味着该对中的符号位实际上是DX的最高位。 AX的最高位不再是符号位。

您对DX做了什么?您只需清除它。您将其设置为0。但是DX设置为0时,您的股息被解释为正数!从机器角度来看,这样的DX:AX对实际上表示正值+32768。即在您的汇编语言实验中,您将+32768除以-1。结果应该是-32768。这里没什么异常。

如果要在DX:AX对中表示-32768,则必须对其进行符号扩展,即必须用全1的位模式而不是0填充DX。您应该先用-32768初始化AX,然后再完成cwd,而不是执行xor DX, DX。那会将AX的符号扩展为DX

例如,在我的实验(不是GCC)中,此代码

1
2
3
4
5
6
__asm  {
  mov AX, -32768
  cwd
  mov BX, -1
  idiv BX
}

导致预期的异常,因为它确实试图将-32768除以-1


当您得到带有整数2的补码加/减/乘的整数溢出时,您仍然可以获得有效的结果-它只是缺少一些高阶位。此行为通常很有用,因此不适合为此生成异常。

但是,对于整数除法,除以零的结果是无用的(因为与浮点数不同,2的补码整数没有INF表示)。


在有关整数溢出的相关部分中:

Unlike the add, mul, and imul
instructions, the Intel division
instructions div and idiv do not set
the overflow flag; they generate a
division error if the source operand
(divisor) is zero or if the quotient
is too large for the designated
register.

在现代平台上,寄存器的大小为32位或64位; 32768将适合这些寄存器之一。但是,以下代码很可能会引发整数溢出执行(它在VC8的我的双核笔记本电脑上执行):

1
2
3
int x= INT_MIN;
int y= -1;
int z= x/y;

Contrary to what this article says, the above did NOT cause a hardware exception

文章没有这么说。是说

... they generate a division error if the source operand (divisor) is zero or if the quotient is too large for the designated register

寄存器大小肯定大于16位(32 || 64)


我猜想在某些旧计算机上,尝试被零除会导致一些严重的问题(例如,将硬件置于一个无穷循环中,试图进行足够的减法,以便在操作员进行修复之前,剩余的余数小于红利。 ),这开始了除数溢出的传统,被认为比整数溢出更严重的故障。

从编程的角度来看,没有理由认为意外的除法溢出应该比意外的整数溢出(有符号或无符号)更严重。考虑到划分的成本,事后检查溢出标志的边际成本将很小。传统是我看到具有硬件陷阱的唯一原因。


在使用32位int的实现中,您的示例不会导致除法溢出。它产生一个完美可表示的int 32768,然后在进行分配时以实现定义的方式将其转换为int16_t。这是由于C语言指定的默认提升,因此,在此处引发异常的实现将不符合要求。

如果您想尝试引发异常(实际上仍然可能会发生或可能不会发生,这取决于实现),请尝试:

1
int a = INT_MIN, b = -1, c = a/b;

您可能需要做一些技巧,以防止编译器在编译时对其进行优化。


  • 您的示例未生成硬件异常的原因是由于C的整数提升规则。小于int的操作数将在执行操作之前自动提升为ints

  • 至于为什么对不同类型的溢出进行不同处理的原因,请考虑在x86机器级别上,确实没有乘法溢出的事情。当您将AX与其他寄存器相乘时,结果进入DX:AX对,因此结果始终有空间,因此没有机会发出溢出异常信号。但是,在C语言和其他语言中,两个ints的乘积应该适合int,因此在C级别会发生溢出。 x86有时会在MUL上设置OF(溢出标志),但这仅意味着结果的大部分不是零。