內(nèi)存中的堆和棧到底是什么

文章也同時在個人博客 http://kimihe.com/更新

引言

網(wǎng)絡上關(guān)于內(nèi)存中各區(qū)段作用的文章有很多断国,但不得不吐槽一下施戴,這些文章大多相互引用,內(nèi)容大同小異橄唬,沒有把問題講解清楚确封。

因此除呵,筆者想通過本文,借助匯編的知識爪喘,深入底層講解內(nèi)存模型颜曾。本文的結(jié)構(gòu)如下:

  • 程序在內(nèi)存中的存儲模型
  • 編程過程中常見的幾類變量所在的位置和作用
  • 堆和棧的細節(jié)
  • 起到拋磚引玉作用的底層原理(這意味著你需要自己去深入研究才能真正理解清楚)
  • 實驗驗證

前三小節(jié)是淺嘗輒止地引題,詳細原理請見第四小節(jié)秉剑,最后在第五小節(jié)筆者給出了可實際操作的方法泛豪,幫助大家更直觀地理解。文章可能較長侦鹏,請堅持讀完诡曙,或者擇篇章閱讀柄错。

網(wǎng)上的資料

首先芜抒,筆者羅列出一些質(zhì)量尚可的博客,大家可以先閱讀一下闯狱。之后筆者會針對大家可能存在的疑惑渊涝,從底層來一一講解清楚慎璧。

《堆和棧的區(qū)別 之 數(shù)據(jù)結(jié)構(gòu)和內(nèi)存》

《執(zhí)行可執(zhí)行程序時內(nèi)存分配的方式&&BSS段》

網(wǎng)上很多文章都引用到了下面這段代碼:

int a = 0; //全局初始化區(qū)
char *p1; //全局未初始化區(qū)
main()
{ 
    int b; //棧
    char s[] = "abc"; //棧
    char *p2; //棧
    char *p3 = "123456"; //123456\0在常量區(qū)床嫌,p3在棧上
    static int c =0; //全局(靜態(tài))初始化區(qū)
    p1 = (char *)malloc(10); //堆
    p2 = (char *)malloc(20);  //堆
}

并且如果你搜索關(guān)鍵詞“內(nèi)存堆棧圖”胸私,將很容易找到下面這張圖:


這也是筆者在查閱資料時很不滿的地方厌处,許多文章互相引用,內(nèi)容雷同岁疼,卻沒有把問題的本質(zhì)講清楚阔涉。

因此,筆者將基于上述代碼以及這張圖捷绒,這兩個最常被引用的東西瑰排,來把原理講清楚。

程序在內(nèi)存中的模型

注:本文所指的程序皆在用戶空間運行暖侨,即不涉及操作系統(tǒng)類程序和驅(qū)動程序凶伙。

目前流行的那幾種高級語言,歸根到底它碎,底層實現(xiàn)的思路都是差不多的。而且當今以Intel主流的CPU架構(gòu)(雖然也有ARM)显押,其設計理念也是一脈相承的扳肛。

要講清楚內(nèi)存模型,我們就要深入底層涉及到匯編乘碑,很多高級語言都會經(jīng)歷翻譯到匯編這一中間過程挖息,匯編可以直觀地使用機器指令,是最接近的底層的語言兽肤。

在一個匯編程序中套腹,常常把一個用戶空間程序按習慣分為三個段:.data段,.bss段资铡,.text段电禀。

.data段

.data段包含了已經(jīng)初始化了的數(shù)據(jù)項,這些數(shù)據(jù)在程序開始運行前就擁有自己的值笤休,這些值是可執(zhí)行文件的一部分尖飞,當可執(zhí)行文件被加載到內(nèi)存中用于執(zhí)行時,這些數(shù)據(jù)也被加載到內(nèi)存中店雅。

定義的初始化數(shù)據(jù)越多政基,可執(zhí)行文件就越大,運行它的時候也就需要更長的時間才能將它們從磁盤加載到內(nèi)存闹啦。

一些全局或者靜態(tài)的沮明,且經(jīng)過定義初始化過的變量,就屬于該段窍奋。例如下面代碼中的a荐健,指針p以及b三個變量:

int a = 2;
int *p = &a;

int main () 
{
    static int b = 1;
...
...
    return 0;
}

.bss段

并不是所有數(shù)據(jù)項在程序開始之前都擁有值酱畅,例如你可以定義一個緩沖區(qū)來存在某些數(shù)據(jù),這個緩沖區(qū)是.bss段中定義的摧扇。

