9

我有几个关于 x86 或 x86_64 架构上的除法溢出错误的问题。最近我一直在阅读有关整数溢出的文章。通常,当算术运算导致整数溢出时,FLAGS 寄存器中的进位位或溢出位被置位。但显然,根据这篇文章,除法运算导致的溢出不会设置溢出位,而是触发硬件异常,类似于除以零时。

现在,除法导致的整数溢出比乘法少得多。只有几种方法可以触发除法溢出。一种方法是执行以下操作:

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) 的寄存器太小而无法容纳商。所以我不明白为什么这里没有引发硬件故障。

4

7 回答 7

17

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


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

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

如果你想-32768DX:AX对中表示,你必须对它进行符号扩展,即你必须DX用全一位模式填充,而不是零。而不是这样做xor DX, DX,你应该AX用 your初始化-32768然后 done cwd。那将符号扩展AXDX.

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

__asm  {
  mov AX, -32768
  cwd
  mov BX, -1
  idiv BX
}

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

于 2010-10-08T16:47:56.193 回答
2

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

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

于 2010-10-08T16:20:59.227 回答
1

与本文所说的相反,上述内容并未导致硬件异常

文章没有这么说。是说

...如果源操作数(除数)为零或商对于指定寄存器来说太大,它们会产生除法错误

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

于 2010-10-08T16:30:09.110 回答
1

从有关整数溢出的相关部分

与 add、mul 和 imul 指令不同,Intel 除法指令 div 和 idiv 不设置溢出标志;如果源操作数(除数)为零或商对于指定的寄存器来说太大,它们会产生除法错误。

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

int x= INT_MIN;
int y= -1;
int z= x/y;
于 2010-10-08T16:34:39.613 回答
0
  1. 您的示例未生成硬件异常的原因是由于 C 的整数提升规则。在执行操作之前,小于int自动提升ints的操作数。

  2. 至于为什么不同类型的溢出处理方式不同,请考虑在 x86 机器级别,实际上没有乘法溢出这样的事情。当您将 AX 乘以某个其他寄存器时,结果进入 DX:AX 对,因此结果总是有空间,因此没有机会发出溢出异常信号。但是,在 C 和其他语言中,两个的乘积ints应该适合一个int,因此在 C 级别存在溢出这样的事情。x86 有时会在 s 上设置 OF(溢出标志)MUL,但这只是意味着结果的高位部分非零。

于 2010-10-08T16:54:26.213 回答
0

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

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

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

您可能需要采取一些技巧来防止编译器在编译时对其进行优化。

于 2010-10-08T17:31:01.367 回答
0

我猜想在一些旧计算机上,试图除以零会导致一些严重的问题(例如,将硬件置于一个无限循环中,试图减去足够多的值,因此在操作员出现解决问题之前,余数会小于被除数),这开始了除法溢出被视为比整数溢出更严重的错误的传统。

从编程的角度来看,意外的除法溢出没有理由比意外的整数溢出(有符号或无符号)更严重或更严重。考虑到除法的成本,之后检查溢出标志的边际成本将非常小。传统是我能看到存在硬件陷阱的唯一原因。

于 2010-10-08T18:01:59.840 回答