簡體   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