【问题标题】:Referencing a char* that went out of scope引用超出范围的 char*
【发布时间】:2017-11-15 17:04:15
【问题描述】:

用了一段时间的C++编程,最近又开始用C编程了,对指针的理解有点生疏了。

我想问一下为什么这段代码没有导致任何错误:

char* a = NULL;
{
    char* b = "stackoverflow";
    a = b;
}

puts(a);

我认为因为b 超出范围,a 应该引用一个不存在的内存位置,因此它们在调用printf 时会出现运行时错误。

我在 MSVC 中运行了这段代码大约 20 次,没有出现任何错误。

【问题讨论】:

  • 这不是未定义的。字符串文字是静态分配的。这段代码非常好。
  • 换句话说,b 已经超出范围,但不是它所指向的范围。
  • OTOH,如果您改为:char b[] = { 's', 't', 'a', 'c', 'k', 'o', 'v', 'e', 'r', 'f', 'l', 'o', 'w', '\0' }.,则行为未定义
  • @DavidBowling 甚至char b[] = "stackoverflow";
  • @CometEngine 他们可能会坐在.rodata 甚至.text 部分。

标签: c


【解决方案1】:

字符串字面量总是静态分配的,程序可以随时访问,

char* a = NULL;
{
    char* b = "stackoverflow";
    a = b;
}

printf(a);

这里的字符串文字"stackoverflow"的内存由编译器分配,就像它为int/char变量或指针分配内存一样。

不同之处在于字符串文字位于 READONLY 部分/段中。 变量b 在堆栈中分配,但它持有只读段/段的内存地址。

在代码中 var b 有一个字符串字面量的地址。即使b 失去其范围,字符串文字的内存也将始终被分配。

注意:分配给字符串字面量的内存是二进制的一部分,一旦程序卸载就会被删除。

请参阅 ELF 二进制规范以了解更多详细信息。

