一篇文章搞定堆棧原理

前方高能預(yù)警,本文較長(zhǎng),涉及到的原理性東西較多,建議收藏方便后期查看。


1.png

我們常常說(shuō)堆棧堆棧,但是堆和棧其實(shí)是完全不同的兩個(gè)概念。棧其實(shí)完全是為了函數(shù)調(diào)用而設(shè)計(jì)的,那么函數(shù)調(diào)用如何通過(guò)棧實(shí)現(xiàn)的呢?不用函數(shù)調(diào)用方式,棧在行為上有什么區(qū)別呢?筆者曾經(jīng)去京東面試一個(gè)高級(jí)開(kāi)發(fā)職位,面試官寫(xiě)了一個(gè)從1累加到100的C程序,讓筆者寫(xiě)出對(duì)應(yīng)的匯編代碼,如果你熟悉棧的原理,其實(shí)這個(gè)題目就并不難,相反,函數(shù)通過(guò)棧如何實(shí)現(xiàn)的,這確實(shí)是我們廣大開(kāi)發(fā)者必須掌握的基礎(chǔ)知識(shí)之一,因?yàn)橐彩敲嬖囍杏糜诳疾煲粋€(gè)開(kāi)發(fā)者基礎(chǔ)水平的一個(gè)常見(jiàn)題型。
好了,那什么是棧呢?下面是正文:

系統(tǒng)棧的工作原理:

1、內(nèi)存的不同用途:

如果您關(guān)注網(wǎng)絡(luò)安全問(wèn)題,那么一定聽(tīng)過(guò)緩沖區(qū)溢出這個(gè)術(shù)語(yǔ)。簡(jiǎn)單說(shuō)來(lái):緩沖區(qū)溢出就是在大緩沖區(qū)中的數(shù)據(jù)向小緩沖區(qū)復(fù)制的過(guò)程中,由于沒(méi)有注意小緩沖區(qū)的邊界,“撐爆”了較小的緩沖區(qū),從而沖掉了和小緩沖區(qū)相鄰內(nèi)存區(qū)域的其他數(shù)據(jù)而引起的內(nèi)存問(wèn)題。緩沖溢出是最常見(jiàn)的內(nèi)存錯(cuò)誤之一,也是攻擊者入侵系統(tǒng)時(shí)所用到的最強(qiáng)大、最經(jīng)典的一類漏洞利用方式。
成功地利用緩沖區(qū)溢出漏洞可以修改內(nèi)存中變量的值,甚至可以劫持進(jìn)程,執(zhí)行惡意代碼,最終獲得主機(jī)的控制權(quán)。要透徹地理解這種攻擊方式,我們需要回顧一些計(jì)算機(jī)體系架構(gòu)方面的基礎(chǔ)知識(shí),搞淸楚CPU、寄存器、內(nèi)存是怎樣協(xié)同工作而讓程序流暢執(zhí)行的。
根據(jù)不同的操作系統(tǒng),一個(gè)進(jìn)程可能被分配到不同的內(nèi)存區(qū)域去執(zhí)行。但是不管什么樣的操作系統(tǒng)、什么樣的計(jì)算機(jī)架構(gòu),進(jìn)程使用的內(nèi)存都可以按照功能大致分成以下4個(gè)部分。
(1)代碼區(qū):這個(gè)區(qū)域存儲(chǔ)著被裝入執(zhí)行的二進(jìn)制機(jī)器代碼,處理器會(huì)到這個(gè)區(qū)域取指令并執(zhí)行。
(2)數(shù)據(jù)區(qū):用于存儲(chǔ)全局變量等。
(3)堆區(qū):進(jìn)程可以在堆區(qū)動(dòng)態(tài)地請(qǐng)求一定大小的內(nèi)存,并在用完之后歸還給堆區(qū)。動(dòng)態(tài)分配內(nèi)存和回收內(nèi)存是堆區(qū)的特點(diǎn)。
(4)棧區(qū):用于動(dòng)態(tài)地存儲(chǔ)函數(shù)之間的調(diào)用關(guān)系,以保證被調(diào)用函數(shù)在返回時(shí)恢復(fù)到母函數(shù)中繼續(xù)執(zhí)行。
題外話:這種簡(jiǎn)單的內(nèi)存劃分方式是為了讓您能夠更容易地理解程序的運(yùn)行機(jī)制入理解計(jì)算機(jī)系統(tǒng)》一書(shū)中有更詳細(xì)的關(guān)于內(nèi)存使用的論述,如有興趣可參考之。
在Windows平臺(tái)下,高級(jí)語(yǔ)言寫(xiě)出的程序經(jīng)過(guò)編譯鏈接,最終會(huì)變成所謂的PE文件。當(dāng)PE文件被裝載運(yùn)行后,就成了所謂的進(jìn)程。
PE文件代碼段中包含的二進(jìn)制級(jí)別的機(jī)器代碼會(huì)被裝入內(nèi)存的代碼區(qū)(.text),處理器將到內(nèi)存的這個(gè)區(qū)域一條一條地取出指令和操作數(shù),并送入算術(shù)邏輯單元進(jìn)行運(yùn)算;如果代碼中請(qǐng)求開(kāi)辟動(dòng)態(tài)內(nèi)存,則會(huì)在內(nèi)存的堆區(qū)分配一塊大小合適的區(qū)域返回給代碼區(qū)的代碼使用;當(dāng)函數(shù)調(diào)用發(fā)生時(shí),函數(shù)的調(diào)用關(guān)系等信息會(huì)動(dòng)態(tài)地保存在內(nèi)存的棧區(qū),以供處理器在執(zhí)行完被調(diào)用函數(shù)的代碼時(shí),返冋母函數(shù)。這個(gè)協(xié)作過(guò)程如圖2.1.1所示。