分別定義.data段.bss段中的數(shù)據(jù)圣贸,它們一個重要的區(qū)別就是:.data段中的數(shù)據(jù)會添加到可執(zhí)行文件的大小上,而.bss段中的則不會扛稽。即便你給.bss段定義一個1M字節(jié)的緩沖區(qū)吁峻,其最終可執(zhí)行文件大小也幾乎不變(除了大約50個字節(jié)用于描述外)。

程序在加載時知道哪些數(shù)據(jù)項沒有初值在张,它會為這些數(shù)據(jù)項分配空間用含,而具有初值的數(shù)據(jù)線會與其初值一同讀入。

一些全局或者靜態(tài)的帮匾,且未經(jīng)過初始化的變量啄骇,屬于.bss段。例如上文中.data段段的三個變量瘟斜,如果不進行初始化缸夹,就會存儲在本段中。

.text段

以上兩個段都是源程序所需要的數(shù)據(jù)螺句,而真正組成程序的機器指令則存放在.text段中虽惭。一般情況下,在.text段中不進行數(shù)據(jù)項的定義蛇尚。.text段包含名為標號(label)的符號芽唇,這些符號用于標識跳轉(zhuǎn)和調(diào)用程序代碼位置。

程序內(nèi)存中的堆棧

先附上筆者在學習匯編時的一張筆記圖取劫,字比較丑匆笤,望各位見諒。


程序內(nèi)存中的堆棧附筆記

通過該筆者圖大家能夠大概了解內(nèi)存中上述三個段的位置谱邪。至于其中的堆棧以及筆記的含義請繼續(xù)閱讀后文炮捧。

編程過程中常見的幾類變量

觀察最開頭經(jīng)常被引用的那段代碼,其中涵蓋了幾類最常見的變量以及其對應的存儲位置惦银。在上一小節(jié)中寓盗,我們已經(jīng)說明了全局變量和靜態(tài)變量存儲的位置取決于是否進行過初始化。對于堆棧的解釋我們留到下一小節(jié)璧函。這里我們著重講解文字常量區(qū)傀蚌。

文字常量區(qū)

考慮如下代碼:

char *p3 = "123456"; //123456\0在常量區(qū),p3在棧上

這個文字常量區(qū)是什么蘸吓?顯然它與字符串存放有關(guān)善炫。所謂字符串是指位于連續(xù)內(nèi)存區(qū)域中的一個字符序列。字符串通過在起始處關(guān)聯(lián)一個標號來進行定義库继。在匯編中箩艺,常見的字符串定義如下:

MSG: db "something"

它是位于.data段中的窜醉。和.data段中的所有變量一樣,它也是一種已經(jīng)初始化的數(shù)據(jù):帶有一個值艺谆,而不僅僅是一個在將來某時刻用于存放數(shù)據(jù)的內(nèi)存空間榨惰。MSG標號和DB指令在內(nèi)存中指定一個字節(jié)作為字符串的起點,而字符串中的字符數(shù)則告訴匯編編譯器為該字符預留多少個字節(jié)的存儲空間静汤。

但高級語言中的字符串可能要比這里復雜一點琅催,以C語言為例,針對printf函數(shù)中包含的字串虫给。筆者認為其存儲于.data段和.text段之間的一個名叫.rodata段的地方藤抡。即那張常見的“堆棧內(nèi)存圖”中底部綠色的“只讀區(qū)”

大家可以發(fā)現(xiàn)抹估,現(xiàn)在引出了更多的背后細節(jié)缠黍。因此,更為深入的說明我會留到第四個小節(jié):底層的原理药蜻。

堆和棧的細節(jié)

下面進入第三小節(jié)瓷式,講解堆和棧,這也是最開頭代碼中仍未涉及的兩種變量存儲位置语泽。注意:我們在匯編中常說的堆棧蒿往,其實是棧,并不包含堆湿弦。

在此之前,推薦大家看一下stackoverflow的這個問答What and where are the stack and heap?

棧由系統(tǒng)管理腾夯。但是為什么呢颊埃?

