iBu*_*Bug 8 c x86 gcc x86-64 floating-point-exceptions
我的任务是表达一些看似奇怪的C代码行为(在x86上运行).我可以很容易地完成其他所有事情,但这个让我很困惑.
代码段1输出
-2147483648Run Code Online (Sandbox Code Playgroud)int a = 0x80000000; int b = a / -1; printf("%d\n", b);
代码片段2没有输出任何内容,并给出了一个
Floating point exceptionRun Code Online (Sandbox Code Playgroud)int a = 0x80000000; int b = -1; int c = a / b; printf("%d\n", c);
我很清楚Code Snippet 1(1 + ~INT_MIN == INT_MIN)的结果的原因,但是我不太明白整数除以-1如何生成FPE,也不能在我的Android手机(AArch64,GCC 7.2.0)上重现它.代码2只输出与代码1相同,没有任何例外.它是x86处理器的隐藏bug功能吗?
该任务没有告诉任何其他内容(包括CPU架构),但由于整个课程基于桌面Linux发行版,您可以放心地认为它是一个现代的x86.
编辑:我联系了我的朋友,他在Ubuntu 16.04(Intel Kaby Lake,GCC 6.3.0)上测试了代码.结果与所指定的任何内容一致(代码1输出所述内容,代码2与FPE崩溃).
Pet*_*des 12
这里有四件事:
gcc -O0行为解释了两个版本之间的区别.(虽然clang -O0恰好用它们编译它们idiv).为什么即使使用编译时常量操作数也能得到它.idiv故障行为与ARM上除法指令的行为如果整数数学导致信号被传递,POSIX要求它是SIGFPE:在哪个平台上整数除以零会触发浮点异常? 但是,POSIX 并不需要捕捉任何特定整数操作.(这就是允许x86和ARM不同的原因).
单Unix规范将SIGFPE定义为"错误的算术运算".它在浮点后以混乱的方式命名,但在FPU处于默认状态的普通系统中,只有整数数学会引发它.在x86上,只有整数除法.在MIPS上,编译器可以使用add而不是addu用于带符号的数学运算,因此您可以在签名的添加溢出上获取陷阱.(gcc addu甚至用于签名,但未定义的行为检测器可能会使用add.)
gcc没有选项是一样的gcc -O0.
-O0减少编译时间并使调试产生预期的结果.这是默认值.
这解释了两个版本之间的区别:
不仅不gcc -O0尝试优化,它还主动去优化以使asm独立地实现函数内的每个C语句.这样gdb的jump命令安全工作,让你跳转到函数中不同的线路和行为像你真的在C源跳来跳去.
它也不能假设语句之间的变量值,因为你可以用变量来改变变量set b = 4.这对性能来说显然是灾难性的,这就是为什么-O0代码比普通代码运行慢几倍,为什么特别优化-O0是完全无意义的原因.由于所有存储/重新加载,甚至缺乏最明显的优化,它也使得-O0asm输出真的很嘈杂,人类难以阅读.
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);
Run Code Online (Sandbox Code Playgroud)
我把你的代码放在Godbolt 编译器资源管理器的函数中,以获取这些语句的asm.
要评估a/b,gcc -O0必须发出代码来重新加载a和b从内存中,而不是对它们的值做任何假设.
但有int c = a / -1;,你不能改变-1一个调试器,以使gcc可以和确实实现这一声明将实施同样的方式int c = -a;,用在x86 neg eax或AArch64 neg w0, w0指令,通过负载(A)/存储(C)包围.在ARM32上,它是一个rsb r3, r3, #0(反向减法:) r3 = 0 - r3.
但是,clang5.0 -O0没有做那个优化.它仍然idiv用于a / -1,因此两个版本都将在x86上使用clang进行故障.为什么gcc会"优化"?请参阅在GCC中禁用所有优化选项.gcc总是通过内部表示进行转换,而-O0只是生成二进制文件所需的最小工作量.它没有"愚蠢和文字"模式,试图尽可能地使asm像源一样.
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
Run Code Online (Sandbox Code Playgroud)
不同的是imul r32,r32,没有2操作数idiv没有红利上半部分输入.无论如何,这并不重要; gcc只使用edx符号位的=副本eax,所以它实际上是32b/32b => 32b商+余数. 如英特尔手册中所述,idiv引发#DE:
如果使用全范围的除数,例如int result = long long / int单个64b/32b => 32b除法,则很容易发生溢出.但gcc不能进行优化,因为它不允许生成错误的代码,而不是遵循C整数提升规则并进行64位除法然后截断为int.即使在已知除数足够大而无法除数的情况下,它也不会优化#DE
当进行32b/32b除法(with 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]
Run Code Online (Sandbox Code Playgroud)
AFAICT,ARM硬件除法指令不会引发除以零或INT_MIN/-1的异常.或者至少,一些 ARM CPU没有. 在ARM OMAP3515处理器中除以零异常
AArch64 sdiv文档没有提到任何例外.
但是,整数除法的软件实现可能会提高:http://infocenter.arm.com/help/index.jsp?topic =/com.arm.doc.faqs/ka4061.html.(默认情况下,gcc在ARM32上使用库调用进行除法,除非您设置了具有HW除法的-mcpu.)
正如PSkocik所解释的那样,INT_MIN/ -1是C中未定义的行为,就像所有有符号整数溢出一样. 这允许编译器在x86等机器上使用硬件除法指令,而无需检查特殊情况. 如果它不是故障,未知输入将需要运行时比较和分支检查,没有人希望C要求它.
更多关于UB的后果:
启用优化,编译器可以假定a,并b仍然有其设定值时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;
}
Run Code Online (Sandbox Code Playgroud)
使用gcc6.3 -O3for x86-64 编译到此
x86_fault:
mov eax, -2147483648
ret
Run Code Online (Sandbox Code Playgroud)
但clang5.0 -O3编译成(即使使用-Wall -Wextra`也没有警告):
x86_fault:
ret
Run Code Online (Sandbox Code Playgroud)
未定义的行为确实是完全未定义的.编译器可以做任何他们想做的事情,包括返回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
Run Code Online (Sandbox Code Playgroud)
您的情况-O0并没有让编译器在编译时看到UB,因此您得到了"预期的"asm输出.
另请参阅每个C程序员应该了解的关于未定义行为的内容(Basile链接的相同LLVM博客文章).
int如果符合以下条件,则在两个补码中的签名除法是不确定的
INT_MIN(== 0x80000000如果int是int32_t),除数是-1(在二进制补码中,
-INT_MIN > INT_MAX导致整数溢出,这是C中未定义的行为)(https://www.securecoding.cert.org建议在检查此类边缘情况的函数中包装整数运算)
由于您通过违反规则2来调用未定义的行为,因此任何事情都可能发生,并且当您发生这种情况时,您平台上的这一特定事件恰好是由处理器生成的FPE信号.
| 归档时间: |
|
| 查看次数: |
1188 次 |
| 最近记录: |