2.1.1.png

如果把計(jì)算機(jī)看成一個(gè)有條不紊的1:廠,我們可以得到如下類比。

  • CPU是完成工作的工人。
  • 數(shù)據(jù)區(qū)、堆區(qū)、棧區(qū)等則是用來(lái)存放原料、半成品、成品等各種東西的場(chǎng)所。
  • 存在代碼區(qū)的指令則告訴CPU要做什么,怎么做,到哪里去領(lǐng)原材料,用什么工具來(lái)做,做完以后把成品放到哪個(gè)貨艙去。
  • 值得一提的是,棧除了扮演存放原料、半成品的倉(cāng)庫(kù)之外,它還是車(chē)間調(diào)度主任的辦公室。
    程序中所使用的緩沖區(qū)可以是堆區(qū)、棧區(qū)和存放靜態(tài)變景的數(shù)據(jù)區(qū)。緩沖區(qū)溢出的利用方法和緩沖區(qū)到底屬于上面哪個(gè)內(nèi)存區(qū)域密不可分。

2、棧與系統(tǒng)棧:

從計(jì)算機(jī)科學(xué)的角度來(lái)看,棧指的是一種數(shù)據(jù)結(jié)構(gòu),是一種先進(jìn)后出的數(shù)據(jù)表。棧的最常見(jiàn)操作有兩種:壓棧(PUSH)、彈棧(POP):用于標(biāo)識(shí)棧的屬性也有兩個(gè):棧頂(TOP)、棧底(BASE)。
可以把棧想象成一摞撲克牌。

  • PUSH:為棧增加一個(gè)元素的操作叫做PUSH,相當(dāng)于在這摞撲克牌的最上面再放上—張。
  • POP:從棧中取出一個(gè)元素的操作叫做POP,相當(dāng)于從這摞撲克牌取出最上面的一張。
  • TOP:標(biāo)識(shí)棧頂位置,并且是動(dòng)態(tài)變化的。每做一次PUSH操作,它都會(huì)自增1;相反,每做一次POP操作,它會(huì)自減1。棧頂元素相當(dāng)于撲克牌最上面一張,只有這張牌的花色是當(dāng)前可以看到的。
  • BASE:標(biāo)識(shí)棧底位置,它記錄著撲克牌最下面一張的位置。BASE用于防止??蘸罄^續(xù)彈棧(牌發(fā)完時(shí)就不能再去揭牌了)。很明顯,一般情況下,BASE是不會(huì)變動(dòng)的。
    內(nèi)存的棧區(qū)實(shí)際上指的就是系統(tǒng)棧。系統(tǒng)棧由系統(tǒng)自動(dòng)維護(hù),它用于實(shí)現(xiàn)高級(jí)語(yǔ)言中函數(shù)的調(diào)用。對(duì)于類似C語(yǔ)言這樣的高級(jí)語(yǔ)言,系統(tǒng)棧的PUSH、POP等堆棧平衡細(xì)節(jié)是透明的。
    —般說(shuō)來(lái),只有在使用匯編語(yǔ)言開(kāi)發(fā)程序的時(shí)候,才需要和它直接打交道。
    好,下面重點(diǎn)部分來(lái)了。