首先,棧是一個后進先出(LIFO)結(jié)構(gòu)蝶俱。當把數(shù)據(jù)放入棧時班利,我們把數(shù)據(jù)push進入;當從棧取出數(shù)據(jù)時榨呆,我們把數(shù)據(jù)pop出來罗标。棧隨著數(shù)據(jù)被壓入或者彈出而增長或者減小。最新壓入棧的項被認為是在“棧的頂部”积蜻。當從棧中彈出一個項時闯割,我們得到的是位于棧最頂部的那一個。就像給彈夾上子彈竿拆,只能在頂部進行操作宙拉。

在x86體系中,棧頂由堆棧指針寄存器ESP來標記丙笋,它是一個32位寄存器谢澈,里面存放著最后一個壓入棧頂?shù)捻椀膬?nèi)存地址煌贴。正因為有它,我們才能夠隨時操作到需要的項锥忿。需要注意的是牛郑,棧頂是朝著地內(nèi)存方向增長的。

再來看我拍的照片敬鬓,為于.bss段之間有一段空余內(nèi)存淹朋,C程序經(jīng)常使用這種剩余內(nèi)存空間來為那些為于堆內(nèi)存中的,“已經(jīng)在運行中”的變量分配空間列林。我們常說的堆就存在于這里瑞你。

二者分別存儲什么以及原因

可以看到棧有一個ESP寄存器管理,從底層就實現(xiàn)了一種“自動化”希痴,而堆似乎并沒有額外的東西來幫助管理者甲。

此外,棧的大小需要有一定的限制砌创,棧的增長是向低地址擴展虏缸,如照片中看到的,如果棧不斷地增加嫩实,很可能會與.bss段發(fā)生碰撞刽辙,這是不堪設想的,系統(tǒng)會發(fā)出錯誤并終止程序甲献。

棧應該被看成一個短期存儲數(shù)據(jù)的地方宰缤,存在在棧中的數(shù)據(jù)項沒有名字,只是按照后進先出來操作罷了晃洒。棧經(jīng)晨穑可以用來在寄存器緊張的情況下,臨時存儲一些數(shù)據(jù)球及,并且十分安全氧骤。當寄存器空閑后,我們可以從棧中彈出該數(shù)據(jù)吃引,供寄存器使用筹陵。這種臨時存放數(shù)據(jù)的特性,使得它經(jīng)常用來存儲局部變量镊尺,函數(shù)參數(shù)朦佩,上下文環(huán)境等。

相反庐氮,堆相對于棧吕粗,更加強調(diào)需要進行控制。常見的就是我們手動申請旭愧,手動釋放颅筋。因此可以分配更大的空間宙暇,但開銷也會更多。

底層原理

拋磚引玉

上面三個小節(jié)對于底層原理都是淺嘗輒止议泵,一上來就講得很深入占贫,會增加閱讀負擔。但在這一小節(jié)先口,我們必須講一些底層的東西型奥。不過筆者必須提前聲明,雖然我們會涉及很多底層的知識碉京,但對于整個計算機系統(tǒng)厢汹,這仍舊是冰山一角的知識。筆者在這里更多地是起到拋磚引玉的作用谐宙,完全講解清楚烫葬,可能需要一本書的篇幅,而且筆者水平也很有限凡蜻。這意味著如果你閱讀了本文搭综,有所啟發(fā)想要一探究竟,可能就真的需要自己去探索了划栓。

好書推薦

在這里兑巾,筆者推薦一本書:《深入理解計算機系統(tǒng)(原書第2版)》。我很詫異這本書竟然出到第三版了忠荞,注意第三版針對64位CPU蒋歌,學習的話還是在32位下比較方便,因此推薦第二版委煤。

可執(zhí)行目標文件

程序在運行前以可執(zhí)行文件的形式存儲在磁盤中堂油,我們先來看一下這張圖:

典型的ELF可執(zhí)行目標文件

ELF格式是類UNIX系統(tǒng)中可執(zhí)行文件的常見格式,在眾多表項中我們重點關(guān)注:.text素标,.rodata,.data萍悴,.bss這四個小段(節(jié))头遭。可以看到.text和.rodata屬于只讀存儲器段(代碼段)癣诱,而.data计维,.bss屬于可讀可寫存儲器段(數(shù)據(jù)段)。下面具體說明這四個小段撕予。

.text

存放已編譯程序段機器代碼鲫惶。

.rodata

存放只讀數(shù)據(jù),如C語言中printf語句中的格式串和開關(guān)語句的跳轉(zhuǎn)表实抡。

