在 gcc linux x86-64 C++ 中,(p+x)-x 是否总是导致指针 p 和整数 x 的 p

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 允许对 void 指针执行指针运算。 (不是你问的扩展。)

因此,在语言标准中描述的相同条件下,gcc 将您询问的指针算法的行为视为未定义。

你可以翻阅那里,看看是否有我遗漏的与你的问题相关的内容。

你不明白 "undefined behavior" 是什么,我不能怪你,因为它经常被解释得不好。这就是标准定义未定义行为的方式,intro.defs:

中的第 3.27 节

behavior for which this document imposes no requirements

就是这样。仅此而已。该标准可以被认为是编译器供应商在生成有效程序时要遵循的一系列约束。当存在未定义的行为时,所有的赌注都会被取消。

有人说未定义的行为会导致您的程序生成龙或重新格式化您的硬盘驱动器,但我觉得这有点像稻草人。更实际的是,像越过数组边界的末端 可以 导致段错误(由于触发页面错误)。有时未定义的行为允许编译器进行优化,以意想不到的方式改变程序的行为,因为没有什么说编译器不能

关键是编译器不是 "generate undefined behavior"。您的程序 中存在未定义的行为。

What I meant is, if GCC has a great feature (specifically, math on invalid pointers) that's not currently named, we can give it a name, and then demand it in future versions too.

那么它将是一个非标准的扩展,人们希望它被记录下来。我也非常怀疑这样的功能是否会有很高的需求,因为它不仅允许人们编写不安全的代码,而且为它生成可移植程序也非常困难。

是的,对于 gcc5.x 以及后来的具体情况,该特定表达式很早就被优化到 p,即使禁用了优化,也不管任何可能的运行时 UB。

即使使用静态数组和 compile-time 常量大小也会发生这种情况。 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 汇编是

# 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] 与 [=27= 是 object 的一部分],所以读取之间的内存/那么远是允许的,不会出现段错误。例如允许 auto-vectorization 搜索循环。

但我怀疑 gcc 甚至会寻找它,更不用说利用它了。

(请注意,您的问题标题是针对 Linux 上针对 x86-64 的 gcc,而不是关于类似的事情在 gcc 中是否安全,例如,如果在单独的语句。我的意思是是的,在实践中可能是安全的,但在解析后几乎不会立即被优化掉。而且绝对不是一般的 C++。)


我强烈建议不要这样做。使用 uintptr_t 保存并非实际有效指针的 pointer-like 值。 就像您在更新 C++ gcc extension for non-zero-based array pointer allocation? 上的答案时所做的那样。