代码之家  ›  专栏  ›  技术社区  ›  Alexey Frunze

realloc()、寿命和UB

  •  5
  • Alexey Frunze  · 技术社区  · 9 年前

    最近有一个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;
    }
    

    代码具有未定义的行为(p在realloc()之后变得无效,即使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;
    }
    

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

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

    4 回复  |  直到 9 年前
        1
  •  5
  •   M.M    9 年前

    如前所述,在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 had,当块被释放时,它将成为无效的令牌。 (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);
    

    起源的概念允许优化器在代码上注册未定义的行为 p + 10 ,因此它可以将此片段翻译为 puts("5") 例如,即使 q 恰巧紧接着 p 在记忆中。(旁白-我想知道DJ伯恩斯坦是不是 boringcc 编译器实际上将无法执行此优化)。

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

        2
  •  1
  •   supercat    9 年前

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

    然而,给定与您的代码类似但不调用未定义行为的代码,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;
    }
    

    在godbolt上使用选项调用Clang 3.9.0 -xc -O3 -pedantic 生成返回1或257的代码,具体取决于 ap aq 比较相等,即使当前标准中不允许 与任何其他类型的变量区别对待 uintptr_t 其恰好保持相同的值。代码的编写方式,因为任何外部代码都无权观察 p ,则允许编译器生成设置 美联社 任何不等于的任意值 aq 然后完全忽略了比较,但是标准中没有允许实现除了将相同的值写入 gap gaq 并返回379(123+256)或将不同的值写入 缺口 gaq 并返回1。

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

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

    有些C实现会在任何时候将指针加载到寄存器中 任何东西

    存在许多实现方式,其中将指针加载到寄存器中将是安全的,或者在指针不被解引用的情况下避免“可捕获”寄存器加载(将指针加载到通用寄存器中进行比较比将其加载到段寄存器并将其传输到通用寄存器进行比较便宜),我认为没有理由相信,本标准的作者意图让代码专门针对上述任何一种不应利用以下技术的实现:

    void do_realloc(int new_size)
    {
      void *new_ptr = realloc(old_ptr, new_size);
      if (!new_ptr) fatal_error;
      if (new_ptr != old_pointer)
        update_pointers();
    }
    

    因为区块正在缩小)和可能的位置——但是 昂贵——如果出现以下情况,则重新生成指向分配存储中的内容的指针: 物体最终被移动了。尽管如此,因为标准没有 要求任何实现支持此类技术,即使在以下情况下: 这样做不需要任何成本,在一些实现中(甚至是那些 支持将不花费任何费用),他们会不遗余力地不提供支持。

        3
  •  0
  •   Alexey Frunze    9 年前

    最新的C标准使这个问题模糊不清。 N2090 声明 DR260 委员会的答复

    没有被纳入标准文本,它也留下了许多不清楚的具体问题。。。

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

        4
  •  -1
  •   Community Mohan Dere    6 年前

    为什么我仍然可以打印“12”?

    由于与原始代码相同的原因,优化器知道 *p 无效,并且在该假设下反向工作。为什么它是无效的?因为

    J、 2未定义行为

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

    使用指针的值,该指针指的是通过调用free或realloc函数释放的空间(7.20.3)。

    *p *(int*)ap 真的 *p 所以它也是无效的。

    查看原始代码和代码的IR clang -S -O3 -emit-llvm ,它们几乎完全相同。两者都是硬代码 1 2 进入 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)
    

    这是你的 main 在IR中。

    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*
    

    这是他们的。

      %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 是转移注意力。这只是为了说明这种行为有多么明显的荒谬,它仍然存在于IR中。

      %5 = icmp eq i8* %1, %2
      br i1 %5, label %6, label %8
    

    如果打印出来,您可以看到优化器正在工作 p , q (int*)ap 他们都是第一次马洛克的结果。

      %1 = tail call i8* @malloc(i64 4)
      ...
      %8 = tail call i32 (i8*, ...) @printf(i8* nonnull getelementptr inbounds ([10 x i8], [10 x i8]* @.str.1, i64 0, i64 0), i8* %1, i8* %1, i8* %1)
    

    指针很好。这就是问题所在。

    推荐文章