导致除法溢出错误 (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 有时会在 MULs 上设置 OF(溢出标志),但这只是意味着结果的高位部分不为零。

【讨论】:

80386 乘以常数指令使用与源相同大小的单个目标寄存器。他们设置标志以防溢出,代码可能会或可能不会忽略。【参考方案6】:

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

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

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

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

【讨论】:

【参考方案7】:

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

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

【讨论】:

以上是关于导致除法溢出错误 (x86)的主要内容,如果未能解决你的问题,请参考以下文章

在 x86 汇编中使用整数除法处理百分比

假设分母<>0,整数除法是不是会溢出/下溢? [复制]

实验10 编写子程序 2.解决除法溢出的问题

Python3 大浮点数的停止除法

连续截断整数除法可以用乘法代替吗?

8086汇编 中断