realloc(),生命周期和UB

Ale*_*nze 5 c compiler-optimization undefined-behavior language-lawyer

有一个最近的CppCon2016谈话我的小优化器:未定义的行为是魔术,它显示以下代码(谈话中的26分钟).我美化了一下:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  int* p = malloc(sizeof(int));
  int* q = realloc(p, sizeof(int));
  *p = 1;
  *q = 2;
  if (p == q)
  {
    printf("%d %d\n", *p, *q);
  }
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

代码具有未定义的行为(即使realloc()返回相同的指针,p在realloc()之后变为无效)并且在编译时不仅可以打印"2 2",还可以打印"1 2".

稍微修改过的代码版本怎么样?:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main(void)
{
  int* p = malloc(sizeof(int));
  uintptr_t ap = (uintptr_t)p;
  int* q = realloc(p, sizeof(int));
  *(int*)ap = 1;
  *q = 2;
  if ((int*)ap == q)
  {
    printf("%d %d\n", *(int*)ap, *q);
  }
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

为什么我还能打印"1 2"?整数变量ap是否也会以某种方式变得无效或"污染"?如果是这样,这里的逻辑是什么?不应该ap与p"脱钩"吗?

PS添加了C++标签.这段代码可以简单地重写为C++,同样的问题也适用于C++.我对C和C++都很感兴趣.

M.M*_*M.M 5

发布时,在C中,代码具有未定义的行为,因为realloc可能返回不同的内存块.在这种情况下,*(int *)ap将形成无效指针.

一个更有趣的问题是,如果我们更改代码,只有在realloc没有更改块时它才会尝试继续,会发生什么:

int* p = malloc(sizeof(int));
uintptr_t ap = (uintptr_t)p;
int* q = realloc(p, sizeof(int));

if ( (uintptr_t)q == ap )
{
    *(int*)ap = 1;
    // ...
}
Run Code Online (Sandbox Code Playgroud)

对于C2X,有一个建议N2090在通过整数类型时指定指针出处.

在当前的C标准中,有一些与指针起源相关的规则,但它没有说明当指针通过整数类型并返回时,起源会发生什么.

根据此提议,我的代码仍然是未定义的行为:ap获取与p原来相同的原始令牌,当块被释放时,它成为无效令牌. (int *)ap然后使用带有无效出处的指针.

该提案旨在避免指针来源被中间操作等"侵入" uintptr_t.在这种情况下,它指定(int *)ap具有与之完全相同的行为p.(即使块没有移动,也是未定义的,因为p在它realloc是否物理移动块之后是无效指针).在C抽象机器中,意图是无法通过realloc判断块是否被移动.

指针出处的背景

"指针起源"表示指针值与它们指向的内存块之间的关联.如果指针值指向一个对象,那么从该值派生的其他指针值(例如通过指针算术)必须保持在该对象的边界内.

(当然,指针变量可以被重新分配以指向不同的对象 - 从而获得新的出处 - 这不是我们所说的).

这不是出现在已编译的可执行文件中的内容,而是编译器在编译期间可能会跟踪的内容,以便执行优化.具有不同种源的两个指针可能具有相同的存储器表示(例如,p并且q在实现使用相同的物理存储器块的情况下).

指针来源提供有用优化机会的一个简单示例如下:

char p[8];
int q = 5;

*(p+10) = 123;
printf("%d\n", q);
Run Code Online (Sandbox Code Playgroud)

出处的想法允许优化器在代码上注册未定义的行为p + 10,因此它可以将此片段转换puts("5")为例如,即使q恰好紧跟p在内存中.(旁白 - 我想知道DJ Bernstein的boringcc编译器实际上是否无法执行此优化).

关于指针边界检查的现有规则(C11 6.5.6/8)确实涵盖了这种情况,但在更复杂的情况下它们不清楚,因此N2090提案.例如,if ( p + 8 == (void *)&q ) *(char *)((uintptr_t)p + 10) = 123;在N2090下,仍然是未定义的行为.


Ale*_*nze 0

最新的 C 标准使这个问题变得模糊。N2090指出DR260委员会的回应

没有被纳入标准文本中,而且还留下了许多具体问题不清楚......

因此,可以合理地假设实际上存在未定义的行为,即使标准本身没有明确记录它。