[英]Does (p+x)-x always result in p for pointer p and integer x in gcc linux x86-64 C++
假设我们有:
char* p;
int x;
正如最近在另一个问题中讨论的那样,包括对无效指针的比较操作在内的算术可能会在 gcc linux x86-64 C++ 中产生意外行为。 这个新问题专门针对表达式(p+x)-x
:它能否在 x86-64 linux 上运行的任何现有 GCC 版本中产生意外行为(即结果不是p
)?
请注意,这个问题只是关于指针算法; 绝对无意访问*(p+x)
指定的位置,这显然通常是不可预测的。
这里的实际兴趣是非零基数组。 需要注意的是(p+x)
并通过减法x
在这些应用程序中的代码不同的地方发生。
如果可以证明 x86-64 上的最新 GCC 版本永远不会为(p+x)-x
产生意外行为,那么这些版本可以针对非零数组进行认证,并且可以修改或配置产生意外行为的未来版本以支持这个认证。
更新
对于上面描述的实际情况,我们还可以假设p
本身是一个有效的指针并且p != NULL
。
这是 gcc 扩展的列表。 https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html
指针算术有一个扩展。 Gcc 允许对空指针执行指针运算。 (不是您要问的扩展名。)
因此,在与语言标准中描述的相同条件下,gcc 将您所询问的指针算术的行为视为未定义。
您可以查看那里,看看我是否遗漏了与您的问题相关的任何内容。
你不明白什么是“未定义的行为”,我不能责怪你,因为它经常被解释得很差。 这是标准定义未定义行为的方式,intro.defs 中的第 3.27 节:
本文件不强加任何要求的行为
就是这样。 不多不少,不多不少。 该标准可以被认为是编译器供应商在生成有效程序时要遵循的一系列约束。 当存在未定义的行为时,所有赌注都将关闭。
有些人说未定义的行为会导致你的程序产生龙或重新格式化你的硬盘,但我觉得这有点像稻草人。 更现实的是,诸如越过数组边界之类的事情可能会导致段错误(由于触发页面错误)。 有时未定义的行为允许编译器进行优化,以意想不到的方式改变程序的行为,因为没有什么说编译器不能。
关键是编译器不会“生成未定义的行为”。 未定义行为存在于你的程序。
我的意思是,如果 GCC 有一个很棒的功能(特别是无效指针的数学运算)但目前没有命名,我们可以给它一个名字,然后在未来的版本中也需要它。
那么它将是一个非标准的扩展,人们会期望它被记录下来。 我也非常怀疑这样的功能是否会有很高的需求,因为它不仅允许人们编写不安全的代码,而且生成可移植程序也极其困难。
是的,对于 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;
}
这是来自带有-O0
的 gcc8.3 的 Godbolt 编译器资源管理器。
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]
是同一对象的一部分,因此读取内存之间/出那么远是允许的,并且赢了t 段错误。 例如,允许搜索循环的自动矢量化。
但我怀疑 gcc 甚至会寻找它,更不用说利用它了。
(请注意,您的问题标题特定于针对 Linux 上 x86-64 的 gcc,而不是关于gcc 中类似的事情是否安全,例如,如果在单独的语句中完成。我的意思是在实践中可能是安全的,但几乎不会被优化掉解析后立即。而且绝对不是一般的 C++。)
我强烈建议不要这样做。 使用uintptr_t
来保存不是实际有效指针的类似指针的值。 就像你在更新你对基于非零的数组指针分配的 C++ gcc 扩展的答案所做的那样? .
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.