调用look_back_1()或look_back_2()时,下面的示例应该会崩溃。 原因:当否定无符号变量时,结果应保持无符号。
#include <stdio.h>
int look_back_1(int *arr, unsigned int nmElems, unsigned long dist)
{
int *elem = arr + nmElems;
elem += -dist;
return (*elem);
}
int look_back_2(int *arr, unsigned int nmElems, unsigned int dist)
{
int *elem = arr + nmElems;
elem += -dist;
return (*elem);
}
int main(int argc, char **argv)
{
int arr[100] = { 0, };
printf("1. %d\n", look_back_1(arr, 100, 1)); // <NEEDS TO CRASH, BUT WORKS????>>
printf("2. %d\n", look_back_2(arr, 100, 1)); // <<CRASH!!!!!>>
}
当进行数组越界访问时,GCC 4.5在每个函数调用中崩溃。 编译器为这两种情况发出NEG操作码。
GCC 6.1或Clang只会在调用int版本时崩溃。 但是当它们为unsigned long版本发出SUB操作码时,它们都避免了崩溃。
他们是否允许这样做?
答案 0 :(得分:4)
[编辑]这是问题的上一版本的答案,它显示了使用参数dist==1
调用这些函数时的问题
-(unsigned long)1
定义明确并且包装好。它只是ULONG_MAX
。出于同样的原因,-(unsigned int)
为UINT_MAX
。
数组边界外的指针运算会导致未定义的行为,因此GCC忽略这种可能性是完全合理的。例如,它们可以将x64上的指针视为带有环绕的64位整数。将64位ULONG_MAX
添加到带有环绕的64位指针只会将指针减少-1,这就是环绕的工作方式。在UINT_MAX
附近添加32位int[100]
点。
因此,您看到的行为是未定义行为的完全有效后果。然而,它完全不可靠。优化器可能知道您不能添加超过数组中允许的最大元素数(在64位平台上4字节整数将为2 ^ 62),并从那里做出假设。
答案 1 :(得分:0)
看看你的“godbolt”拆卸,差异很容易。您正在编译一个原生64位的平台,unsigned int
32位和unsigned long
64位。也就是说,数学本身是模2 ^ 64,它与unsigned long
的行为完全匹配。但是对于unsigned int
,需要一条额外的指令。这是从32位寄存器到其自身(!)的细微MOV
指令。这个指示的原因是什么?它清除64位结果的高32位,这就是“模2 ^ 32”行为所需的。
这很有效,也很聪明。对于展示未定义行为的代码,它可能会给出意想不到的结果,但无论如何你都不应该对这些案例抱有期望。