繁体   English   中英

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

[英]How does the global variable declaration solve the stack overflow in 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段中,对吧?

我在网上查了一下,这部分的大小只有几个字节。 怎么能阻止堆栈溢出?

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

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

它通过分配存储“不在堆栈上”来避免堆栈溢出。 它通常位于数据段中,在文本段之后和堆段开始之前 - 但是这些简单的内存映像现在可能更复杂。

正式地说,应该有一些警告,“标准并没有说必须有堆叠”和其他各种小部分'''',但这并没有改变答案的实质。 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,数据和文本段是固定大小的。 内存映射段实际上是固定大小的,但是当程序加载新的动态库或使用另一个内存映射函数时,它会增长。 但是,这不会经常发生,并且大小增加始终是要映射的库或文件(或共享内存)的大小。

堆栈

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

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

堆用于动态内存分配。 分配有一个alloc函数的内存存在于堆上。 所有其他内存分配都不在堆上。 堆启动为大块未使用的内存。 在堆上分配内存时,操作系统会尝试在堆中查找用于分配的空间。 我不打算讨论实际的分配过程是如何工作的。

虚拟内存

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

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

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

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

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

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

进一步阅读

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM