【问题标题】:Strings, char array sizes, and calloc --- what causes this segmentation fault?字符串、char 数组大小和 calloc --- 是什么导致了这种分段错误?
【发布时间】:2014-06-06 03:01:48
【问题描述】:

为了使这个问题简短(不检查 null 等),以下内容被缩写。

program1.c

main()
{
   char *aString = calloc(10, sizeof(char));

   printf("Enter string: ");
   scanf("%s", aString);

   printf("You typed in %s\n", aString);
}

program2.c

main()
{
   char aString[10];

   printf("Enter string: ");
   scanf("%s", aString);

   printf("You typed in %s\n", aString);
}

program1.c 将让我看似永远输入字符。我已经输入了 2000 多个字符,程序将毫无错误地执行,尽管这是“未定义的行为”。

program2.c 会让我输入超过 10 个字符,但如果接近 30 或 40 个字符,它会给我一个分段错误。


现在我对课堂和其他教程的有限理解告诉我,这两个程序在引擎盖下都在做同样的事情 --- 留出一块内存,打算成为长度为 10 的字符数组。但似乎program2.c 的实现提供了一定程度的安全性。或者,当您超过授予的内存空间时,分段错误错误是完全随机的,而我只是碰巧用 program2.c 得到它,只是因为我的计算机现在处于这种状态?

program1.c 和program2.c 有什么区别,哪个是输入字符串“更安全”的方法?我知道还有其他方法可能更好,但我很好奇这两种方法之间的比较。