3、函數(shù)調(diào)用時(shí)發(fā)生了什么?

我們下面就來(lái)探究一下高級(jí)語(yǔ)言中函數(shù)的調(diào)用和遞歸等性質(zhì)是怎樣通過(guò)系統(tǒng)棧巧妙實(shí)現(xiàn)的。請(qǐng)看如下代碼:

int func_B(int arg_B1, int arg_B2)
{
    int var_B1;
    int var_B2;
    var_B1 = arg_B1 + arg_B2;
    var_B2 = arg_B1 - arg_B2;
    return var_B1 * var_B2;
}
int func_A(int arg_A1, int arg_A2)
{
      int var_A;
      var_A = func_B(arg_A1, arg_A2) + arg_A1;
      return var_A;
}
int main(int argc, char** argv, char** envp)
{
      int var_main;
      var_main = func_A(3, 4);
      return 0;
}

這段代碼經(jīng)過(guò)編譯器編譯后,各個(gè)函數(shù)對(duì)應(yīng)的機(jī)器指令在代碼區(qū)中可能是這樣分布的,如圖2.1.2所示:
2.1.2.png

根據(jù)操作系統(tǒng)的不問(wèn)、編譯器和編譯選項(xiàng)的不同,同一文件不同函數(shù)的代碼在內(nèi)存代碼區(qū)中的分布可能相鄰,也可能相離甚遠(yuǎn),可能先后有序,也可能無(wú)序;但它們都在同一個(gè)PE文件的代碼所映射的一個(gè)“節(jié)”里。我們可以簡(jiǎn)單地把它們?cè)趦?nèi)存代碼區(qū)中的分布位置理解成是散亂無(wú)關(guān)的。

當(dāng)CPU在執(zhí)行調(diào)用func_A函數(shù)的時(shí)候,會(huì)從代碼區(qū)中main函數(shù)對(duì)應(yīng)的機(jī)器指令的區(qū)域跳轉(zhuǎn)到func_A函數(shù)對(duì)應(yīng)的機(jī)器指令區(qū)域,在那里取指并執(zhí)行;當(dāng)函數(shù)執(zhí)行完閉,需要返會(huì)的時(shí)候,又會(huì)跳回到main函數(shù)對(duì)應(yīng)的指令區(qū)域,緊接著調(diào)用func_A后面的指令繼續(xù)執(zhí)行main函數(shù)的代碼。在這個(gè)過(guò)程中,CPU的取指軌跡如圖2.1.3所示。
2.1.3.png
那么CPU是怎么知道要去func_A的代碼區(qū)取指,在執(zhí)行完func_A后又是怎么知道跳回到main函數(shù)(而不是func_B的代碼區(qū))的呢?這些跳轉(zhuǎn)地址我們?cè)贑語(yǔ)言中并沒(méi)有直接說(shuō)明,CPU是從哪里獲得這些函數(shù)的調(diào)用及返回的信息的呢?
原來(lái),這些代碼區(qū)中精確的跳轉(zhuǎn)都是在與系統(tǒng)棧巧妙地配臺(tái)過(guò)程中完成的。當(dāng)函數(shù)被調(diào)用時(shí),系統(tǒng)棧會(huì)為這個(gè)函數(shù)開(kāi)辟一個(gè)新的棧幀,并把它壓入棧中。這個(gè)棧幀中的內(nèi)存空間被它所屬的函數(shù)獨(dú)占,正常情況下是不會(huì)和別的函數(shù)共享的。當(dāng)函數(shù)返回時(shí),系統(tǒng)棧會(huì)彈出該函數(shù)所對(duì)應(yīng)的棧幀。