所謂開關(guān)語句的跳轉(zhuǎn)表欠母,一個典型的例子就是switch(開關(guān))語句的匯編實現(xiàn)欢策,其使用了數(shù)組來映射代碼塊的地址,以此構(gòu)成一張?zhí)D(zhuǎn)表赏淌,相關(guān)的內(nèi)容存儲于只讀數(shù)據(jù)中踩寇。

.data

已初始化的全局C變量。局部C變量在運行時保存在棧中六水,既不出現(xiàn)在.data中俺孙,也不在.bss中。

.bss

未初始化的全局C變量掷贾。如前文匯編語言講解中提到的睛榄,它在目標文件中不占據(jù)實際空間,僅僅是一個占位符想帅。目標文件格式區(qū)分初始化和未初始化變量是為了空間效率:在目標文件中场靴,未初始化變量不需要占據(jù)任何實際的磁盤空間。

值得一提的是博脑,.bss原本是IBM704匯編語言(大約在1957年)中Block Started by Simple指令的首字母縮寫憎乙,并沿用至今。不過在今天叉趣,我們只需要記住區(qū)別.data.bss的最簡單的方法就是把.bss看成是“更好地節(jié)省空間”(Better Save Space)的縮寫泞边!

有一些特例
  • 標記有static靜態(tài)標志的局部變量不在棧中管理,而是根據(jù)有無初始化疗杉,在.data或者.bss中阵谚。
  • 對于GCC編譯器,初始化為0的變量存儲在.bss中烟具。

所以說梢什,如果想真的搞清楚來龍去脈,仍舊需要你自己去閱讀各類文獻朝聋。

加載可執(zhí)行目標文件

可執(zhí)行文件在內(nèi)存中運行時嗡午,有一個運行時存儲器印象,我們來看一下這其中的情況冀痕,如下圖:


Linux 運行時存儲器映像

這張圖涵蓋了本文所講的大多數(shù)知識點。相比于前文的那張匯編語言內(nèi)存圖言蛇,更加細分了僻他。

  • 代碼段總是從地址0x08048000處開始。
  • 數(shù)據(jù)段在接下來的下一個4KB對齊的地址處腊尚。
  • 運行時讀/寫段(數(shù)據(jù)段)之后接下來的第一個4KB對齊的地址處吨拗,并通過malloc庫往上(高地址方向)增長。
  • 中間還有一個段是為共享庫(shared library)保留的。
  • 用戶總是從最大的合法用戶地址開始劝篷,向下增長(低地址方向)
  • 棧上方的段是為操作系統(tǒng)駐留存儲器部分(也就是內(nèi)核)的代碼和數(shù)據(jù)保留的哨鸭。
  • 當程序開始運行時,加載器在可執(zhí)行文件中段頭部表的指引下携龟,將可執(zhí)行文件的相關(guān)內(nèi)容拷貝到代碼段和數(shù)據(jù)段兔跌。

上述的一些名詞,比如共享庫峡蟋,其含義可能需要你自己去研究坟桅。Tips:Windows的.DLL。另外蕊蝗,筆者在參考各類文獻時發(fā)現(xiàn)仅乓,上述諸如數(shù)據(jù)段data段等名字經(jīng)常包含不同的含義蓬戚,且經(jīng)常一個概念有多種說法夸楣。例如只讀段又可以被認為是代碼段。這里大家需要注意我們所說的數(shù)據(jù)段不是指data段子漩,而是data段bss段豫喧。

其實完全細分的名稱會與操作系統(tǒng)和CPU架構(gòu)有關(guān),筆者在這里只能針對共通的地方加以概括幢泼。

動態(tài)存儲器分配

這里重點講一下堆紧显。

動態(tài)存儲器分配維護這一個進程的虛擬存儲器區(qū)域,稱為堆(heap)缕棵。我們假設堆是一個請求二進制零的區(qū)域孵班,它緊接在未初始化的.bss區(qū)域后開始,并向上(高地址方向)生長招驴。對于每一個進程篙程,內(nèi)核維護這一個變量brk(讀作"break"),它指向堆堆頂部别厘。如下圖:

