gpt4 book ai didi

c - 为什么整数除以-1(负数)会导致FPE?

转载 作者:行者123 更新时间:2023-11-30 16:12:15 27 4
gpt4 key购买 nike

我的任务是解释一些看似奇怪的C代码行为(在x86上运行)。我可以轻松完成所有其他工作,但是这确实让我感到困惑。


  代码段1输出-2147483648

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






  代码段2不输出任何内容,并给出 Floating point exception

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



我很清楚代码片段1( 1 + ~INT_MIN == INT_MIN)生成结果的原因,但是我不太了解如何将-1除以整数会生成FPE,也无法在Android手机(AArch64,GCC 7.2)上重现它。 0)。代码2的输出与代码1相同,没有任何例外。它是x86处理器的隐藏错误功能吗?

作业没有其他内容(包括CPU体系结构),但是由于整个课程都是基于台式机Linux发行版的,因此您可以放心地假设它是现代x86。



编辑:我联系了我的朋友,他在Ubuntu 16.04(Intel Kaby Lake,GCC 6.3.0)上测试了代码。结果与指定的赋值一致(代码1输出上述内容,代码2因FPE崩溃)。

最佳答案

这里有四件事:


gcc -O0行为说明了两个版本之间的区别。 (而clang -O0恰巧同时使用idiv编译它们)。以及为什么即使使用编译时常数操作数也能得到它。
x86 idiv故障行为与ARM上除法指令的行为
如果整数运算导致传递信号,则POSIX要求它为SIGFPE:On which platforms does integer divide by zero trigger a floating point exception?但是POSIX不需要为任何特定的整数运算而设陷阱。 (这就是为什么x86和ARM可以不同的原因)。

单一Unix规范defines SIGFPE作为“错误的算术运算”。它以浮点数来混淆地命名,但是在FPU处于默认状态的正常系统中,只有整数运算会提高它。在x86上,只有整数除法。在MIPS上,编译器可以使用add instead of addu进行带符号的数学运算,因此您可能会在带符号的添加溢出中获取陷阱。 (gcc uses addu even for signed,但是未定义行为的检测器可能使用add。)
C未定义的行为规则(有符号的溢出和除法)使gcc发出可以在这种情况下捕获的代码。




没有选项的gcc与gcc -O0相同。


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


这说明了两个版本之间的区别:

gcc -O0不仅不尝试优化,还主动进行反优化,以使asm独立实现函数中的每个C语句。这使gdb's jump command安全地工作,使您可以跳到函数内的另一行,并像在C语言源中真正跳来跳去一样。

它也不能假设有关语句之间的变量值,因为您可以使用set b = 4更改变量。这显然对性能造成灾难性的不利影响,这就是为什么-O0代码运行速度比普通代码慢几倍的原因,也是为什么optimizing for -O0 specifically is total nonsense的原因。由于所有存储/重新加载,甚至缺少最明显的优化,它也使-O0 asm输出really noisy and hard for a human to read

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);


我将您的代码放在Godbolt compiler explorer的函数中,以获取这些语句的汇编。

要评估 a/bgcc -O0必须发出代码以从内存中重新加载 ab,并且不对其值进行任何假设。

但是使用 int c = a / -1;时,您无法使用调试器更改 -1,因此gcc可以并且确实以与xcc int c = -a;或AArch64 neg eax指令实现 neg w0, w0相同的方式来实现该语句,被负载(a)/商店(c)包围。在ARM32上,它是一个 rsb r3, r3, #0(反减: r3 = 0 - r3)。

但是,clang5.0 -O0不会执行该优化。它仍然对 idiv使用 a / -1,因此两个版本在使用clang的x86上都会出错。为什么gcc完全“优化”了?请参见 Disable all optimization options in GCC。 gcc总是通过内部表示形式进行转换,而-O0只是生成二进制文件所需的最少工作量。它没有一种“笨拙而文字化”的模式,该模式试图使asm尽可能地类似于源。



x86 idiv与AArch64 sdiv

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商+余数。 As documented in Intel's manualidiv在以下位置引发#DE:


除数= 0
对于目标,签名结果(商)太大。


如果您使用所有除数,例如对于具有单个64b / 32b => 32b划分的 int result = long long / int。但是gcc不能进行这种优化,因为它不允许创建有故障的代码,而不是遵循C整数提升规则并进行64位除法然后截断为 int。它也 doesn't optimize even in cases where the divisor is known to be large enough that it couldn't #DE

当进行32b / 32b划分(使用 cdq)时,唯一可以溢出的输入是 INT_MIN / -1。 “正确”商是一个33位带符号整数,即带前导零符号位的正 0x80000000,使其成为正2的补码带符号整数。由于这不适用于 eax,因此 idiv会引发 #DE异常。然后内核提供 SIGFPE

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]


AFAICT,ARM硬件除法指令不会引发被零除或INT_MIN / -1的异常。或者至少,某些ARM CPU没有。
  divide by zero exception in ARM OMAP3515 processor

AArch64 sdiv documentation没有提及任何例外。

但是,整数除法的软件实现可能会引发: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka4061.html。 (默认情况下,gcc使用库调用在ARM32上进行除法,除非您设置了具有硬件除法的-mcpu。)



C未定义的行为。

PSkocik explains一样, INT_MIN / -1在C中是未定义的行为,就像所有有符号整数溢出一样。这使编译器可以在x86之类的机器上使用硬件划分指令,而无需检查特殊情况。如果不必出错,未知的输入将需要运行时比较和分支检查,并且没人希望C要求这样做。



有关UB后果的更多信息:

启用优化后,编译器可以假定 aba/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;
}


使用xcc-64的 gcc6.3 -O3对此进行编译

x86_fault:
mov eax, -2147483648
ret


但是 clang5.0 -O3编译为(即使使用-Wall -Wextra`也没有警告):

x86_fault:
ret


未定义的行为实际上是完全未定义的。编译器可以按照自己的意愿进行操作,包括在函数条目上返回 eax中的所有垃圾,或加载NULL指针和非法指令。例如对于x86-64,使用gcc6.3 -O3:

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


您使用 -O0的情况没有让编译器在编译时看到UB,因此您获得了“预期”的asm输出。

另请参见 What Every C Programmer Should Know About Undefined Behavior(与Basile链接的同一LLVM博客文章)。

关于c - 为什么整数除以-1(负数)会导致FPE?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58373496/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com