当我溢出分配的字符数组时,为啥我的 C 程序不会崩溃?

Posted

技术标签:

【中文标题】当我溢出分配的字符数组时,为啥我的 C 程序不会崩溃?【英文标题】:How come my C program doesn't crash when I overflow an allocated array of characters?当我溢出分配的字符数组时,为什么我的 C 程序不会崩溃? 【发布时间】:2013-08-30 12:16:43 【问题描述】:

我有一个简单的 C 文件 I/O 程序,它演示了逐行读取文本文件,并将其内容输出到控制台:

/**
* simple C program demonstrating how
* to read an entire text file
*/

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

#define FILENAME "ohai.txt"

int main(void)

    // open a file for reading
    FILE* fp = fopen(FILENAME, "r");

    // check for successful open
    if(fp == NULL)
    
        printf("couldn't open %s\n", FILENAME);
        return 1;
    

    // size of each line
    char output[256];

    // read from the file
    while(fgets(output, sizeof(output), fp) != NULL)
        printf("%s", output);

    // report the error if we didn't reach the end of file
    if(!feof(fp))
    
        printf("Couldn't read entire file\n");
        fclose(fp);
        return 1;
    

    // close the file
    fclose(fp);
    return 0;
   

看起来我已经为每行 256 个字符分配了一个数组(在 32 位机器上为 1024 个 bytes 位)。即使我在第一行用超过 1000 个字符的文本填充 ohai.txt,程序也不会出现段错误,我认为它会发生段错误,因为它溢出了由 output[] 数组指定的分配的可用空间量.

我的假设是操作系统会给程序额外的内存,同时它有额外的内存可供使用。这意味着程序只会在 ohai.txt 中的一行文本消耗的内存导致堆栈溢出时崩溃。

是否可以在 C 和内存管理支持方面有更多经验的人或反驳我关于为什么该程序不会崩溃的假设,即使文本文件的一行中的字符数远大于 256?

【问题讨论】:

你的假设是错误的。您应该仔细重新阅读fgets 的文档,尤其是它的第二个 /*parameter*/ 参数的意义。这应该会有所启发! 会做...你的意思是第二个参数,确定吗? @ironicaldiction 是的,第二个参数,肯定。 是的,争论 - 我不确定哪个术语在英语中是正确的 也 sizeof(char) == 1 根据 C 标准,在大多数系统上,char 是一个字节,而不是 4。其次,堆栈的工作方式不是你想象的那样。在这种情况下,在出现段错误之前,您必须使缓冲区溢出很多。基本上,您会为堆栈获得一定数量的页面,除非超出此范围,否则不会出现段错误,程序不会在每个函数调用上分配更多页面,因此整个可能的堆栈大小都是可写的。您的数组正在接近尾端,因此您可以在收到段错误之前最多写入几个堆栈帧 【参考方案1】:

解释堆栈以及为什么即使您确实溢出也不会出现段错误(正如其他人指出的那样编写的代码不会)

你的堆栈指针从某个地址开始,比如 0x8000000,然后运行时调用 main,它会向下移动一点(那里可能还有其他东西,所以我们不知道堆栈开始时有多少东西main),然后 main 将为所有局部变量移动堆栈指针。因此,此时您的数组将有一个比 0x8000000 低 256 个字节的地址,除非您一直运行在 main 的所有堆栈帧和任何其他称为 main 的 C 运行时东西的堆栈帧上,否则您不会遇到段错误.

因此,为了简单起见,假设您的数组以 0x7fffd00 处的基地址结束,该地址比 0x8000000 低 768 个字节,这意味着您至少必须溢出那么多才能获得段错误,(那么您可能会得到当 main 返回或调用 feof 时出现段错误,因为您用随机字符填充了堆栈帧,但我们正在谈论 fgets() 中的段错误,但如果可写的内容映射到堆栈上方的页面,即使这样也不是必须的(大多数操作系统不太可能避免这样做,所以如果溢出足够多,你会得到一个段错误)

如果堆栈以其他方式运行(即:向上增长),您必须运行整个最大大小堆栈,这在用户空间中通常非常大(Linux 上 32 位 x86 的默认值为 2MB)但我'我很确定 x86 堆栈会向下增长,因此您的情况不太可能。

【讨论】:

【参考方案2】:

OP 的程序没有崩溃,因为没有发生缓冲区溢出

while(fgets(output, sizeof(output), fp) != NULL)
  printf("%s", output);

fgets() 很好地阅读了一组 char 最多计数或 255 或 \n。然后printf("%s" ... 很好地将它们打印出来。如此重复直到没有更多数据/

没有崩溃、没有溢出、没有运行、没有命中、没有错误。

【讨论】:

【参考方案3】:

这里没有溢出任何内容:fgets 不会向缓冲区写入超过 sizeof(output) 个字符,因此不会溢出任何内容(请参阅the documentation)。

但是,如果确实溢出缓冲区,则会出现未定义的行为。根据 C 规范,程序可能会做任何事情:崩溃、不崩溃、静默销毁重要数据、意外调用rm -rf / 等。所以,不要期望程序在调用时崩溃UB。

【讨论】:

你能解释为什么我没有溢出任何东西,即使我在程序读取的文件中的一行文本中有超过 254 个字符?如果我在这里遗漏了一些明显的东西,我深表歉意。 254 应该是 256 以上 你不会溢出任何东西因为你告诉fgets不要这样做。您应该查看我的答案中链接的fgets 文档,以了解它是如何工作的;然后你会发现你不可能用你调用它的方式溢出缓冲区。 啊,好吧。如果我没记错的话,fgets 将读取 256 个字节的次数,因为它包含在while 循环中。因此,它会不断地解决问题,一次 256 个字节,直到读取 fp 指向的文件中的所有 char 字节(此时遇到 NULL)。非常感谢@nneonneo【参考方案4】:

fgets(output, sizeof(output), fp) 在这种情况下读取 (sizeof(output) -1) 个字符(否则读取到换行符或文件末尾)

【讨论】:

我的问题是“否则它会读取到换行符或文件末尾”。它如何“决定”读取到换行符或文件末尾,而不是最大程度地读取sizeof(output - 1) @ironicaldiction 它有自己的内部缓冲(在 FILE * 对象中有一个指向它的指针),即:它将数据读入它自己的缓冲区,实际上可能会读到行尾,但因为它复制到您的数组中,它会查找换行符并在找到时停止,并且 FILE * 会跟踪缓冲区中有多少数据以供后续读取。

以上是关于当我溢出分配的字符数组时,为啥我的 C 程序不会崩溃?的主要内容,如果未能解决你的问题,请参考以下文章

C 微小的、预分配的数组不会溢出

为啥在 c++ 中分配 char 数组元素时,分配的字符被破坏?

为啥我的字符串分配会导致分段错误?

当我分配 NSError userInfo 来设置 NSDictionary 时,为啥我的应用程序会崩溃?

为啥当我返回时我的主视图控制器不会更新?

“错误:分配给带有数组类型错误的表达式”当我分配结构字段时(C)