分配器將堆視為一組不同大小的塊(block)的集合來維護虱饿。每一個塊就是一個連續(xù)的虛擬存儲片(chunk),要么已分配触趴,要么是空閑的氮发。已分配的塊顯式地保留為供應用程序使用〉癖危空閑的塊可以用來分配折柠”瞿龋空閑塊保持空閑批狐,直到它顯式地被應用所分配。一個已分配的塊保持已分配的狀態(tài),知道它被釋放嚣艇。這種釋放要么是應用程序顯式執(zhí)行的承冰,要么是存儲器分配起自身隱式執(zhí)行的。

  • 顯式分配器(explicit allocator)食零,要求應用顯式地釋放任何已分配的塊困乒。如C中的malloc和free。C++中的new和delete贰谣。
  • 隱式分配器(implicit allocator)娜搂,要求分配器檢測一個已分配的塊何時不再被程序所使用,就去釋放這個塊吱抚。隱式分配器也叫做垃圾收集器(garbage collector)百宇,而自動釋放未使用的且已被分配的塊的過程叫做垃圾收集(garbage collection)。不用我說秘豹,你們也可能已經(jīng)想到了Java的垃圾回收機制携御。

可見堆也并不是非要人工手動去管理的,文章最開始的一些說法確實是值得推敲的既绕。

對于堆的組織方式啄刹,筆者略提一下其中的一種方式:我們可以將堆組織為一個連續(xù)的已分配塊和空閑塊的序列,我們稱這種結(jié)構(gòu)為隱式空閑鏈表凄贩∈木空閑塊通過頭部中的大小字段隱含地連接著,分配器可以通過遍歷堆中的所有塊怎炊,從而間接遍歷整個空閑塊的集合谭企。如下圖:


隱式空閑鏈表

此外,筆者還想順帶說一個很容易出問題的地方:對于C語言malloc的內(nèi)存區(qū)域评肆,通過一個指針去訪問债查,當該片內(nèi)存被free后,請務必將無效指針設為NULL瓜挽!請務必將無效指針設為NULL盹廷!請務必將無效指針設為NULL!(在iOS對應的OC中久橙,請將對象指針設為nil俄占。)

之所以要這樣,簡而言之淆衷,在分配器的實現(xiàn)細節(jié)中缸榄,在調(diào)用free返回之后,指向分配區(qū)域的指針仍會指向被釋放了的塊(野指針)∽U現(xiàn)在甚带,該塊已經(jīng)實效她肯,如果再通過該野指針去訪問,會出現(xiàn)可怕的后果鹰贵。因此應該確保在該塊被一個新的malloc調(diào)用重新初始化之前晴氨,不再使用該野指針,最好的防治誤用的做法就是給指針置零碉输。

分配器的設計和實現(xiàn)是復雜的籽前,想要一探究竟還需要你自己去研究。

過程和棧幀

這里講述最后一點:棧敷钾。
C語言中的函數(shù)枝哄,對應匯編中的過程。一個過程調(diào)用包括將數(shù)據(jù)(以過程參數(shù)和返回值的形式)和控制從代碼的一部分傳遞到另一部分阻荒。另外膘格,它還必須在進入時為過程的局部變量分配空間,并在退出時釋放這些空間财松。上述的數(shù)據(jù)傳遞瘪贱,局部變量的分配和釋放通過操縱程序幀來實現(xiàn)。

程序用程序棧來支持過程調(diào)用辆毡。機器用棧來傳遞過程參數(shù)菜秦,存儲返回信息,保存寄存器用于以后恢復舶掖,以及本地存儲球昨。為單個過程分配的那部分成為棧幀(stack frame)。下圖描繪了棧幀的通用結(jié)構(gòu)眨攘,最頂端的棧幀以兩個指針界定主慰,寄存器%ebp為幀指針,而寄存器%esp為棧指針鲫售。當程序執(zhí)行時共螺,棧指針可以移動,因此大多數(shù)信息訪問都是相對于幀指針的情竹。(注:%espESP是同一個寄存器的不同說法而已藐不,%ebp同理)

棧幀結(jié)構(gòu)

假設過程P(調(diào)用者)調(diào)用過程Q(被調(diào)用者),則Q的參數(shù)放在P的棧幀中秦效。另外雏蛮,當P調(diào)用Q時凿叠,P中的返回地址被壓入棧中嘶朱,形成P的棧幀的末尾。返回地址就是當P從Q返回時應該繼續(xù)執(zhí)行的地方勋磕。Q的棧幀從保存的幀指針的值(例如寄存器%ebp的副本)開始苔货,后面時保存的其他寄存器的值犀概。

