是的,特别是对于 gcc5.x 及更高版本,该特定表达式很早就优化为 p,即使禁用了优化,也不管任何可能的运行时 UB。
即使使用静态数组和编译时常量大小也会发生这种情况。 gcc -fsanitize=undefined 也没有插入任何工具来查找它。 -Wall -Wextra -Wpedantic也没有警告
int *add(int *p, long long x) {
return (p+x) - x;
}
int *visible_UB(void) {
static int arr[100];
return (arr+200) - 200;
}
在任何优化通过之前使用gcc -dump-tree-original 转储其程序逻辑的内部表示表明这种优化甚至发生在在gcc5.x 和更新版本之前。 (甚至发生在-O0)。
;; Function int* add(int*, long long int) (null)
;; enabled by -tree-original
return <retval> = p;
;; Function int* visible_UB() (null)
;; enabled by -tree-original
{
static int arr[100];
static int arr[100];
return <retval> = (int *) &arr;
}
那是from the Godbolt compiler explorer with gcc8.3 和-O0。
x86-64 asm 输出只是:
; g++8.3 -O0
add(int*, long long):
mov QWORD PTR [rsp-8], rdi
mov QWORD PTR [rsp-16], rsi # spill args
mov rax, QWORD PTR [rsp-8] # reload only the pointer
ret
visible_UB():
mov eax, OFFSET FLAT:_ZZ10visible_UBvE3arr
ret
-O3 输出当然只是mov rax, rdi
gcc4.9 及更早版本仅在稍后的通道中进行此优化,而不是在 -O0:树转储仍然包括减法,而 x86-64 asm 是
# g++4.9.4 -O0
add(int*, long long):
mov QWORD PTR [rsp-8], rdi
mov QWORD PTR [rsp-16], rsi
mov rax, QWORD PTR [rsp-16]
lea rdx, [0+rax*4] # RDX = x*4 = x*sizeof(int)
mov rax, QWORD PTR [rsp-16]
sal rax, 2
neg rax # RAX = -(x*4)
add rdx, rax # RDX = x*4 + (-(x*4)) = 0
mov rax, QWORD PTR [rsp-8]
add rax, rdx # p += x + (-x)
ret
visible_UB(): # but constants still optimize away at -O0
mov eax, OFFSET FLAT:_ZZ10visible_UBvE3arr
ret
这确实与-fdump-tree-original 输出一致:
return <retval> = p + ((sizetype) ((long unsigned int) x * 4) + -(sizetype) ((long unsigned int) x * 4));
如果x*4 溢出,您仍然会得到正确的答案。在实践中,我想不出一种方法来编写一个会导致 UB 导致行为发生明显变化的函数。
作为更大函数的一部分,允许编译器推断一些范围信息,例如 p[x] 与 p[0] 属于同一对象,因此在 / 之间读取内存允许并且不会出现段错误。例如允许搜索循环的自动矢量化。
但我怀疑 gcc 甚至会寻找它,更不用说利用它了。
(请注意,您的问题标题是特定于 gcc 在 Linux 上针对 x86-64,不是关于 gcc 中类似的事情是否安全,例如,如果在单独的语句中完成。我的意思是可能是安全的练习,但不会在解析后立即被优化掉。而且绝对不是一般的 C++。)
我强烈建议不要这样做。使用uintptr_t 来保存不是实际有效指针的类似指针的值。就像您在更新C++ gcc extension for non-zero-based array pointer allocation? 上的答案时所做的那样。