【讨论】:

    【解决方案2】:

    我认为,作为先前答案的证明,最好看一下代码中的真正内容。人们已经提到字符串文字位于 .text 部分中。因此,它们(字面量)简单地、总是在那里。您可以轻松找到此代码

    #include <string.h>
    
    int main() {
      char* a = 0;
      {
        char* b = "stackoverflow";
        a = c;
      }
      printf("%s\n", a);
    }
    

    使用以下命令

    > cc -S main.c
    

    在 main.s 中,您会在最底部找到

    ...
    ...
    ...
            .section        __TEXT,__cstring,cstring_literals
    L_.str:                                 ## @.str
            .asciz  "stackoverflow"
    
    L_.str.1:                               ## @.str.1
            .asciz  "%s\n"
    

    您可以在此处阅读有关汇编程序部分的更多信息(例如):https://docs.oracle.com/cd/E19455-01/806-3773/elf-3/index.html

    在这里您可以找到准备充分的 Mach-O 可执行文件: https://www.objc.io/issues/6-build-tools/mach-o-executables/

    【讨论】:

    • 谢谢 :) 我也看过 msvc 反汇编窗口;好像是一样的
    【解决方案3】:

    代码不会产生任何错误,因为您只是将字符指针b 分配给另一个字符指针a,这非常好。

    在 C 中,您可以将指针引用分配给另一个指针。这里实际上字符串“stackoverflow”用作文字,该字符串的基地址位置将分配给a变量。

    虽然您超出了变量 b 的范围,但仍然使用 a 指针完成了赋值。因此它将打印结果而不会出现任何错误。

    【讨论】:

      【解决方案4】:

      其他人已经解释说此代码完全有效。这个答案是关于您的期望,如果代码无效,调用printf 时会出现运行时错误。不一定如此。

      让我们看看你的代码的这个变体,它是无效的:

      #include <stdio.h>
      int main(void)
      {
          int *a;
          {
              int b = 42;
              a = &b;
          }
          printf("%d\n", *a); // undefined behavior
          return 0;
      }
      

      这个程序有未定义的行为,但它碰巧事实上,由于几个不同的原因,它很可能会打印 42 — 许多编译器会为分配的b 留下堆栈槽对于main 的整个主体,因为没有其他东西需要空间,并且最小化堆栈调整的数量简化了代码生成;即使编译器确实正式解除了堆栈槽的分配,数字 42 也可能会保留在内存中,直到有其他东西覆盖它,并且在 a = &amp;b*a 之间没有任何东西可以这样做;标准优化(“常量和复制传播”)可以消除这两个变量并将*a 的最后一个已知值直接写入printf 语句(就像您编写了printf("%d\n", 42))。

      理解“未定义的行为”并不意味着“程序会意外崩溃”这一点非常重要。它的意思是“任何事情都可能发生”,任何事情都包括看起来像程序员可能想要的那样工作(在这台计算机上,今天使用这个编译器)。


      最后一点,我可以方便地访问的任何积极的调试工具(Valgrind、ASan、UBSan)都没有足够详细地跟踪“自动”变量的生命周期来捕获这个错误,但是 GCC 6 确实会产生这个有趣的警告:

      $ gcc -std=c11 -O2 -W -Wall -pedantic test.c
      test.c: In function ‘main’:
      test.c:9:5: warning: ‘b’ is used uninitialized in this function
          printf("%d\n", *a); // undefined behavior
          ^~~~~~~~~~~~~~~~~~
      

      我相信这里发生的事情是,它完成了我上面描述的优化——将b 的最后一个已知值复制到*a,然后复制到printf——但它的“最后一个已知值”是b是一个“此变量未初始化”的标记而不是 42。(然后它会生成等效于 printf("%d\n", 0) 的代码。)

      【讨论】:

        【解决方案5】:

        在定义b 的范围内,它被分配了一个字符串文字的地址。这些文字通常位于内存的只读部分,而不是堆栈。

        当您执行a=b 时,您将b 分配给a,即a 现在包含字符串文字的地址。在b 超出范围后,此地址仍然有效。

        如果您获取了b地址,然后尝试取消引用该地址,那么您将调用undefined behavior

        所以您的代码是有效的,并且会调用未定义的行为,但以下代码会:

        int *a = NULL;
        {
            int b = 6;
            a = &b;
        }
        
        printf("b=%d\n", *a);
        

        另一个更微妙的例子:

        char *a = NULL;
        {
            char b[] = "stackoverflow";
            a = b;
        }
        
        printf(a);
        

        这个例子和你的不同之处在于b,它是一个数组,当分配给a 时,衰减指向指向第一个元素的指针。所以在这种情况下,a 包含一个局部变量的地址,然后超出范围。

        编辑:

        附带说明,将变量作为printf 的第一个参数传递是一种不好的做法,因为这会导致format string vulnerability。最好使用如下字符串常量:

        printf("%s", a);
        

        或者更简单地说:

        puts(a);
        

        【讨论】:

        • 感谢您的解释和示例;真的有帮助:)
        • @CometEngine 很高兴我能帮上忙。如果您觉得有用,请随时 accept this answer
        • 我认为即使是后面的代码也可能会优化到 printf("stackoverflow"),具体取决于您使用的编译器/开关
        • @Govind Parmar 刚刚在 msvc 上进行了全面优化和内联测试;似乎是这样。
        • @Stargateur 我喜欢 SiggiSv 给出的答案的分步性质。它使初学者更容易遵循。它绝对值得超过设置赏金时的 1 次赞成票。
        【解决方案6】:

        逐行,这就是你的代码所做的:

        char* a = NULL;
        

        a 是一个不引用任何内容的指针(设置为NULL)。

        {
            char* b = "stackoverflow";
        

        b 是一个指针,引用静态常量字符串文字"stackoverflow"

            a = b;
        

        a 设置为还引用静态常量字符串文字 "stackoverflow"

        }
        

        b 超出范围。但由于a引用b,那么这并不重要(它只是引用与b 引用的相同的静态常量字符串文字)。

        printf(a);
        

        打印由a 引用的静态常量字符串文字"stackoverflow"

        【讨论】:

          【解决方案7】:

          字符串字面量是静态分配的,因此指针无限期有效。如果您说char b[] = "stackoverflow",那么您将在堆栈上分配一个字符数组,当范围结束时该数组将变为无效。修改字符串时也会出现这种差异:char s[] = "foo" 堆栈分配了一个您可以修改的字符串,而 char *s = "foo" 只为您提供了一个指向可以放置在只读内存中的字符串的指针,因此修改它是未定义的行为。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2019-06-09
            • 2012-05-04
            • 2018-03-01
            • 1970-01-01
            • 2013-09-28
            • 1970-01-01
            相关资源
            最近更新 更多