前方高能預(yù)警三椿,本文較長,涉及到的原理性東西較多葫辐,建議收藏方便后期查看搜锰。
我們常常說堆棧堆棧,但是堆和棧其實是完全不同的兩個概念耿战。棧其實完全是為了函數(shù)調(diào)用而設(shè)計的蛋叼,那么函數(shù)調(diào)用如何通過棧實現(xiàn)的呢?不用函數(shù)調(diào)用方式剂陡,棧在行為上有什么區(qū)別呢狈涮?筆者曾經(jīng)去京東面試一個高級開發(fā)職位,面試官寫了一個從1累加到100的C程序鸭栖,讓筆者寫出對應(yīng)的匯編代碼歌馍,如果你熟悉棧的原理,其實這個題目就并不難晕鹊,相反松却,函數(shù)通過棧如何實現(xiàn)的,這確實是我們廣大開發(fā)者必須掌握的基礎(chǔ)知識之一溅话,因為也是面試中用于考察一個開發(fā)者基礎(chǔ)水平的一個常見題型晓锻。
好了买窟,那什么是棧呢囊骤?下面是正文:
系統(tǒng)棧的工作原理:
1辨嗽、內(nèi)存的不同用途:
如果您關(guān)注網(wǎng)絡(luò)安全問題羹蚣,那么一定聽過緩沖區(qū)溢出這個術(shù)語糠悼。簡單說來:緩沖區(qū)溢出就是在大緩沖區(qū)中的數(shù)據(jù)向小緩沖區(qū)復(fù)制的過程中,由于沒有注意小緩沖區(qū)的邊界痊末,“撐爆”了較小的緩沖區(qū)檀蹋,從而沖掉了和小緩沖區(qū)相鄰內(nèi)存區(qū)域的其他數(shù)據(jù)而引起的內(nèi)存問題。緩沖溢出是最常見的內(nèi)存錯誤之一灿里,也是攻擊者入侵系統(tǒng)時所用到的最強(qiáng)大关炼、最經(jīng)典的一類漏洞利用方式。
成功地利用緩沖區(qū)溢出漏洞可以修改內(nèi)存中變量的值匣吊,甚至可以劫持進(jìn)程儒拂,執(zhí)行惡意代碼,最終獲得主機(jī)的控制權(quán)色鸳。要透徹地理解這種攻擊方式社痛,我們需要回顧一些計算機(jī)體系架構(gòu)方面的基礎(chǔ)知識,搞淸楚CPU命雀、寄存器蒜哀、內(nèi)存是怎樣協(xié)同工作而讓程序流暢執(zhí)行的。
根據(jù)不同的操作系統(tǒng)吏砂,一個進(jìn)程可能被分配到不同的內(nèi)存區(qū)域去執(zhí)行撵儿。但是不管什么樣的操作系統(tǒng)、什么樣的計算機(jī)架構(gòu)狐血,進(jìn)程使用的內(nèi)存都可以按照功能大致分成以下4個部分淀歇。
(1)代碼區(qū):這個區(qū)域存儲著被裝入執(zhí)行的二進(jìn)制機(jī)器代碼,處理器會到這個區(qū)域取指令并執(zhí)行匈织。
(2)數(shù)據(jù)區(qū):用于存儲全局變量等浪默。
(3)堆區(qū):進(jìn)程可以在堆區(qū)動態(tài)地請求一定大小的內(nèi)存,并在用完之后歸還給堆區(qū)缀匕。動態(tài)分配內(nèi)存和回收內(nèi)存是堆區(qū)的特點纳决。
(4)棧區(qū):用于動態(tài)地存儲函數(shù)之間的調(diào)用關(guān)系,以保證被調(diào)用函數(shù)在返回時恢復(fù)到母函數(shù)中繼續(xù)執(zhí)行弦追。
題外話:這種簡單的內(nèi)存劃分方式是為了讓您能夠更容易地理解程序的運(yùn)行機(jī)制入理解計算機(jī)系統(tǒng)》一書中有更詳細(xì)的關(guān)于內(nèi)存使用的論述岳链,如有興趣可參考之。
在Windows平臺下劲件,高級語言寫出的程序經(jīng)過編譯鏈接掸哑,最終會變成所謂的PE文件。當(dāng)PE文件被裝載運(yùn)行后零远,就成了所謂的進(jìn)程苗分。
PE文件代碼段中包含的二進(jìn)制級別的機(jī)器代碼會被裝入內(nèi)存的代碼區(qū)(.text),處理器將到內(nèi)存的這個區(qū)域一條一條地取出指令和操作數(shù),并送入算術(shù)邏輯單元進(jìn)行運(yùn)算牵辣;如果代碼中請求開辟動態(tài)內(nèi)存摔癣,則會在內(nèi)存的堆區(qū)分配一塊大小合適的區(qū)域返回給代碼區(qū)的代碼使用;當(dāng)函數(shù)調(diào)用發(fā)生時,函數(shù)的調(diào)用關(guān)系等信息會動態(tài)地保存在內(nèi)存的棧區(qū)择浊,以供處理器在執(zhí)行完被調(diào)用函數(shù)的代碼時戴卜,返冋母函數(shù)。這個協(xié)作過程如圖2.1.1所示琢岩。
如果把計算機(jī)看成一個有條不紊的1:廠投剥,我們可以得到如下類比。
- CPU是完成工作的工人担孔。
- 數(shù)據(jù)區(qū)江锨、堆區(qū)、棧區(qū)等則是用來存放原料糕篇、半成品啄育、成品等各種東西的場所。
- 存在代碼區(qū)的指令則告訴CPU要做什么拌消,怎么做挑豌,到哪里去領(lǐng)原材料,用什么工具來做拼坎,做完以后把成品放到哪個貨艙去浮毯。
- 值得一提的是,棧除了扮演存放原料泰鸡、半成品的倉庫之外,它還是車間調(diào)度主任的辦公室壳鹤。
程序中所使用的緩沖區(qū)可以是堆區(qū)盛龄、棧區(qū)和存放靜態(tài)變景的數(shù)據(jù)區(qū)。緩沖區(qū)溢出的利用方法和緩沖區(qū)到底屬于上面哪個內(nèi)存區(qū)域密不可分芳誓。
2余舶、棧與系統(tǒng)棧:
從計算機(jī)科學(xué)的角度來看,棧指的是一種數(shù)據(jù)結(jié)構(gòu)锹淌,是一種先進(jìn)后出的數(shù)據(jù)表匿值。棧的最常見操作有兩種:壓棧(PUSH)、彈棧(POP):用于標(biāo)識棧的屬性也有兩個:棧頂(TOP)赂摆、棧底(BASE)挟憔。
可以把棧想象成一摞撲克牌。
- PUSH:為棧增加一個元素的操作叫做PUSH烟号,相當(dāng)于在這摞撲克牌的最上面再放上—張绊谭。
- POP:從棧中取出一個元素的操作叫做POP,相當(dāng)于從這摞撲克牌取出最上面的一張汪拥。
- TOP:標(biāo)識棧頂位置达传,并且是動態(tài)變化的。每做一次PUSH操作,它都會自增1宪赶;相反宗弯,每做一次POP操作,它會自減1搂妻。棧頂元素相當(dāng)于撲克牌最上面一張罕伯,只有這張牌的花色是當(dāng)前可以看到的。
- BASE:標(biāo)識棧底位置叽讳,它記錄著撲克牌最下面一張的位置追他。BASE用于防止棧空后繼續(xù)彈棧(牌發(fā)完時就不能再去揭牌了)岛蚤。很明顯邑狸,一般情況下,BASE是不會變動的涤妒。
內(nèi)存的棧區(qū)實際上指的就是系統(tǒng)棧单雾。系統(tǒng)棧由系統(tǒng)自動維護(hù),它用于實現(xiàn)高級語言中函數(shù)的調(diào)用她紫。對于類似C語言這樣的高級語言硅堆,系統(tǒng)棧的PUSH、POP等堆棧平衡細(xì)節(jié)是透明的贿讹。
—般說來渐逃,只有在使用匯編語言開發(fā)程序的時候,才需要和它直接打交道民褂。
好茄菊,下面重點部分來了。
3赊堪、函數(shù)調(diào)用時發(fā)生了什么面殖?
我們下面就來探究一下高級語言中函數(shù)的調(diào)用和遞歸等性質(zhì)是怎樣通過系統(tǒng)棧巧妙實現(xiàn)的。請看如下代碼:
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)過編譯器編譯后哭廉,各個函數(shù)對應(yīng)的機(jī)器指令在代碼區(qū)中可能是這樣分布的脊僚,如圖2.1.2所示:根據(jù)操作系統(tǒng)的不問、編譯器和編譯選項的不同遵绰,同一文件不同函數(shù)的代碼在內(nèi)存代碼區(qū)中的分布可能相鄰辽幌,也可能相離甚遠(yuǎn),可能先后有序街立,也可能無序舶衬;但它們都在同一個PE文件的代碼所映射的一個“節(jié)”里。我們可以簡單地把它們在內(nèi)存代碼區(qū)中的分布位置理解成是散亂無關(guān)的赎离。
原來陨溅,這些代碼區(qū)中精確的跳轉(zhuǎn)都是在與系統(tǒng)棧巧妙地配臺過程中完成的。當(dāng)函數(shù)被調(diào)用時绍在,系統(tǒng)棧會為這個函數(shù)開辟一個新的棧幀门扇,并把它壓入棧中。這個棧幀中的內(nèi)存空間被它所屬的函數(shù)獨(dú)占偿渡,正常情況下是不會和別的函數(shù)共享的臼寄。當(dāng)函數(shù)返回時,系統(tǒng)棧會彈出該函數(shù)所對應(yīng)的棧幀溜宽。
如圖2.1.4所示吉拳,在函數(shù)調(diào)用的過程中,伴隨的系統(tǒng)棧中的操作如下坑质。
- 在main函數(shù)中調(diào)用func_A的時候合武,首先在自己的棧幀中壓入函數(shù)返回地址,然后為func_A創(chuàng)建新棧幀并壓入系統(tǒng)棧涡扼。
- 在func__A調(diào)用func_B的時候,同樣先在自己的棧幀中壓入函數(shù)返回地址盟庞,然后為func_B創(chuàng)建新棧幀并壓入系統(tǒng)棧吃沪。
- 在func_B返回時,func_B的棧幀被彈出系統(tǒng)棧什猖,func_A棧幀中的返回地址被“露”在棧頂票彪,此時處理器按照這個返回地址重新跳到func_A代碼區(qū)中執(zhí)行。
- 在func_A返同時不狮,func_A的棧幀被彈出系統(tǒng)棧.macn函數(shù)棧幀中的返回地址被“露” 在棧頂降铸,此時處理器按照這個返回地址跳到main函數(shù)代碼區(qū)中執(zhí)行。
4摇零、寄存器與函數(shù)棧幀
每一個函數(shù)獨(dú)占自己的棧幀空間推掸。當(dāng)前正在運(yùn)行的函數(shù)的棧幀總是在棧頂。CPU系統(tǒng)提供兩個特殊的寄存器用于標(biāo)識位于系統(tǒng)棧頂端的棧幀。
(1) ESP:棧指針寄存器(extended stack pointer)谅畅,其內(nèi)存放著一個指針登渣,該指針永遠(yuǎn)指向系統(tǒng)棧地上面-個棧幀的棧頂。
(2) EBP:基址指針寄存器(extended base pointer)-其內(nèi)存放著一個指針毡泻,該指針永遠(yuǎn)指向系統(tǒng)棧展上面一個棧幀的底部胜茧。
注意:EBP指向當(dāng)前位于系統(tǒng)棧最上邊一個棧幀的底部,而不是系統(tǒng)棧的底部仇味。嚴(yán)格說來呻顽,“棧幀底部”和“棧底”是不同的概念,本文在敘述中將堅特使用“棧幀底部”這一提法以示區(qū)別丹墨;ESP所指的棧幀頂部和系統(tǒng)棧的頂部是同一個位置廊遍,所以后面敘述中并不嚴(yán)格區(qū)分“棧幀頂部”和“棧頂”的概念。請您注意這里的差異带到,不要產(chǎn)生概念混淆昧碉。
在函數(shù)棧幀中揽惹,一般包含以下幾類重要信息被饿。
(1) 局部變量:為函數(shù)局部變量開辟的內(nèi)存空間。
(2)棧幀狀態(tài)值:保存前棧幀的頂部和底部(實際上只保存前棧幀的底部搪搏,前棧幀的頂部可以通過堆棧平衡計算得到)狭握,用于在本幀被彈出后恢復(fù)出上一個棧幀。
(3) 函數(shù)返回地址:保存當(dāng)前函數(shù)調(diào)用前的“斷點”信息疯溺,也就是函數(shù)調(diào)用前的指令位置论颅,以便在函數(shù)返回時能夠恢復(fù)到函數(shù)被調(diào)用前的代碼區(qū)中繼續(xù)執(zhí)行指令。
除了與棧相關(guān)的寄存器外囱嫩,您還需要記住另一個至關(guān)重要的寄存器恃疯。
EIP:指令寄存器(Extended Instruction Pointer),其內(nèi)存放著一個指針墨闲,該指針永遠(yuǎn)指向下一條等待執(zhí)行的指令地址今妄,可以說如果控制了EIP寄存器的內(nèi)容,就控制了進(jìn)程——我們讓EIP指向哪里鸳碧,CPU就會去執(zhí)行哪里的指令盾鳞。
5、函數(shù)調(diào)用約定與相關(guān)指令
函數(shù)調(diào)用約定描述了函數(shù)傳遞參數(shù)方式和棧協(xié)同工作的技術(shù)細(xì)節(jié)瞻离。不同的操作系統(tǒng)腾仅、不同的語言、不同的編譯器在實現(xiàn)函數(shù)調(diào)用時的原理雖然基本相同套利,但具體的調(diào)用約定還是有差別的推励。這包括參數(shù)傳遞方式鹤耍,參數(shù)入棧順序是從右向左還是從左向古,函數(shù)返回時恢復(fù)堆棧平衡的操作在子函數(shù)中進(jìn)行還是在母函數(shù)中進(jìn)行吹艇。表2-1-1列出了幾種調(diào)用方式之間的差異惰蜜。除了上邊的參數(shù)入棧方向和恢復(fù)棧平衡操作位置的不同之外,參數(shù)傳遞有時也會有所不同撑碴。例如撑教,每一個c++類成員函數(shù)都有一個this指針,在Wndows平臺中醉拓,這個指針一般是用ECX寄存器來傳遞的伟姐,但如果用GCC編譯器編譯,這個指針會作為最后一個參數(shù)壓入棧中亿卤。注意:同一段代碼用不同的編譯選項愤兵、不同的編譯器編譯鏈接后,得到的可執(zhí)行文件會有很多不同排吴。
函數(shù)調(diào)用大致包括以下幾個步驟.
(1) 參數(shù)入棧:將參數(shù)從右向左依次壓入系統(tǒng)棧中秆乳。
(2)返回地址入棧:將當(dāng)前代碼區(qū)調(diào)用指令的下一條指令地址壓入棧中,供函數(shù)返回時繼續(xù)執(zhí)行钻哩。
(3) 代碼區(qū)跳轉(zhuǎn):處理器從當(dāng)前代碼區(qū)跳轉(zhuǎn)到被調(diào)用函數(shù)的入口處屹堰。
(4)棧幀調(diào)整:具體包括。
保存當(dāng)前棧幀狀態(tài)值街氢,已備后面恢復(fù)本棧幀時使用(EBP入棧):
將當(dāng)前棧幀切換到新棧幀(將ESP值裝入EBP.更新棧幀底部):
給新棧幀分配空間(把ESP減去所需空間的大小扯键,抬高棧頂):
(1) 保存返回值:通常將函數(shù)的返回值保存在寄存器EAX中近范。
(2) 彈出當(dāng)前棧幀,恢復(fù)上一個棧幀延蟹。
具體包括:
- 在堆棧平衡的基礎(chǔ)上评矩,給ESP加上棧幀的大小,降低棧頂阱飘,回收當(dāng)前棧幀的空間斥杜。
- 將當(dāng)前棧幀底部保存的前棧幀EBP值彈入EBP寄存器虱颗,恢復(fù)出上一個棧幀。
-
將函數(shù)返回地址彈給EIP寄存器蔗喂。
(3) 跳轉(zhuǎn):按照函數(shù)返回地址跳同母函數(shù)中繼續(xù)執(zhí)行忘渔。
還是以C語言和Win32平臺為例,函數(shù)返回時的相關(guān)的指令序列如下缰儿。
add esp, xxx ;降低棧頂畦粮,回收當(dāng)前的棧幀
pop ebp ;將上一個棧幀底部位置恢復(fù)至ebp.
retn ;這條指令有兩個功能:
a)彈出當(dāng)前棧頂元素,即彈出棧幀中的返回地址乖阵。棧幀恢復(fù)工作完成宣赔。
b)讓處理器跳轉(zhuǎn)到彈出的返回地址,恢復(fù)調(diào)用前的代碼區(qū)瞪浸。
按照這樣的函數(shù)調(diào)用約定組織起來的系統(tǒng)棧結(jié)構(gòu)如圖2.1.8所示: