全局变量声明如何解决C中的堆栈溢出问题?

我有一些C代码。 它做的很简单,从io获得一些数组,然后对其进行分类。

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

#define ARRAY_MAX 2000000

int main(void) {
    int my_array[ARRAY_MAX];
    int w[ARRAY_MAX];
    int count = 0;

    while (count < ARRAY_MAX && 1 == scanf("%d", &my_array[count])) {
        count++;
    }

    merge_sort(my_array, w, count);
    return EXIT_SUCCESS;
}

它运作良好,但如果我真的给它一个2000000的数字,它会导致堆栈溢出。 是的,它用完了所有的堆栈。 其中一个解决方案是使用malloc()为这两个变量分配一个内存空间,将它们移动到堆中,所以完全没有问题。

另一种解决方案是将下面的2声明移到全局范围,使它们成为全局变量。

    int my_array[ARRAY_MAX];
    int w[ARRAY_MAX];

我的导师告诉我,这个解决方案做的是同样的工作:将这两个变量放入堆中。

但我在网上查了一些文件。 全局变量,没有初始化,它们将驻留在BSS段,对不对?

我在网上查过,这部分的大小只是几个字节。 它如何防止堆栈溢出?

或者,因为这两种类型都是数组,所以它们是指针,而全局指针驻留在数据段中,它表明数据段的大小也可以动态改变?


目标文件(4或8字节)中的bss(由符号启动的块)很小,但存储的值是在初始化数据之后分配的归零内存的字节数。

它通过分配存储'不在堆栈上'来避免堆栈溢出。 它通常在数据段中,在文本段之后和堆段开始之前 - 但是现在简单的内存图片可能会更复杂。

官方应该注意'标准没有说必须有堆栈'以及其他各种各样的小块',但这并不会改变答案的实质。 bss部分很小,因为它是一个单一的数字 - 但是这个数字可能代表了很多内存。


免责声明:这不是一个指南,它是一个概述。 它基于Linux如何做事,尽管我可能弄错了一些细节。 大多数(桌面)操作系统使用一个非常相似的模型,具有不同的细节。 此外,这仅适用于用户空间程序。 除非您正在为内核开发或者在模块(linux),驱动程序(windows),内核扩展(osx)上工作,否则这就是您正在编写的内容。

虚拟内存:我将在下面详细介绍,但要点是每个进程都有独占的32/64位地址空间。 显然,一个进程的整个地址空间并不总是映射到真实的内存。 这意味着:A)一个进程的地址对另一个进程没有任何意义; B)操作系统决定进程地址空间的哪些部分被加载到实际内存中,哪些部分可以在任何给定的时间点停留在磁盘上。

可执行文件格式

可执行文件有许多不同的部分。 我们关心的是.text.data.bss.rodata.text部分是您的代码。 .data.bss段是全局变量。 .rodata部分是常量值'变量'(又名consts)。 Consts是错误字符串和其他消息,或者也许是幻数。 您的程序需要引用但不会更改的值。 .data节存储具有初始值的全局变量。 这包括定义为<type> <varname> = <value>;变量<type> <varname> = <value>; 。 例如,一个包含状态变量的数据结构,包含初始值,程序用它来跟踪自己。 .bss节记录没有初始值的全局变量或初始值为零的全局变量。 这包括定义为<type> <varname>;变量<type> <varname>;<type> <varname> = 0; 。 由于编译器和操作系统都知道.bss段中的变量应该初始化为零,所以没有理由实际存储所有这些零。 所以可执行文件只存储变量元数据,包括应为变量分配的内存量。

进程内存布局

当操作系统加载可执行文件时,它会创建六个内存段。 bss,数据和文本段全部位于一起。 数据和文本段从文件中加载(不是真的,请参阅虚拟内存)。 bss部分分配给所有未初始化/零初始化变量的大小(请参阅VM)。 内存映射段与数据和文本段类似,因为它由从文件加载(参见VM)的内存块组成。 这是加载动态库的地方。

