文章也同時在個人博客 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ì)量尚可的博客,大家可以先閱讀一下闯狱。之后筆者會針對大家可能存在的疑惑渊涝,從底層來一一講解清楚慎璧。
網(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)存中上述三個段的位置谱邪。至于其中的堆棧以及筆記的含義請繼續(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格式是類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)存中運行時嗡午,有一個運行時存儲器印象,我們來看一下這其中的情況冀痕,如下圖:
這張圖涵蓋了本文所講的大多數(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ù)信息訪問都是相對于幀指針的情竹。(注:%esp與ESP是同一個寄存器的不同說法而已藐不,%ebp同理)
假設過程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)注梗摇、點贊~