如圖2.1.4所示,在函數(shù)調(diào)用的過(guò)程中,伴隨的系統(tǒng)棧中的操作如下。
2.1.4.png
  • 在main函數(shù)中調(diào)用func_A的時(shí)候,首先在自己的棧幀中壓入函數(shù)返回地址,然后為func_A創(chuàng)建新棧幀并壓入系統(tǒng)棧。
  • 在func__A調(diào)用func_B的時(shí)候,同樣先在自己的棧幀中壓入函數(shù)返回地址,然后為func_B創(chuàng)建新棧幀并壓入系統(tǒng)棧。
  • 在func_B返回時(shí),func_B的棧幀被彈出系統(tǒng)棧,func_A棧幀中的返回地址被“露”在棧頂,此時(shí)處理器按照這個(gè)返回地址重新跳到func_A代碼區(qū)中執(zhí)行。
  • 在func_A返同時(shí),func_A的棧幀被彈出系統(tǒng)棧.macn函數(shù)棧幀中的返回地址被“露” 在棧頂,此時(shí)處理器按照這個(gè)返回地址跳到main函數(shù)代碼區(qū)中執(zhí)行。

4、寄存器與函數(shù)棧幀

每一個(gè)函數(shù)獨(dú)占自己的棧幀空間。當(dāng)前正在運(yùn)行的函數(shù)的棧幀總是在棧頂。CPU系統(tǒng)提供兩個(gè)特殊的寄存器用于標(biāo)識(shí)位于系統(tǒng)棧頂端的棧幀。
(1) ESP:棧指針寄存器(extended stack pointer),其內(nèi)存放著一個(gè)指針,該指針永遠(yuǎn)指向系統(tǒng)棧地上面-個(gè)棧幀的棧頂。
(2) EBP:基址指針寄存器(extended base pointer)-其內(nèi)存放著一個(gè)指針,該指針永遠(yuǎn)指向系統(tǒng)棧展上面一個(gè)棧幀的底部。
注意:EBP指向當(dāng)前位于系統(tǒng)棧最上邊一個(gè)棧幀的底部,而不是系統(tǒng)棧的底部。嚴(yán)格說(shuō)來(lái),“棧幀底部”和“棧底”是不同的概念,本文在敘述中將堅(jiān)特使用“棧幀底部”這一提法以示區(qū)別;ESP所指的棧幀頂部和系統(tǒng)棧的頂部是同一個(gè)位置,所以后面敘述中并不嚴(yán)格區(qū)分“棧幀頂部”和“棧頂”的概念。請(qǐng)您注意這里的差異,不要產(chǎn)生概念混淆。

寄存器對(duì)棧幀的標(biāo)識(shí)作用如圖2.1.5所示
2.1.5.png
函數(shù)棧幀:ESP和EBP之間的內(nèi)存空間為當(dāng)前棧幀.EBP標(biāo)識(shí)了當(dāng)前棧幀的底部.ESP標(biāo)識(shí)了當(dāng)前棧幀的頂部。
在函數(shù)棧幀中,一般包含以下幾類重要信息。
(1) 局部變量:為函數(shù)局部變量開(kāi)辟的內(nèi)存空間。
(2)棧幀狀態(tài)值:保存前棧幀的頂部和底部(實(shí)際上只保存前棧幀的底部,前棧幀的頂部可以通過(guò)堆棧平衡計(jì)算得到),用于在本幀被彈出后恢復(fù)出上一個(gè)棧幀。

(3) 函數(shù)返回地址:保存當(dāng)前函數(shù)調(diào)用前的“斷點(diǎn)”信息,也就是函數(shù)調(diào)用前的指令位置,以便在函數(shù)返回時(shí)能夠恢復(fù)到函數(shù)被調(diào)用前的代碼區(qū)中繼續(xù)執(zhí)行指令。
除了與棧相關(guān)的寄存器外,您還需要記住另一個(gè)至關(guān)重要的寄存器。
EIP:指令寄存器(Extended Instruction Pointer),其內(nèi)存放著一個(gè)指針,該指針永遠(yuǎn)指向下一條等待執(zhí)行的指令地址,可以說(shuō)如果控制了EIP寄存器的內(nèi)容,就控制了進(jìn)程——我們讓EIP指向哪里,CPU就會(huì)去執(zhí)行哪里的指令。

5、函數(shù)調(diào)用約定與相關(guān)指令

