- 編譯系統透視:圖解編譯原理
- 新設計團隊
- 2613字
- 2019-01-04 03:30:29
1.1 一個簡單C程序的運行時結構
解決編程過程中的實際問題,需要透徹了解程序在內存中的運行時結構,而透徹的程度自然成為衡量計算機語言學習水平的重要標準,也成為衡量軟件項目開發水平的重要標準。
C程序運行的核心是函數的執行和調用,它構成了整個C程序運行時結構的基礎框架。這一運行過程主要是在程序指令的驅動以及數據壓棧、清棧的支持下實現的。為了介紹這一過程,我們設計了一個簡單C程序,如下所示:
int fun(int a,int b); int m=10; int main() { inti=4; int j=5; m = fun(i,j); return 0; } int fun(int a,int b) { int c=0; c=a+b; return c; }
程序很簡單,卻凸現了函數調用和執行的最基本情況。我們把此情景展現在內存中,共有三個區域,分別是代碼區、靜態數據區和動態數據區。情景如圖1-1所示。

圖1-1 內存區域特性的總體介紹
代碼區裝載了這個程序所對應的機器指令,main函數和fun函數的機器指令裝載位置如圖1-2所示。

圖1-2 main函數和fun函數在代碼區的位置
全局變量m的數值裝載在靜態數據區中,情景如圖1-3所示。

圖1-3 全局變量m在靜態數據區的位置
程序開始執行前,動態數據區中沒有數據,情景如圖1-4所示。

圖1-4 動態數據區沒有數據
這是因為,只有程序開始執行后,在指令的驅動下,這一區域才會產生數據,壓棧和清棧的工作就是在這一區域完成的,情景如圖1-5所示。

圖1-5 建棧和清棧的情景
程序執行的本質就是代碼區的指令不斷執行,驅使動態數據區和靜態數據區產生數據變化。這一過程需要計算機的管控。下面我們著重介紹對代碼區和動態數據區的管控。CPU中有三個寄存器,分別是eip、ebp和esp,情景如圖1-6所示。

圖1-6 對代碼區和動態數據區的管控
其中eip永遠指向代碼區將要執行的下一條指令,它的管控方式有兩種,一種是“順序執行”,即程序執行完一條指令后自動指向下一條執行;另一種是跳轉,也就是執行完一條跳轉指令后跳轉到指定的位置。
ebp和esp用來管控棧空間,ebp指向棧底,esp指向棧頂,在代碼區中,函數調用、返回和執行伴隨著不斷壓棧和清棧,棧中數據存儲和釋放的原則是后進先出。
內存的劃分及程序執行的總體情況先介紹到這里。下面詳細介紹案例程序的運行時結構。初始情景是這樣的,eip指向main函數的第一條指令,此時程序還沒有運行,棧空間里還沒有數據,ebp和esp指向的位置是程序加載時內核設置的(詳情請看《Linux內核設計的藝術》一書),情景如圖1-7所示。

圖1-7 程序加載時esp和ebp的起始位置
程序開始執行main函數第一條指令,eip自動指向下一條指令。第一條指令的執行,致使ebp的地址值被保存在棧中,保存的目的是本程序執行完畢后,ebp還能返回現在的位置,復原現在的棧。隨著ebp地址值的壓棧,esp自動向棧頂方向移動,它將永遠指向棧頂,情景如圖1-8所示。

圖1-8 保存ebp
程序繼續執行,開始構建main函數自己的棧,ebp原來指向的地址值已經被保存了,它被騰出來了,用來看管main函數的棧底,此時它和esp是重疊的,情景如圖1-9所示。

圖1-9 準備構建main函數的棧
程序繼續執行,eip指向下一條指令,此次執行的是局部變量i的初始化,初始值4被存儲在棧中,esp自動向棧頂方向移動,情景如圖1-10所示。

圖1-10 局部變量i壓棧并初始化
繼續執行下一條指令,局部變量j的初始值5也被壓棧,情景如圖1-11所示。

圖1-11 局部變量j壓棧并初始化
這兩個局部數據都是供main函數自己用的,接下來調用fun函數時壓棧的數據雖然也保存在main函數的棧中,但它們都是供fun函數用的。可以說fun函數的數據,一半在fun函數中,一半在主調函數中,下面來看函數調用時留在main函數中的那一半數據。
先執行傳參的指令,此時參數入棧的順序和代碼中傳參的書寫順序正好相反,參數b先入棧,數值是main函數中局部變量j的數值5,情景如圖1-12所示。

