realloc(),生命周期和UB

时间:2016-10-15 23:37:08

标签: c language-lawyer compiler-optimization undefined-behavior

最近有一个CppCon2016讲话My Little Optimizer: Undefined Behavior is Magic,它显示了以下代码(讲话后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;
}

代码具有未定义的行为(即使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;
}

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

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

4 个答案:

答案 0 :(得分: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;
    // ...
}

对于C2X,当通过整数类型传递时,a proposal N2090指定指针起源

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

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

该提案旨在避免指针来源被uintptr_t等中间操作“黑客攻击”。在这种情况下,它指定(int *)app具有完全相同的行为。 (即使块没有移动,这也是未定义的,因为prealloc之后的无效指针,无论它是否物理移动了块)。在C抽象机器中,目的是无法通过realloc判断块是否被移动。

指针出处的背景

“Pointer originance”表示指针值与它们指向的内存块之间的关联。如果指针值指向一个对象,则从该值导出的其他指针值(例如通过指针算术)必须保持在该对象的边界内。

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

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

为什么指针起源提供有用的优化机会的一个简单示例将是以下代码段:

char p[8];
int q = 5;

*(p+10) = 123;
printf("%d\n", q);

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

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

答案 1 :(得分:0)

最新的C标准使问题模棱两可。 N2090表明DR260委员会的回复

  

尚未纳入标准文本,并且还留下许多具体问题不清楚......

因此,假设事实上存在未定义的行为是合理的,即使它没有在标准中明确记录。

答案 2 :(得分:0)

原始问题中给出的代码调用Undefined Behavior,因此编译器有权做任何想做的事情。关于这种形式的未定义行为的一些背景如下。

但是,Clang会表现得很奇怪,因为代码与你的代码类似但不会调用未定义的行为。除非有人认为标准中的某些语言毫无意义,否则clang在这方面似乎不符合要求。有些人想改变标准以使下面的内容调用UB,从而证明clang的行为是合理的,但我认为这些提议基本上是错误的。

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

uintptr_t gap,gaq;
int test(void)
{
  int x=0;
  uint8_t *p = calloc(4,1);
  uintptr_t ap = (uintptr_t)p;
  uint8_t *q = realloc(p,4);
  // p is no longer valid after this, but ap still holds some number.
  uintptr_t aq = (uintptr_t)q;
  *q=1;
  if (ap == aq)
  {
    x=256;
    // Nothing in the Standard would say that the result of casting a
    // uintptr_t to a pointer is affected by anything other than the
    // numerical value of the uintptr_t in question.  If aq happened
    // to equal e.g. 8675309, then casting any expression equal to
    // 8675309 into an int* should yield the same value as casting aq;
    // since were here, we'd know that ap was also equal to 8675309, and
    // thus that (int*)ap is equivalent to (int*)aq.
    *(uint8_t*)ap = 123;
  }
  gap=ap;
  gaq=aq;
  return *q+x;
}

使用选项-xc -O3 -pedantic在godbolt上调用的Clang 3.9.0生成的代码将返回1或257,具体取决于apaq是否相等,即使本标准中没有任何内容允许apuintptr_t类型的任何其他变量保持相同值的处理方式不同。编写代码的方式,因为没有外部代码有权观察p,编译器可以生成将ap设置为任何不等于{的任意值的代码。 {1}}然后完全忽略了比较,但标准中的任何内容都不允许实现执行任何操作,只需将相同的值写入aqgap并返回379(123 + 256)或将不同的值写入gaqgap并返回1.

将无效指针与有效指针进行比较的UB背景

在某些处理器上,尝试将指针加载到寄存器中会导致处理器对其有效性进行一些验证。例如,在80286上,每个指针都包含一个段选择器和一个偏移量,加载段选择器将使处理器从有效段的表中获取一些信息。