【问题讨论】:

  • 他们并没有在后台做同样的事情,因为静态或动态分配的内存不在同一个地方(分别是堆栈与堆)。更安全地使用fgets 来限制输入的字符串的大小。
  • 它们都不安全:它不崩溃并不意味着它是安全的。您可以参考臭名昭著的“heartbleed”错误。
  • 一些观察:main的原型是int main(int,char**)int main(void)sizeof(char) 是根据定义 1。您可以考虑使用 "%9s" 来避免 char[10] 上的缓冲区溢出。
  • 让我猜你可以用 prog 1 输入 4095-4 个字符。 Calloc() 将在现代系统上为进程提供一个完整的内存页面,这些页面将被后续 [mc ]allocs,通常按升序排列。只有使用特殊的调试库,您的指针才能位于该页面的末尾,以便立即覆盖段错误,而不是破坏稍后分配的内存中的数据。
  • 此代码与使用 gets 一样安全……出于某种原因,人们会跳过 gets 帖子,但对 scanf("%s" 帖子的反应不同

标签: c arrays string memory segmentation-fault


【解决方案1】:

两者在引擎盖下相同。

program1 正在调用calloc 以从分配内存。

program2 已编译为在调用函数时在堆栈上保留额外空间。

这两个程序都是可利用的,因为您在调用scanf() 时没有检查任何边界。他可以随意向任一缓冲区写入任意数量的字节。这里的解决方案是scanf("%9s", aString),它告诉scanf 最多只能写入9+1 个字节。

【讨论】:

  • 请注意,C 既不要求也不指定“堆”或“堆栈”。在适当的术语中,有两个区别。在第一个程序中,aString 是一个指针(类型 char*),它指向具有所谓动态持续时间的内存。在第二个程序中,aString 是一个自动持续时间的数组(类型 char[10])。
  • @fstd 感谢您指出这一点。我经常在“现代”或“通用”架构中根深蒂固,但你在技术上是正确的。但是,我不愿意为我的文本添加更多复杂性。
  • 有趣。来自 10k 多名代表用户的两个答案,均被否决且未发表评论。
  • 也许我们与“未定义的意味着紫色独角兽同样可能”的人群发生冲突。
  • @AShelly 确实如此。他们似乎忘记了 C 规范也没有定义“分段错误”,因此我们当然应该被允许将我们的答案限制在出现此类错误的平台上。
【解决方案2】:

虽然这两个程序都不安全,但您看到该行为的可能原因是程序 B 在堆栈上分配数组,一旦超出范围,您就会覆盖其他有用的东西,例如 @ 的堆栈帧987654321@电话。

而程序 A 分配堆内存,并且由于您在这个玩具程序中没有做任何其他事情,因此您正在写入的内存是未使用的。

在任何实际程序中,两者都同样不安全。

注意:C 标准未定义这种越界行为,编译器理论上可以做任何事情。但在大多数常见的现实世界编译器中,上述情况很可能是实际发生的情况。

【讨论】:

    【解决方案3】:

    假设一个典型的现代操作系统,您的程序 1 不会崩溃,因为 calloc 必须从操作系统请求整个页面(通常是 4096 字节的 RAM)以满足 10 字节的请求。如果您为该程序提供足够多的字符,它崩溃。但是,禁止写入比公开请求的大小(10 字节)多一个字节,并且极有可能破坏用于跟踪“堆”分配的内部数据结构。如果您在此程序中添加另一个 mallocfree 调用,则很可能在 scanf 之后,它会在 mallocfree 内部崩溃。举例来说,考虑这个程序:

    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
      char *p = malloc(23);
      memcpy(p, "abcdefghijklmnopqrstuvwx", 25);
      char *q = malloc(1);
      return 0;
    }
    

    $ MALLOC_CHECK_=1 ./a.out 
    *** Error in `./a.out': malloc: top chunk is corrupt: 0x0000000001bc4020 ***
    

    (在 this 系统上,仅复制 24 个字节不会崩溃。不要依赖此信息。)

    与此同时,程序 2 崩溃可能不是因为 scanf 调用一直写入未映射的内存(出于类似原因,这将需要更多字节的输入),而是因为堆栈上的数据非常密集它破坏了一些关键的东西,例如main 应该返回的地址。

    在一个比你的例子更复杂一点的程序中,这两种“技术”同样危险——堆和堆栈溢出都可能并且已经导致灾难性的安全漏洞。


    您明确要求比较您的两种不安全技术,但为了未来读者的利益,我将描述两种更好从标准输入读取字符串的技术。如果你的 C 库包含它,最好的选择是 getline,它(在像这样的简单程序中)会这样使用:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        char *line = 0; 
        size_t n = 0;
        ssize_t r;
    
        fputs("Enter a string: ", stdout);
        fflush(stdout);
    
        r = getline(&line, &n, stdin);
        if (r == -1) {
            perror("getline");
            return 1;
        }
        if (r > 0 && line[r-1] == '\n')
            line[r-1] = '\0';
    
        printf("You entered %s\n", line);
        free(line);
        return 0;
    }
    

    如果你没有getline,并且你需要从用户那里读取任意长的字符串,你最好的选择是自己实现getline (gnulib has an implementation you can borrow, if your code can be released under the GPL)。但在许多情况下,可接受的替代方法是对输入长度设置上限,此时您可以使用fgets

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define MAX_LINE_LEN 81
    
    int main(void)
    {
        char *line = malloc(MAX_LINE_LEN);
        size_t n;
    
        fputs("Enter a string: ", stdout);
        fflush(stdout);
    
        if (!fgets(line, MAX_LINE_LEN, stdin)) {
            perror("fgets");
            return 1;
        }
        n = strlen(line);
        if (line[n] != '\n') {
            fprintf(stderr, "string too long - %u characters max\n", MAX_LINE_LEN);
            return 1;
        }
        line[n] = '\0';
    
        printf("You entered %s\n", line);
        free(line);
        return 0;
    }
    

    注意事项:

    1. sizeof(char) == 1 定义;因此,sizeof(char) 永远不应出现在编写良好的代码中。如果您想使用calloc 分配预置零的字符数组,请编写calloc(1, nchars)
    2. Never use scanf, fscanf, or sscanf
    3. 不要将fgetsgets 混淆。如果使用正确,fgets 是安全的; it is impossible to use gets safely

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-15
      • 2016-03-22
      • 2012-09-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多