圖1-12 j的數值作為參數被壓棧
程序繼續執行,參數a被壓入棧中,數值是局部變量i的數值4,情景如圖1-13所示。

圖1-13 i的數值作為參數被壓棧
程序繼續執行,此次壓入的是fun函數返回值,將來fun函數返回之后,這里的值會傳遞給m,情景如圖1-14所示。

圖1-14 設定fun函數返回值的位置
還剩最后一步,跳轉到fun函數去執行,這一步分為兩部分動作,一部分是把fun函數執行后的返回地址壓入棧中,以便fun函數執行完畢后能返回到main函數中繼續執行,情景如圖1-15所示。

圖1-15 fun函數執行后的返回地址被壓棧
到這里,函數調用的數據準備工作就完成了。另一部分就是跳轉到被調用的函數的第一條指令去執行,情景如圖1-16所示。

圖1-16 跳轉到fun函數去執行
fun函數開始執行,第一件事就是保存ebp指向的地址值,此時ebp指向的是main函數的棧底,保存的目的是在返回時恢復main函數棧底的位置,這和前面main函數剛開始執行時第一步就保存ebp的地址值的目的是一樣的,情景如圖1-17所示。

圖1-17 fun函數開始執行后先保存main函數棧底地址值
再往后就要構建fun函數的棧了。程序繼續執行,仍然使用騰出來的ebp看管棧底,ebp和esp此時指向相同的位置,情景如圖1-18所示。

圖1-18 準備建立fun函數的棧空間
程序繼續執行,局部變量c開始初始化,入棧,數值為0,這個c就是fun函數的數據,存在于fun函數的棧中,情景如圖1-19所示。

圖1-19 局部變量c被壓棧
此時回顧fun函數的數據,可以發現一半在main函數中,情景如圖1-20所示。

圖1-20 fun函數的數據一半在main函數中
另一半在fun函數中,情景如圖1-21所示。

圖1-21 fun函數的數據的另一半在fun函數中
接下來會執行幾個運算指令,展現對這些數據的應用。以ebp為基點,很容易找到main函數棧和fun函數棧中數據的位置,情景如圖1-22所示。

圖1-22 將加法運算結果賦值給局部變量c
程序繼續執行,fun函數中局部變量c的數據當成返回值返回,情景如圖1-23所示。

圖1-23 c的數值返回
現在fun函數已經執行完畢,要恢復main函數調用fun函數的現場,這一現場包括兩個部分,一部分是main函數的棧要恢復,包括棧頂和棧底,另一部分是要找到fun函數執行后的返回地址,然后再跳轉到那里繼續執行。
我們來看ebp的恢復。前面存儲了ebp的地址值,現在可以把存儲的地址值賦值給ebp,使之指向main函數的棧底,情景如圖1-24所示。

圖1-24 恢復main函數棧底地址值
ebp地址值出棧后,esp自動退棧,指向fun函數執行后的返回地址,之后執行ret指令,即返回指令,把地址值傳給eip,使之指向fun函數執行后的返回地址,情景如圖1-25所示。

圖1-25 返回到main函數中執行
恢復現場以后,把fun函數返回值傳遞給m,情景如圖1-26所示。

圖1-26 將返回值賦值給m
該處理fun函數調用時的傳參和返回值設置了,這兩者已經沒有存在的必要了,全部清棧,情景如圖1-27所示。

圖1-27 參數和返回值清棧
剩下就是main函數的內容了,main函數執行完畢以后,棧也全部清掉。清棧的方式與fun函數執行完后采用的清棧方式一致,情景如圖1-28所示。

圖1-28 main函數清棧
操作系統已經為整個程序執行完的善后工作做了準備,詳情請看《Linux內核設計的藝術》一書。
- Java Web基礎與實例教程(第2版·微課版)
- Magento 2 Theme Design(Second Edition)
- Mastering Articulate Storyline
- Java Web開發就該這樣學
- Frank Kane's Taming Big Data with Apache Spark and Python
- Spring Boot+Vue全棧開發實戰
- Unity 2018 Augmented Reality Projects
- JavaScript程序設計:基礎·PHP·XML
- C語言程序設計教程
- Java Web開發基礎與案例教程
- C++從零開始學(視頻教學版)(第2版)
- Swift 2 Blueprints
- iOS程序員面試筆試真題與解析
- Kali Linux Wireless Penetration Testing Essentials
- 零基礎輕松學Java