有些C实现会在任何完成时将指针加载到寄存器中,无论它们是否将用于访问内存,而80286的某些C实现可能会使段描述符无效(如果相应段中唯一的东西是已释放的内存块。 C标准的作者不希望要求C实现在指针未被解除引用的情况下花费精力避免寄存器加载,也不希望要求实现为已释放的指针维护有效的段描述符。强加任何一个要求的最简单方法是在代码执行任何可以释放指针的事情的情况下避免需要任何东西,然后做任何可能导致指针被加载到寄存器的事情。

有许多实现,即使已释放占用的存储空间,将指针加载到寄存器中也是安全的,或者在指针不会被解除引用的情况下避免“可捕获的”寄存器加载(加载指针)进入通用寄存器进行比较会比将它们加载到段寄存器并将它们转移到通用寄存器进行比较更便宜,而且我认为没有理由相信标准的作者打算那些专门针对实现实现的代码上述任何一种都不应该使用像以下技术:

gaq

在realloc很可能“就地”成功的情况下(例如 因为块正在缩小)并且它可能会在哪里 - 但是 昂贵的 - 如果要重新生成分配存储内的东西的指针 对象最终被移动了。尽管如此,因为标准没有 即使在以下情况下,也要求任何实现支持此类技术 这样做会花费任何费用,一些实现(即使是那样的 支持不会花费任何费用)尽量不提供它。

答案 3 :(得分:-1)

  

为什么我还能打印“1 2”?

出于与原始代码相同的原因,优化器知道Dim myFolder As StorageFolder = Await GetLogFolder 无效并且在该假设下向后工作。为什么无效?因为...

  

J.2未定义的行为

     

1在以下情况下,行为未定义:

     

使用指向通过调用free或realloc函数解除分配的空间的指针的值(7.20.3)。

function f = fixed(p,e) i=1; pn=g(p); while (abs(pn - p) <= e) pn = g(p) i=i+1; p=pn end end 无效。优化器知道*p确实是*p,因此它也无效。

使用*(int*)ap查看原始代码和代码的IR,它们几乎完全相同。硬编码*pclang -S -O3 -emit-llvm都包含在1

2

这是您在IR中的printf

%7 = tail call i32 (i8*, ...) @printf(i8* nonnull getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i64 0, i64 0), i32 1, i32 2)

除了尾调用和bitcasts的顺序不同之外,几乎完全相同。这是你的。

main

这是他们的。

define i32 @main() #0 {
  %1 = tail call i8* @malloc(i64 4)
  %2 = tail call i8* @realloc(i8* %1, i64 4)
  %3 = bitcast i8* %2 to i32*
  %4 = bitcast i8* %1 to i32*
  store i32 1, i32* %4, align 4, !tbaa !2
  store i32 2, i32* %3, align 4, !tbaa !2
  %5 = icmp eq i8* %1, %2
  br i1 %5, label %6, label %8

; <label>:6                                       ; preds = %0
  %7 = tail call i32 (i8*, ...) @printf(i8* nonnull getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i64 0, i64 0), i32 1, i32 2)
  br label %8

; <label>:8                                       ; preds = %6, %0
  ret i32 0
}

其他一切都是一样的。

正如谈话中提到的, %1 = tail call i8* @malloc(i64 4) %2 = tail call i8* @realloc(i8* %1, i64 4) %3 = bitcast i8* %2 to i32* %4 = bitcast i8* %1 to i32* 是一个红鲱鱼。只是在那里说明这种行为显然是多么荒谬。它仍然存在于IR中。

  %1 = tail call i8* @malloc(i64 4)
  %2 = bitcast i8* %1 to i32*
  %3 = tail call i8* @realloc(i8* %1, i64 4)
  %4 = bitcast i8* %3 to i32*

如果您打印出if %5 = icmp eq i8* %1, %2 br i1 %5, label %6, label %8 p,则可以看到优化工具在工作。它们都是第一个malloc的结果。

q

指针很好。它正在解除它的问题。