bss,数据和文本段是固定大小的。 内存映射段实际上是固定大小的,但是当你的程序加载一个新的动态库或使用另一个内存映射函数时它会增长。 但是,这不会经常发生,并且大小的增加始终是映射的库或文件(或共享内存)的大小。

堆栈

堆栈有点复杂。 内存区域的大小由程序决定,为堆栈保留。 堆栈的顶部(低内存地址)用主函数的变量进行初始化。 在执行过程中,可以在堆栈底部添加或移除更多变量。 将数据压入堆栈会使其“增长”(更高的内存地址),从而增加堆栈指针(它维护堆栈底部的地址)。 从堆栈中弹出数据会将其缩小,从而减少堆栈指针。 当一个函数被调用时,调用函数中下一条指令的地址(文本段中的返回地址)被压入堆栈。 当一个函数返回时,它将堆栈恢复到它调用函数之前的状态(它被压入堆栈的所有东西都会弹出)并跳转到返回地址。

如果堆栈变得太大,结果取决于许多因素。 有时你会遇到堆栈溢出。 有时运行时(在你的情况下,C运行时)试图为堆栈分配更多的内存。 这个话题超出了这个答案的范围。

堆用于动态内存分配。 分配有一个alloc函数的内存alloc在堆上。 所有其他内存分配不在堆上。 堆开始是一大块未使用的内存。 当您在堆上分配内存时,操作系统会尝试在堆中查找您的分配空间。 我不打算查看实际分配过程的工作方式。

虚拟内存

操作系统使你的进程认为它有整个32/64位内存空间可供使用。显然,这是不可能的。 通常这意味着你的进程可以访问比你的计算机更多的内存; 在具有4GB内存的32位处理器上,这意味着您的进程可以访问每一个内存位,而其他进程没有剩余空间。

您的流程使用的地址是假的。 它们不映射到实际的内存。 另外,进程地址空间中的大部分内存都是不可访问的,因为它不涉及任何内容(在32位处理器上它可能不是最重要的)。 可用/有效地址的范围被分成页面。 内核为每个进程维护一个页表。

当你的可执行文件被加载并且当你的进程加载一个文件时,实际上它被映射到一个或多个页面。 操作系统不一定实际上将该文件加载到内存中。 它所做的是在页表中创建足够的条目来覆盖整个文件,同时指出这些页面由文件支持。 页表中的条目有两个标志和一个地址。 第一个标志(有效/无效)表示页面是否加载到实内存中。 如果页面未加载,则另一个标志和地址是没有意义的。 如果页面已加载,则第二个标志指示页面的真实内存是否已被加载,并且地址将页面映射到实际内存。

堆栈,堆和bss的工作方式相似,只是它们不受“真实”文件支持。 如果操作系统决定你的一个进程的页面没有被使用,它将卸载该页面。 在卸载页面之前,如果在该页面的页表中设置了修改标志,则会将页面保存到某处的磁盘。 这意味着如果堆栈或堆中的页面被卸载,则将创建一个现在映射到该页面的'文件'。

当您的进程尝试访问(虚拟)内存地址时,内核/内存管理硬件使用页表将该虚拟地址转换为实际内存地址。 如果有效/无效标志无效,则触发页面错误。 内核暂停你的进程,找到或建立一个空闲页面,将映射文件的一部分(或堆栈或堆的伪文件)加载到该页面中,将有效/无效标志设置为有效,更新地址,然后重新运行原始触发页面错误的指令。

AFAIK,bss部分是一个特殊的页面或页面。 当第一次访问本节中的页面(并触发页面错误)时,页面在内核将控制权返回给您的进程之前归零。 这意味着当您的进程加载时,内核不会预先清零整个bss部分。

进一步阅读

  • 记忆中的程序剖析
  • 内核如何管理你的记忆

  • 全局变量不在堆栈上分配。 它们分配在数据段(如果初始化)或bss(如果它们未初始化)。

    链接地址: http://www.djcxy.com/p/28347.html

    上一篇: How does the global variable declaration solve the stack overflow in C?

    下一篇: Heap array allocation instead of on stack