- C和C++安全編碼(原書第2版)
- (美)Robert C.Seacord
- 1593字
- 2020-10-30 17:56:40
2.3.5 棧管理
棧通過維護自動的進程狀態數據來支持程序的執行。例如,如果一個程序的主例程(main routine)調用了a()函數,a()又調用了b()函數,則b()函數最終會將控制權返回給a(),a()則會接著將控制權返回給main()函數,如圖2.6所示。
圖2.6 棧管理
要做到將程序控制返回到正確的位置,就需要將返回地址的序列存儲起來。棧很適合做這項工作,因為動態的LIFO數據結構在內存限制允許的情況下可以支持任意層數的嵌套。當調用一個子例程時,調用例程中的下一條將要執行的指令地址被壓入棧中。當被調用的子例程返回時,預先存儲的返回地址從棧中彈出,程序的執行點就跳到該指定位置上,如圖2.7所示。棧維護的這些信息反映了任何時刻進程的執行狀態。
圖2.7 調用一個子例程
除了返回地址以外,棧還被用來保存子例程的參數以及局部(或自動)變量。幀(frame)指由函數調用引發的壓入棧的數據。當前幀的地址被存儲到幀或者基址寄存器中。在x86-32架構上,擴展基址指針(extended base pointer,ebp)寄存器就是用作此目的。幀指針在棧中是一個定點的引用。當調用一個子例程時,調用端函數的幀指針同樣被壓入棧,這樣當被調用子例程退出時,幀指針能被重新恢復。
Intel指令有兩種符號,微軟使用Intel符號。
mov eax, 4 # Intel Notation
GCC使用AT&T符號
mov $4, %eax # AT&T Notation
這兩種指令都把直接數4移動到eax寄存器。例2.4展示了調用foo(MyInt.MyStrPtr)所得的使用Intel符號表示的x86-32反匯編形式。
例2.4 使用Intel符號表示的反匯編
01 void foo(int, char *); // 函數原形 02 03 int main(void) { 04 int MyInt=1; // 棧變量位于 ebp-8 05 char *MyStrPtr="MyString"; // 棧變量位于ebp-4 06 /* ... */ 07 foo(MyInt, MyStrPtr); // 調用 foo 函數 08 mov eax, [ebp-4] 09 push eax # 把第2 個參數壓入棧 10 mov ecx, [ebp-8] 11 push ecx # 把第1 個參數壓入棧 12 call foo # 把返回地址壓入棧 13 # 并跳到那個地址 14 add esp, 8 15 /* ... */ 16 }
調用由三個步驟組成,如下所示。
1.第二個參數被移到eax寄存器中,接著被壓入棧(第8行和第9行)。注意mov指令是如何利用ebp寄存器來引用參數以及棧中的局部變量的。
2.第一個參數被移到ecx寄存器中,接著被壓入棧(第10行和第11行)。
3.call指令將一個返回地址(call指令下一條指令的地址)壓入棧,然后將控制轉移到foo()函數(第12行)。
指令指針(eip)指向將要執行的下一條指令。當執行連續指令時,它會按照每個指令的大小自動遞增,從而使CPU按順序執行序列中的下一條指令。通常情況下,不能直接修改eip,相反,它必須通過諸如跳轉(jump),調用(call)和返回(return)指令間接修改。
當控制返回到返回地址時,棧指針(SP)被遞增8個字節(第14行)。(在x86-32中,棧指針被命名為esp,e前綴代表“擴展”,用來區分32位棧指針與16位棧指針)。棧指針指向棧的頂端。棧增長的方向取決于具體架構上的pop和push指令的實現(換言之,取決于對棧指針是遞增還是遞減操作)。對于很多流行的架構,包括x86、SPARC以及MIPS處理器在內,棧向低地址方向增長。在具有這些架構的機器上,遞增棧指針就意味著從棧中彈出數據。
foo()函數開頭。函數開頭中包含一個函數調用后所執行的指令。foo()函數的函數開頭如下所示:
1 void foo(int i, char *name) { 2 char LocalChar[24]; 3 int LocalInt; 4 push ebp # 保存幀指針 5 mov ebp, esp # 子例程的幀指針被設置為 6 # 當前棧指針. 7 sub esp, 28 # 為局部變量分配空間. 8 /* ... */
push指令將保存有指向調用者棧幀指針的ebp寄存器壓入棧。mov指令將函數的幀指針(ebp寄存器)指向當前棧指針。最后,函數在棧上為局部變量分配了總共28個字節的空間(Local Char占24字節,Local Int占4字節)。
函數開頭部分執行之后,foo()的棧幀如表2.2所示。在x86上,棧向內存低地址增長。
foo()的函數結尾。函數結尾包含將一個函數返回給調用者所執行的指令。下面是從foo()函數返回的函數結尾:
1 /* ... */ 2 return; 3 mov esp, ebp # 恢復棧指針 4 pop ebp # 恢復幀指針 5 ret # 將返回地址從棧彈出 6 # 并把控制移交給那個位置 7 }
表2.2 函數開頭部分執行之后,foo()的棧幀
這些返回序列可以看成是前面的函數開頭的逆序執行形式。mov指令將棧指針(esp)從幀指針(ebp)中恢復。pop指令將調用者的幀指針從棧中恢復。ret指令從棧中彈出調用函數中的返回地址,并且將控制轉移到該位置。
- 自己動手寫搜索引擎
- Software Testing using Visual Studio 2012
- Responsive Web Design with HTML5 and CSS3
- 編寫高質量代碼:改善C程序代碼的125個建議
- Scratch 3.0少兒編程與邏輯思維訓練
- React.js Essentials
- 機械工程師Python編程:入門、實戰與進階
- Mastering Swift 2
- 征服RIA
- Java Web開發技術教程
- Functional Kotlin
- Node.js全程實例
- Visual FoxPro程序設計習題集及實驗指導(第四版)
- HTML 5與CSS 3權威指南(第3版·上冊)
- 人工智能算法(卷1):基礎算法