官术网_书友最值得收藏!

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內核設計的藝術》一書。

主站蜘蛛池模板: 佛学| 自治县| 泽库县| 扎兰屯市| 特克斯县| 阿城市| 宝兴县| 邢台县| 宣化县| 财经| 珲春市| 新乡市| 绿春县| 家居| 伊通| 塔城市| 皋兰县| 石屏县| 铜鼓县| 色达县| 彭州市| 南平市| 淳安县| 鄂托克前旗| 班玛县| 开化县| 阿克| 防城港市| 龙州县| 耿马| 东兴市| 苏尼特左旗| 元谋县| 顺平县| 宁阳县| 五家渠市| 乌审旗| 昭通市| 嘉义市| 南郑县| 威海市|