【问题标题】:How does the global variable declaration solve the stack overflow in C?全局变量声明如何解决C中的堆栈溢出?
【发布时间】:2016-12-18 23:40:44
【问题描述】:

我有一些 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];

我的导师告诉我,这个解决方案做同样的工作:将这 2 个变量移到堆中。

但是我在网上查了一些文件。全局变量,不用初始化,就驻留在bss段吧?

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

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

【问题讨论】:

  • 简短回答:您的操作系统和编译器很智能。如果你声明了一个大的全局变量但没有给它赋值(在函数之外),那么存储在可执行文件中的唯一的东西就是变量的大小和位置。加载可执行文件后,操作系统将“分配”足够的内存来保存整个变量(在您访问变量之前,它实际上可能不会分配 RAM)。
  • 你可能应该看看Why is the bss segment required?我不确定这是否是重复的,但它们非常密切相关。
  • @EthanReesor 谢谢,但是一旦我加载可执行文件,它会存储在哪里?作为您的上下文,在堆中?
  • AFAIK 它是特定于操作系统的,但通常 BSS 将放置在与代码相邻的进程(虚拟)内存空间的读写部分中; 绝对不是堆。 BSS 部分和数据部分之间的差异即使在运行时不存在,也可以忽略不计。真正的区别在于这两个部分在可执行文件中的编码方式。数据部分存储可执行文件中变量的值,而 BSS 部分仅存储大小和位置。有兴趣的话,明天(太平洋标准时间)我会就进程内存做一个详细的解答,包括虚拟内存的概述。
  • @EthanReesor 谢谢!感谢您的澄清!现在我想我现在得到了答案 :) 但如果你能提供更多细节,那就太好了

标签: c pointers memory stack


【解决方案1】:

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

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

正式地,应该有关于“标准没有说必须有堆栈”和其他各种次要内容的警告,但这不会改变答案的实质。 bss 部分很小,因为它是一个数字——但是这个数字可以代表非常多的内存。

【讨论】:

  • 谢谢,我明白了,你的最后一句话解释了它。但问题是,它怎么能先在bss段中存储一些字节呢?在这个阶段,程序只编译,不运行。它无法知道这两个全局变量的确切大小应该是多少,对吧?
  • C 程序中的所有全局变量的大小在编译时都是固定的,因此编译器(或者至少是编译器调用的链接器)确实知道如何那些全局变量一定很大。
  • 啊哈,这是一个先决条件。谢谢,一个新问题,是否意味着这两个全局变量首先驻留在 bss 中,然后在运行时中,当我使用它们时,它们将在数据段中获得它们的空间。并且数据段可以动态变化,这样就解决了这个问题?
  • 查看我在另一个comment 中交叉引用的问题。加载程序后,数据段和 bss 段之间没有显着差异;它实际上是所有数据段。唯一的区别是目标文件中的“数据”部分有一些非零初始化(通常也有很多零字节),但 bss 都是零字节。一旦解决了,就没有区别了。
  • 谢谢,仔细阅读。你能检查我是否正确知道吗? 1.所有2个全局变量在运行前都存储在bss段中。 2.程序运行时,系统会初始化bss中的变量,即使它不是由coder分配的。它将为其分配0。 3.由于我们声明了一个大小为20000000的数组,系统会为其分配这么多的0。 4. 这样一来,问题就明显解决了。 5. 这意味着 bss 的大小会变大,因为我们已经隐式地将 0 串分配给它。这一次,我是对的,对吧?
【解决方案2】:

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

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

可执行文件格式

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

进程内存布局

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

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

堆栈

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

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

堆用于动态内存分配。使用alloc 函数之一分配的内存位于堆上。所有其他内存分配都不在堆上。堆以一大块未使用的内存开始。当您在堆上分配内存时,操作系统会尝试在堆内为您的分配寻找空间。我不会详细介绍实际的分配过程是如何工作的。

虚拟内存

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

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

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

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

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

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

进一步阅读

【讨论】:

  • 感谢您提供详细信息。所以我们在 C 中讲的内存模型其实并不是物理内存,对吧?它们都是虚拟内存,稍后系统会使用页表映射到物理内存。并且 bss 部分不会被初始化为全零,它将在访问变量时被初始化。对吗?
  • 正确;虽然我不是 100% 确定 BSS 部分。但是,请记住,这仅适用于现代计算机编程。如果您在 DOS 或微控制器上工作,则只有部分或可能不应用虚拟内存,并且进程内存布局可能会有所不同。例如。大多数微控制器没有虚拟内存,内存保护很少,当然也没有内存映射文件(除非您计算内存映射闪存 IO,但这是不同的)。
【解决方案3】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-02-04
    • 1970-01-01
    • 2014-12-13
    • 1970-01-01
    • 2013-07-11
    • 2016-05-17
    • 2012-05-09
    相关资源
    最近更新 更多