過程Q也用棧來保存其他不能存放在寄存器中的局部變量鹊汛,這樣做的原因如下:

  • 沒有足夠的寄存器存放所有的局部變量。和前文匯編語言部分解釋的原因相同阱冶。
  • 有些局部變量是數(shù)組或者結(jié)構(gòu),因此必須通過數(shù)組或者結(jié)構(gòu)引用來訪問滥嘴。
  • 要對一個局部變量使用地址操作符'&'木蹬,我們必須能夠為它生成一個地址。

另外若皱,Q也會用棧幀來存放它調(diào)用的其它過程的參數(shù)镊叁。參數(shù)n位于相對于%ebp偏移量為4+4n字節(jié)的地方。較大的參數(shù)(如結(jié)構(gòu)體和較大的數(shù)字格式)需要棧上更大的區(qū)域走触。

正如前文所講晦譬,棧向低地址方向增長。棧指針%esp指向棧頂元素互广,可以用push存入數(shù)據(jù)敛腌,用pop取出數(shù)據(jù)。將棧指針的值減小適當?shù)拇笮】梢苑峙錄]有指定初始值的數(shù)據(jù)的空間(加入數(shù)據(jù)棧頂向低地址方向移動)惫皱。類似地像樊,可以通過增加棧指針來釋放空間(取出數(shù)據(jù)棧頂向高地址方向移動)。

實驗環(huán)節(jié)(更新于2016/12/10)

純理論的東西可能讓人沒有實感旅敷,對于各區(qū)段在內(nèi)存中的模型生棍,筆者也一直思索該如何以編程的方式展現(xiàn),今天終于找到了一個好方法媳谁。

首先涂滴,請確保你有一個Linux或類Unix的系統(tǒng)環(huán)境,我們需要用一些命令晴音。筆者是在Mac上實驗柔纵,發(fā)現(xiàn)Mac的命令有點差異,于是ssh到了自己的Ubuntu服務器锤躁。

開始實驗

考慮下述代碼:

#include <stdio.h>
int a = 0; //全局初始化區(qū)
char *p1; //全局未初始化區(qū)


int main()
{
    int b; //棧
    char s[] = "abc"; //棧
    char *p2; //棧
    char *p3 = "123456"; //123456\0在常量區(qū)首量,p3在棧上
    static int c =0; //全局(靜態(tài))初始化區(qū)
    p1 = (char *)malloc(10); //堆
    p2 = (char *)malloc(20);  //堆

    return 0;
}

其實就是那段引用爛了的代碼,筆者補全了int main()return 0进苍。我們以此為藍本加缘,修改一些代碼來觀察生成的可執(zhí)行文件的結(jié)構(gòu),以此讓大家對各區(qū)段的作用有個清晰的認識觉啊。

復制粘貼編輯拣宏,gcc編譯完成后,筆者將其命名為origin杠人。接著在命令行中鍵入:

> size origin

可以看到如下結(jié)果:

   text    data     bss     dec     hex filename
   1384     568      24    1976     7b8 origin

關(guān)注前三個表項勋乾,列出了各區(qū)段的大小宋下,請記住這些大小。

修改一(加入全局變量并初始化)

我們在main()函數(shù)前加入一個全局數(shù)組辑莫,并初始化一下学歧,代碼如下:

#include <stdio.h>
int a = 0; //全局初始化區(qū)
char *p1; //全局未初始化區(qū)

int arr[1000] = {233}; // 修改的代碼在這里,全局數(shù)組已初始化
int main()
{
    int b; //棧
    char s[] = "abc"; //棧
    char *p2; //棧
    char *p3 = "123456"; //123456\0在常量區(qū)各吨,p3在棧上
    static int c =0; //全局(靜態(tài))初始化區(qū)
    p1 = (char *)malloc(10); //堆
    p2 = (char *)malloc(20);  //堆

    return 0;
}

同樣編譯并執(zhí)行size命令枝笨,筆者將其命名為addToDataSection,得到如下結(jié)果:

   text    data     bss     dec     hex filename
   1384    4584      24    5992    1768 addToDataSection

注意到data段大小增加了4000字節(jié)揭蜒,原因就是全局數(shù)組在源碼編譯后横浑,會直接增加到生成的可執(zhí)行文件中,1000個int在32位下就是10004B = 4000B*屉更。

修改二(加入全局變量但不初始化)

接下來徙融,對于增加的全局數(shù)組,去掉其初始化操作瑰谜,代碼如下:

#include <stdio.h>
int a = 0; //全局初始化區(qū)
char *p1; //全局未初始化區(qū)