函數(shù)調(diào)用約定描述了函數(shù)傳遞參數(shù)方式和棧協(xié)同工作的技術(shù)細(xì)節(jié)。不同的操作系統(tǒng)、不同的語(yǔ)言、不同的編譯器在實(shí)現(xiàn)函數(shù)調(diào)用時(shí)的原理雖然基本相同,但具體的調(diào)用約定還是有差別的。這包括參數(shù)傳遞方式,參數(shù)入棧順序是從右向左還是從左向古,函數(shù)返回時(shí)恢復(fù)堆棧平衡的操作在子函數(shù)中進(jìn)行還是在母函數(shù)中進(jìn)行。表2-1-1列出了幾種調(diào)用方式之間的差異。
表2-1-1.png

具體的,對(duì)于Visual C++來(lái)說(shuō),可支持以下3中函數(shù)調(diào)用約定,如表2-1-2所示
表2-1-2.png
如果要明確使用某一種調(diào)用約定,只需要在函數(shù)前加上調(diào)用約定的聲明即可,否則默認(rèn)情況下會(huì)使用__cdecl的調(diào)用方式。
除了上邊的參數(shù)入棧方向和恢復(fù)棧平衡操作位置的不同之外,參數(shù)傳遞有時(shí)也會(huì)有所不同。例如,每一個(gè)c++類成員函數(shù)都有一個(gè)this指針,在Wndows平臺(tái)中,這個(gè)指針一般是用ECX寄存器來(lái)傳遞的,但如果用GCC編譯器編譯,這個(gè)指針會(huì)作為最后一個(gè)參數(shù)壓入棧中。注意:同一段代碼用不同的編譯選項(xiàng)、不同的編譯器編譯鏈接后,得到的可執(zhí)行文件會(huì)有很多不同。

函數(shù)調(diào)用大致包括以下幾個(gè)步驟.
(1) 參數(shù)入棧:將參數(shù)從右向左依次壓入系統(tǒng)棧中。
(2)返回地址入棧:將當(dāng)前代碼區(qū)調(diào)用指令的下一條指令地址壓入棧中,供函數(shù)返回時(shí)繼續(xù)執(zhí)行。
(3) 代碼區(qū)跳轉(zhuǎn):處理器從當(dāng)前代碼區(qū)跳轉(zhuǎn)到被調(diào)用函數(shù)的入口處。
(4)棧幀調(diào)整:具體包括。
保存當(dāng)前棧幀狀態(tài)值,已備后面恢復(fù)本棧幀時(shí)使用(EBP入棧):
將當(dāng)前棧幀切換到新棧幀(將ESP值裝入EBP.更新棧幀底部):
給新棧幀分配空間(把ESP減去所需空間的大小,抬高棧頂):

對(duì)于__stdcall調(diào)用約定,函數(shù)調(diào)用時(shí)用到的指令序列大致如下。
2.1.6.png
上面這段用于函數(shù)調(diào)用的指令在棧中引起的變化如圖2.1.7所示。
2.1.7.png

題外話.png
類似地,函數(shù)返回的步驟如下:
(1) 保存返回值:通常將函數(shù)的返回值保存在寄存器EAX中。
(2) 彈出當(dāng)前棧幀,恢復(fù)上一個(gè)棧幀。
具體包括:
  • 在堆棧平衡的基礎(chǔ)上,給ESP加上棧幀的大小,降低棧頂,回收當(dāng)前棧幀的空間。
  • 將當(dāng)前棧幀底部保存的前棧幀EBP值彈入EBP寄存器,恢復(fù)出上一個(gè)棧幀。
  • 將函數(shù)返回地址彈給EIP寄存器。
    (3) 跳轉(zhuǎn):按照函數(shù)返回地址跳同母函數(shù)中繼續(xù)執(zhí)行。
    還是以C語(yǔ)言和Win32平臺(tái)為例,函數(shù)返回時(shí)的相關(guān)的指令序列如下。
    add esp, xxx ;降低棧頂,回收當(dāng)前的棧幀
    pop ebp ;將上一個(gè)棧幀底部位置恢復(fù)至ebp.
    retn ;這條指令有兩個(gè)功能:
    a)彈出當(dāng)前棧頂元素,即彈出棧幀中的返回地址。棧幀恢復(fù)工作完成。
    b)讓處理器跳轉(zhuǎn)到彈出的返回地址,恢復(fù)調(diào)用前的代碼區(qū)。
    按照這樣的函數(shù)調(diào)用約定組織起來(lái)的系統(tǒng)棧結(jié)構(gòu)如圖2.1.8所示:


    2.1.8.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容