当前位置: 首页 > 知识库问答 >
问题:

为什么整数除以-1(负1)会产生FPE?

温星华
2023-03-14

我有一项任务,阐述C代码(在x86上运行)的一些看似奇怪的行为。我可以很容易地完成其他一切,但这件事真的让我困惑。

代码段1输出-2147483648

int a = 0x80000000;
int b = a / -1;
printf("%d\n", b);
int a = 0x80000000;
int b = -1;
int c = a / b;
printf("%d\n", c);

共有1个答案

阴凯歌
2023-03-14

这里发生了四件事:

>

  • GCC-O0行为解释了您的两个版本之间的差异:IDIVNEG。(而clang-o0恰好使用idiv编译它们)。以及为什么即使使用编译时常量操作数也能得到这个结果。

    ARM上x86IDIV错误行为与除法指令行为的比较

    -O0减少编译时间并使调试产生预期结果。这是默认值。

    这解释了您的两个版本之间的差异:

    gcc-o0不仅不尝试优化,而且主动地去优化,以生成在函数中独立实现每个C语句的asm。这允许gdbjump命令安全地工作,使您可以跳转到函数中的另一行,并像在C源代码中跳转一样。为什么clang用-O0生成低效的asm(对于这个简单的浮点和)?详细说明-o0如何编译以及为什么编译。

    int a = 0x80000000;
    int b = -1;
      // debugger can stop here on a breakpoint and modify b.
    int c = a / b;        // a and b have to be treated as runtime variables, not constants.
    printf("%d\n", c);
    

    但是,Clang5.0-O0不进行这种优化。它仍然使用idiv来表示a/-1,因此这两个版本在x86上都会出现clang错误。海合会为什么要“优化”?请参见禁用GCC中的所有优化选项。gcc总是通过内部表示进行转换,而-o0只是生成二进制文件所需的最小工作量。它没有试图使asm尽可能像源代码的“哑巴和文字”模式。

    x86-64:

        # int c = a / b  from x86_fault()
        mov     eax, DWORD PTR [rbp-4]
        cdq                                 # dividend sign-extended into edx:eax
        idiv    DWORD PTR [rbp-8]           # divisor from memory
        mov     DWORD PTR [rbp-12], eax     # store quotient
    

    IMUL r32、r32不同,没有2个操作数IDIV没有红利的上半部分输入。不管怎样,这并不重要;gcc只在edx=eax中符号位的副本中使用它,所以它实际上是在做32b/32b=>32b商+余数。正如Intel手册中所述,IDIV引发#de on:

      null

    AARCH64:

        # int c = a / b  from x86_fault()  (which doesn't fault on AArch64)
        ldr     w1, [sp, 12]
        ldr     w0, [sp, 8]          # 32-bit loads into 32-bit registers
        sdiv    w0, w1, w0           # 32 / 32 => 32 bit signed division
        str     w0, [sp, 4]
    

    ARM硬件除法指令不会引发被零除或int_min/-1溢出的异常。内特·埃尔德里奇评论道:

    完整的ARM体系结构参考手册指出,UDIV或SDIV在被零除时,只返回零作为结果,“没有任何被零除发生的迹象”(Armv8-A版本中的C3.4.8)。没有异常,也没有标志--如果您想要捕获除零,您必须编写一个显式测试。同样,int_min-1带符号的除法返回int_min,但没有溢出指示。

    启用优化后,编译器可以假设ab在运行a/b时仍然具有它们的设置值。然后它可以看到程序有未定义的行为,因此可以做任何它想做的事情。gcc选择生成int_min,就像从-int_min生成一样。

    在2的补系统中,最负的数是它自己的负数。对于2的补码来说,这是一个令人讨厌的拐角处,因为它意味着abs(x)仍然可以是负数。https://en.wikipedia.org/wiki/two%27s_complement#most_negative_number

    int x86_fault() {
        int a = 0x80000000;
        int b = -1;
        int c = a / b;
        return c;
    }
    

    使用gcc6.3-o3对x86-64进行编译

    x86_fault:
        mov     eax, -2147483648
        ret
    
    x86_fault:
        ret
    
    int *local_address(int a) {
        return &a;
    }
    
    local_address:
        xor     eax, eax     # return 0
        ret
    
    void foo() {
        int *p = local_address(4);
        *p = 2;
    }
    
     foo:
       mov     DWORD PTR ds:0, 0     # store immediate 0 into absolute address 0
       ud2                           # illegal instruction
    

    还请参见每个C程序员应该了解的关于未定义行为的内容(与Basile链接的LLVM博客文章相同)。

  •  类似资料:
    • 问题内容: 我对此很好奇: 使用Java进行评估时,会发生以下异常: 线程“主”中的异常java.lang.ArithmeticException:/在Foo.main(Foo.java:3)处为零 但是被评估为。 为什么会这样? 问题答案: 这是因为整数没有+/- Inf,NaN的值,并且不允许除以0,而浮点数确实具有这些特殊值。

    • 我观察到库函数,当它在循环中被调用一次时,它几乎总是产生正数。

    • 在测试CRC实现时,我注意到0x01的CRC通常(?)似乎是多项式本身。然而,当试图手动进行二进制长除法时,我总是以丢失多项式的前导“1”告终,例如,当消息为“0x01”和多项式“0x1021”时,我将得到 通过查看https://en.wikipedia.org/wiki/computation_of_cyclic_redundancy_checks,我可以看到使用生成多项式离开移位寄存器的高位

    • 为什么这个程序的输出是而不是 当你除以3/2,它等于1.5,我认为Java只取整数的第一个值。发生什么事?

    • 问题内容: -1作为转换为二进制的int表示为321。当我右移31次时,得到1(31个0和一个1)。但是当我右移32次时,我又得到-1。它不应该等于0吗? 问题答案: Java规范对移位运算符的解释如下: 如果左侧操作数的提升类型为,则仅将右侧操作数的最低5位用作移位距离。就像右手操作数受到掩码值的按位逻辑AND运算符(第15.22.1节)一样。因此,实际使用的移动距离始终在0到31(含)范围内。

    • 问题内容: 我想知道这是否是JVM错误? Java版本“ 1.6.0_0” OpenJDK运行时环境(IcedTea6 1.4.1)(6b14-1.4.1-0ubuntu13)OpenJDK 64位服务器VM(内部版本14.0-b08,混合模式) 当我运行它时会产生这个: 对于32的任意倍数,我也得到相同的结果。 我需要编写自己的右移来检查吗? 问题答案: http://docs.oracle.c