- C和C++安全編碼(原書第2版)
- (美)Robert C.Seacord
- 1593字
- 2020-10-30 17:56:40
2.3.5 棧管理
棧通過維護(hù)自動(dòng)的進(jìn)程狀態(tài)數(shù)據(jù)來支持程序的執(zhí)行。例如,如果一個(gè)程序的主例程(main routine)調(diào)用了a()函數(shù),a()又調(diào)用了b()函數(shù),則b()函數(shù)最終會(huì)將控制權(quán)返回給a(),a()則會(huì)接著將控制權(quán)返回給main()函數(shù),如圖2.6所示。
圖2.6 棧管理
要做到將程序控制返回到正確的位置,就需要將返回地址的序列存儲起來。棧很適合做這項(xiàng)工作,因?yàn)閯?dòng)態(tài)的LIFO數(shù)據(jù)結(jié)構(gòu)在內(nèi)存限制允許的情況下可以支持任意層數(shù)的嵌套。當(dāng)調(diào)用一個(gè)子例程時(shí),調(diào)用例程中的下一條將要執(zhí)行的指令地址被壓入棧中。當(dāng)被調(diào)用的子例程返回時(shí),預(yù)先存儲的返回地址從棧中彈出,程序的執(zhí)行點(diǎn)就跳到該指定位置上,如圖2.7所示。棧維護(hù)的這些信息反映了任何時(shí)刻進(jìn)程的執(zhí)行狀態(tài)。
圖2.7 調(diào)用一個(gè)子例程
除了返回地址以外,棧還被用來保存子例程的參數(shù)以及局部(或自動(dòng))變量。幀(frame)指由函數(shù)調(diào)用引發(fā)的壓入棧的數(shù)據(jù)。當(dāng)前幀的地址被存儲到幀或者基址寄存器中。在x86-32架構(gòu)上,擴(kuò)展基址指針(extended base pointer,ebp)寄存器就是用作此目的。幀指針在棧中是一個(gè)定點(diǎn)的引用。當(dāng)調(diào)用一個(gè)子例程時(shí),調(diào)用端函數(shù)的幀指針同樣被壓入棧,這樣當(dāng)被調(diào)用子例程退出時(shí),幀指針能被重新恢復(fù)。
Intel指令有兩種符號,微軟使用Intel符號。
mov eax, 4 # Intel Notation
GCC使用AT&T符號
mov $4, %eax # AT&T Notation
這兩種指令都把直接數(shù)4移動(dòng)到eax寄存器。例2.4展示了調(diào)用foo(MyInt.MyStrPtr)所得的使用Intel符號表示的x86-32反匯編形式。
例2.4 使用Intel符號表示的反匯編
01 void foo(int, char *); // 函數(shù)原形 02 03 int main(void) { 04 int MyInt=1; // 棧變量位于 ebp-8 05 char *MyStrPtr="MyString"; // 棧變量位于ebp-4 06 /* ... */ 07 foo(MyInt, MyStrPtr); // 調(diào)用 foo 函數(shù) 08 mov eax, [ebp-4] 09 push eax # 把第2 個(gè)參數(shù)壓入棧 10 mov ecx, [ebp-8] 11 push ecx # 把第1 個(gè)參數(shù)壓入棧 12 call foo # 把返回地址壓入棧 13 # 并跳到那個(gè)地址 14 add esp, 8 15 /* ... */ 16 }
調(diào)用由三個(gè)步驟組成,如下所示。
1.第二個(gè)參數(shù)被移到eax寄存器中,接著被壓入棧(第8行和第9行)。注意mov指令是如何利用ebp寄存器來引用參數(shù)以及棧中的局部變量的。
2.第一個(gè)參數(shù)被移到ecx寄存器中,接著被壓入棧(第10行和第11行)。
3.call指令將一個(gè)返回地址(call指令下一條指令的地址)壓入棧,然后將控制轉(zhuǎn)移到foo()函數(shù)(第12行)。
指令指針(eip)指向?qū)⒁獔?zhí)行的下一條指令。當(dāng)執(zhí)行連續(xù)指令時(shí),它會(huì)按照每個(gè)指令的大小自動(dòng)遞增,從而使CPU按順序執(zhí)行序列中的下一條指令。通常情況下,不能直接修改eip,相反,它必須通過諸如跳轉(zhuǎn)(jump),調(diào)用(call)和返回(return)指令間接修改。
當(dāng)控制返回到返回地址時(shí),棧指針(SP)被遞增8個(gè)字節(jié)(第14行)。(在x86-32中,棧指針被命名為esp,e前綴代表“擴(kuò)展”,用來區(qū)分32位棧指針與16位棧指針)。棧指針指向棧的頂端。棧增長的方向取決于具體架構(gòu)上的pop和push指令的實(shí)現(xiàn)(換言之,取決于對棧指針是遞增還是遞減操作)。對于很多流行的架構(gòu),包括x86、SPARC以及MIPS處理器在內(nèi),棧向低地址方向增長。在具有這些架構(gòu)的機(jī)器上,遞增棧指針就意味著從棧中彈出數(shù)據(jù)。
foo()函數(shù)開頭。函數(shù)開頭中包含一個(gè)函數(shù)調(diào)用后所執(zhí)行的指令。foo()函數(shù)的函數(shù)開頭如下所示:
1 void foo(int i, char *name) { 2 char LocalChar[24]; 3 int LocalInt; 4 push ebp # 保存幀指針 5 mov ebp, esp # 子例程的幀指針被設(shè)置為 6 # 當(dāng)前棧指針. 7 sub esp, 28 # 為局部變量分配空間. 8 /* ... */
push指令將保存有指向調(diào)用者棧幀指針的ebp寄存器壓入棧。mov指令將函數(shù)的幀指針(ebp寄存器)指向當(dāng)前棧指針。最后,函數(shù)在棧上為局部變量分配了總共28個(gè)字節(jié)的空間(Local Char占24字節(jié),Local Int占4字節(jié))。
函數(shù)開頭部分執(zhí)行之后,foo()的棧幀如表2.2所示。在x86上,棧向內(nèi)存低地址增長。
foo()的函數(shù)結(jié)尾。函數(shù)結(jié)尾包含將一個(gè)函數(shù)返回給調(diào)用者所執(zhí)行的指令。下面是從foo()函數(shù)返回的函數(shù)結(jié)尾:
1 /* ... */ 2 return; 3 mov esp, ebp # 恢復(fù)棧指針 4 pop ebp # 恢復(fù)幀指針 5 ret # 將返回地址從棧彈出 6 # 并把控制移交給那個(gè)位置 7 }
表2.2 函數(shù)開頭部分執(zhí)行之后,foo()的棧幀
這些返回序列可以看成是前面的函數(shù)開頭的逆序執(zhí)行形式。mov指令將棧指針(esp)從幀指針(ebp)中恢復(fù)。pop指令將調(diào)用者的幀指針從棧中恢復(fù)。ret指令從棧中彈出調(diào)用函數(shù)中的返回地址,并且將控制轉(zhuǎn)移到該位置。
- Learn ECMAScript(Second Edition)
- 自己動(dòng)手寫搜索引擎
- Getting Started with ResearchKit
- JavaScript:Functional Programming for JavaScript Developers
- Vue.js快速入門與深入實(shí)戰(zhàn)
- 實(shí)戰(zhàn)Java程序設(shè)計(jì)
- Learning Bayesian Models with R
- C#程序設(shè)計(jì)(慕課版)
- INSTANT Django 1.5 Application Development Starter
- Extending Puppet(Second Edition)
- 現(xiàn)代C++編程實(shí)戰(zhàn):132個(gè)核心技巧示例(原書第2版)
- R用戶Python學(xué)習(xí)指南:數(shù)據(jù)科學(xué)方法
- 機(jī)器學(xué)習(xí)微積分一本通(Python版)
- Exploring SE for Android
- Visual Basic語言程序設(shè)計(jì)基礎(chǔ)(第3版)