int arr[1000]; // 全局數(shù)組不進行初始化
int main()
{
    int b; //棧
    char s[] = "abc"; //棧
    char *p2; //棧
    char *p3 = "123456"; //123456\0在常量區(qū)欺冀,p3在棧上
    static int c =0; //全局(靜態(tài))初始化區(qū)
    p1 = (char *)malloc(10); //堆
    p2 = (char *)malloc(20);  //堆

    return 0;
}

編譯,命名為addToBssSection萨脑,執(zhí)行size命令脚猾,結(jié)果如下:

   text    data     bss     dec     hex filename
   1384     568    4064    6016    1780 addToBssSection

可以看到bss段增加了4000字節(jié),別急砚哗,這并不意味著bss段增加的數(shù)組會作用于生成的可執(zhí)行文件龙助,還記得上文說過的嗎?bss段并不增加可執(zhí)行文件大小蛛芥,只是加入少許記錄信息提鸟。我們ls三個文件即可看到區(qū)別:

-rwxrwxr-x 1 ubuntu ubuntu 8658 Dec 10 18:53 origin*
-rwxrwxr-x 1 ubuntu ubuntu 12720 Dec 10 18:55 addToDataSection*
-rwxrwxr-x 1 ubuntu ubuntu 8695 Dec 10 18:56 addToBssSection*

可以看到bss段段增加并未顯著增大可執(zhí)行文件的大小,只有data段才會有所影響仅淑,增加了大約4000字節(jié)称勋。

修改三(加入局部變量)

在這里,我們把全局數(shù)組移入main()函數(shù)中涯竟,代碼如下:

#include <stdio.h>
int a = 0; //全局初始化區(qū)
char *p1; //全局未初始化區(qū)


int main()
{
    int b; //棧
    char s[] = "abc"; //棧
    char *p2; //棧
    char *p3 = "123456"; //123456\0在常量區(qū)赡鲜,p3在棧上
    static int c =0; //全局(靜態(tài))初始化區(qū)
    p1 = (char *)malloc(10); //堆
    p2 = (char *)malloc(20);  //堆


    int arr[1000] = {233};// 內(nèi)部數(shù)組

    return 0;
}

同樣編譯命名為addLocalVariable,執(zhí)行size命令庐船,結(jié)果如下:

   text    data     bss     dec     hex filename
   1456     568      24    2048     800 addLocalVariable

可以看到data段和bss段都沒有什么變化银酬,這說明局部變量不存儲于這兩個段,同時我們ls來查看一下四個文件:

-rwxrwxr-x 1 ubuntu ubuntu 8658 Dec 10 18:53 origin*
-rwxrwxr-x 1 ubuntu ubuntu 12720 Dec 10 18:55 addToDataSection*
-rwxrwxr-x 1 ubuntu ubuntu 8695 Dec 10 18:56 addToBssSection*
-rwxrwxr-x 1 ubuntu ubuntu 8668 Dec 10 18:59 addLocalVariable*

可執(zhí)行文件大小也幾乎不變筐钟,說明局部變量不會保存在其中揩瞪。

修改四(局部變量設置為靜態(tài),根據(jù)是否初始化有不同的結(jié)果)

下面我們進行最后一個修改篓冲,把上述的內(nèi)部數(shù)組加上static關(guān)鍵詞李破,代碼如下:

#include <stdio.h>
int a = 0; //全局初始化區(qū)
char *p1; //全局未初始化區(qū)


int main()
{
    int b; //棧
    char s[] = "abc"; //棧
    char *p2; //棧
    char *p3 = "123456"; //123456\0在常量區(qū)宠哄,p3在棧上
    static int c =0; //全局(靜態(tài))初始化區(qū)
    p1 = (char *)malloc(10); //堆
    p2 = (char *)malloc(20);  //堆


    static int arr[1000] = {233};// 靜態(tài)數(shù)組

    return 0;
}

編譯命名為addStaticVariable,執(zhí)行size命令嗤攻,結(jié)果如下:

   text    data     bss     dec     hex filename
   1384    4584      24    5992    1768 addStaticVariable

仔細觀察毛嫉,發(fā)現(xiàn)結(jié)果與增加全局初始化數(shù)組是一樣的,這說明帶有static關(guān)鍵詞的局部變量并不存放在中妇菱,如果未初始化則存在于data段承粤。

大家可以去掉靜態(tài)數(shù)組的初始化語句,編譯后執(zhí)行size會返回如下:

   text    data     bss     dec     hex filename
   1384     568    4080    6032    1790 addStaticVariable

可以看到原先data段的增量轉(zhuǎn)移到了bss段恶耽。同時在更改前后分別ls一下可以看到如下區(qū)別:

// 數(shù)組進行初始化,編譯在data段中時颜启,其體積計算在可執(zhí)行文件中
-rwxrwxr-x 1 ubuntu ubuntu 12726 Dec 11 22:20 addStaticVariable*

// 數(shù)組未初始化偷俭,編譯在bss段中時,其體積不計算在可執(zhí)行文件中
-rwxrwxr-x 1 ubuntu ubuntu  8702 Dec 13 15:41 addStaticVariable*

大家可以嘗試在C程序中開一個很大的局部變量數(shù)組缰盏,看看編譯器會怎樣提示你涌萤。

之前我曾經(jīng)在Win7的VS上試過,int數(shù)組若含有超過1000個元素口猜,編譯器就總是提示編譯失敗负溪。后來解決的辦法是利用static關(guān)鍵詞,將其編譯進bss段济炎。因為默認的局部變量數(shù)組存放在棧中川抡,一下子開太大會超過Windows的限制。不過顯然须尚,生成的exe文件在執(zhí)行前需要讀取更多的信息崖堤。

實驗小結(jié)

我們可以將上述實驗結(jié)果總結(jié)如下:

  • data段保存在目標文件中
  • bss段不保存在目標文件中(除了記錄bss段在運行時所需的大小)
  • 局部變量并不進入可執(zhí)行文件,它們在運行時創(chuàng)建耐床,一般在棧上密幔。
  • 含有static關(guān)鍵詞修飾的變量根據(jù)有無初始化,存儲于數(shù)據(jù)段撩轰,即data段bss段

題外話

不少公司面試喜歡問內(nèi)存中堆和棧區(qū)別胯甩,以及內(nèi)存模型等等。這里筆者發(fā)現(xiàn)了一個略有trick又不失區(qū)分度的題目:請寫一段代碼堪嫂,用來指明程序中堆棧段的大致位置偎箫。

后續(xù)會公布答案,答案非常簡單也很神奇皆串,請大家積極思考或者留言~

總結(jié)

以上就是筆者對于堆棧以及內(nèi)存模型的一些理解镜廉,在總結(jié)過程中參考不少資料,以確庇拚剑可靠性娇唯。希望能夠解答大家的疑惑齐遵。

感謝閱讀,歡迎分享塔插、關(guān)注梗摇、點贊~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市想许,隨后出現(xiàn)的幾起案子伶授,更是在濱河造成了極大的恐慌,老刑警劉巖流纹,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件糜烹,死亡現(xiàn)場離奇詭異,居然都是意外死亡漱凝,警方通過查閱死者的電腦和手機疮蹦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茸炒,“玉大人愕乎,你說我怎么就攤上這事”诠” “怎么了感论?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長紊册。 經(jīng)常有香客問我比肄,道長,這世上最難降的妖魔是什么囊陡? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任薪前,我火速辦了婚禮,結(jié)果婚禮上关斜,老公的妹妹穿的比我還像新娘示括。我一直安慰自己,他們只是感情好痢畜,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布垛膝。 她就那樣靜靜地躺著,像睡著了一般丁稀。 火紅的嫁衣襯著肌膚如雪吼拥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天线衫,我揣著相機與錄音凿可,去河邊找鬼。 笑死,一個胖子當著我的面吹牛枯跑,可吹牛的內(nèi)容都是我干的惨驶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼敛助,長吁一口氣:“原來是場噩夢啊……” “哼粗卜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起纳击,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤续扔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后焕数,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纱昧,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年堡赔,在試婚紗的時候發(fā)現(xiàn)自己被綠了识脆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡加匈,死狀恐怖存璃,靈堂內(nèi)的尸體忽然破棺而出仑荐,到底是詐尸還是另有隱情雕拼,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布粘招,位于F島的核電站啥寇,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏洒扎。R本人自食惡果不足惜辑甜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望袍冷。 院中可真熱鬧磷醋,春花似錦、人聲如沸胡诗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽煌恢。三九已至骇陈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瑰抵,已是汗流浹背你雌。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留二汛,地道東北人婿崭。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓拨拓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親逛球。 傳聞我的和親對象是個殘疾皇子千元,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容