摘要:語言深層理解函數中棧幀的創建與銷毀引言引言問題一引言問題二引言問題三一棧的簡單認識內存的簡單了解棧的簡單了解棧的定義棧的結構二寄存器與簡單的匯編指令寄存器的定義寄存器的分類簡單的匯編指令三棧幀的創建于銷毀調試調用堆棧調
我們在學習C語言的過程中,一定會經歷過或者思考過下面的問題:
①當我們C語言中進行printf操作時,有時會出現"燙燙燙"的字眼,那么為什么會出現"燙燙燙"這樣的字眼呢?
②我們在學習與使用函數時,當我們進行函數的值傳遞時,我們被告知當被調函數中,形參的改變,并不會改變傳參變量(實參)的數據內容,那么為什么不會改變傳遞的參數的內容呢?
③我們在第二個問題中,還會被告知,當進行參數值傳遞時,在被調函數中,其實那些參數的值是實參的一份令時拷貝的數據,那么為什么是臨時拷貝的數據呢?
下面我們就來徹底理解這些情況的真實原因與過程。
我們在初期學習C語言時,會學到各種變量,有的是可變變量,有的是不可修改的常量,又會接觸到一些棧,堆的概念,下面的圖例,是為了我們便于我們了解函數棧幀的創建與銷毀的簡單內存圖解:
定義:棧是一種特殊的線性表,其只允許在固定的一端進行插入和刪除元素操作。
①進行數據插入和刪除操作的一端稱為棧頂,另一端稱為棧底。棧中的數據元素遵守后進先出LIFO(Last In First Out)的原則
②壓棧:棧的插入操作叫做進棧/壓棧/入棧,入數據在棧頂。
③出棧:棧的刪除操作叫做出棧。出數據也在棧頂。
①定義:寄存器是中央處理器內的組成部分。寄存器是有限存貯容量的高速存貯部件,它們可用來暫存指令、數據和地址。在中央處理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序計數器(PC)。在中央處理器的算術及邏輯部件中,存器有累加器(ACC)。
寄存器的基本單元是 D觸發器,
按照其用途分為基本寄存器和移位寄存器
基本寄存器是由 D觸發器組成,在 CP 脈沖作用下,每個 D觸發器能夠寄存一位二進制碼。在 D=0 時,寄存器儲存為 0,在 D=1 時,寄存器儲存為 1。在低電平為 0、高電平為 1 時,需將信號源與 D 間連接一反相器,這樣就可以完成對數據的儲存。
需要強調的是,目前大型數字系統都是基于時鐘運作的,其中寄存器一般是在時鐘的邊緣被觸發的,基于電平觸發的已較少使用。(通常說的CPU的頻率就是指數字集成電路的時鐘頻率)
移位寄存器按照移位方向可以分為單向移位寄存器和雙向移位寄存器。單向移位寄存器是由多個 D 觸發器串接而成,在串口 Di 輸入需要儲存的數據,觸發器 FF0 就能夠儲存當前需要儲存數據,在 CP 發出一次時鐘控制脈沖時,串口 Di 同時輸入第二個需要儲存是的數據,而第一個數據則儲存到觸發器 FF1 中。雙向移位寄存器按圖中方式排列,調換連接端順序,可以控制寄存器向左移位,增加控制電路可以使寄存器右移,這樣構成雙向移位寄存器。
②特點:
寄存器又分為內部寄存器與外部寄存器,所謂內部寄存器,其實也是一些小的存儲單元,也能存儲數據。但同存儲器相比,寄存器又有自己獨有的特點:
a、寄存器位于CPU內部,數量很少,僅十四個
b、寄存器所能存儲的數據不一定是8bit,有一些寄存器可以存儲16bit數據,對于386/486處理器中的一些寄存器則能存儲32bit數據
c、每個內部寄存器都有一個名字,而沒有類似存儲器的地址編號。
③用途:
1.可將寄存器內的數據執行算術及邏輯運算
2.存于寄存器內的地址可用來指向內存的某個位置,即尋址
3.可以用來讀寫數據到電腦的周邊設備。
寄存器 | 用途 |
---|---|
eax | 累加寄存器,相對于其他寄存器,在運算方面比較常用 |
ebx | 基地址寄存器,作為內存偏移指針使用 |
edi | 在內存操作指令中作為“目的地址”使用 |
esi | 在內存操作指令中作為“源地址指針”使用 |
ecx | 計數器,用于特定的技術 |
edx | 作為EAX的溢出寄存器,(除法產生的余數) |
esp | 指針的寄存器,用于堆棧操作。被形象地稱為棧頂指針,堆棧的頂部是地址小的區域,壓入堆棧的數據越多,ESP也就越來越小。在32位平臺上,ESP每次減少4字節。 |
ebp | 基址指針,指棧的棧底指針 |
匯編指令 | 對應操作 |
---|---|
push | 壓棧 |
pop | 出棧 |
move | move A,B 將A移動到當前B的位置 |
call | 將程序的執行交給其他代碼段(即函數的調用) |
lea | 加載有效地址 |
ret | 子程序返回指令 |
sub | 減法操作 |
add | 加法操作 |
經過上面關于棧與寄存器的簡單解釋,我們開始本文章的重點,對函數中的棧幀的創建與銷毀的理解
為了方便我們理解,我們用下面的代碼進行講解:
#include int Add(int x, int y){ int z = 0; z = x + y; return z;}int main(){ int a = 20; int b = 10; int c = 0; c = Add(a, b); printf("%d/n", c); return 0;}
這里我們使用后的VS2013編譯器進行講解,當我們使用不同的編譯器去執行代碼與進行理解時,會有一些偏差
①我們首先按F10進行調試,進入調試之后,我們按照下面的操作,調用堆棧窗口:
②當我們進入堆棧窗口之后,我們在程序中anF10進行程序,當程序進行結束之后,我們的堆棧窗口出現下圖:上圖的內容是告訴我們:
main函數在VS2013中任然是被調函數,main函數首先被_tmainCRTSartup()函數調用,而_tmainCRTSartup()函數被mainCRTSartup函數調用;
③根據上述內容,我們可以得出關于當前程序的棧幀簡圖:
①現在我們為了深入了解我們的棧幀是如何創建與銷毀的過程,我們按照下圖步驟進行操作:
此時我們獲取了程序的反匯編語言,通過反匯編語言,我們才能夠深入了解程序的進行過程與棧幀的創建與銷毀過程,也正是通過對反匯編的分析,我們才能夠解決我們引言中的問題;
①這里我們在進行對main函數的反匯編語言進行分析前,我們先對補充一些內容的講解:
a、在函數的棧幀中,ebp和esp這兩個寄存器是存放地址的,也是這兩個地址用來維護函數的棧幀;
b、在esp和ebp這兩個寄存器中,esp寄存器存放的是函數的棧頂地址,也就是棧頂指針;ebp寄存器存放的是函數的棧底地址,也就是棧底指針;
舉例(如圖):
②我們對main函數的反匯編語言進行分析:
壓棧操作:將ebp移動到當前棧的棧底的位置,然后esp指針會自動上移,指向壓棧進入棧的ebp
也就是將t_mainSRTSartup函數的ebp的值存放到棧頂,占用一個內存空間;
舉例(如圖):
③我們對main函數的反匯編語言進行分析:
將ebp指針指向當前esp指針指向的位置
通過監視窗口,從地址進行觀察,可以確定,ebp指向了esp指向的位置,此時兩個指針的地址值相同
④我們對main函數的反匯編語言進行分析:
將esp的值減少0E4h(也就是將esp指針上移)
⑤我們對main函數的反匯編語言進行分析:
實現步驟同上,從棧頂壓入三個元素,分別為ebx、esi、edi(我們暫時不用理會這三個寄存器)
⑥我們對main函數的反匯編語言進行分析:
我們勾選lea(load effective address)加載有效地址,將ebp-0E4h這個地址上存儲的內容加載到edi中
⑦我們對main函數的反匯編語言進行分析:
mov操作,將"39h"存儲到ecx中;將"0CCCCCCCCh"存儲到eax中
⑧我們對main函數的反匯編語言進行分析:
從edi開始,向下39h個內存空間的數據全部轉化為0CCCCCCCCh
引言問題的解決:
在這里我們回顧初學C語言時,我們當時創建一個變量,卻沒有對其賦值時,或者在打印字符串時,沒有找到’/0’時,我們卻將其打印,得到結果中含有"燙燙燙"的字樣,這種情況的出現,其實就是打印的初始化值"0CCCCCCCCh"
⑨我們對main函數的反匯編語言進行分析:
mov操作的執行,將"14h"存儲到ebp-8的位置、將"0Ah"存儲到ebp-4h的位置、將"0"存儲到ebp-20h的位置
①我們對Add函數的反匯編語言進行分析:
將ebp-14h位置上的值移動到eax的位置上,即將ebp-14h位置上的值傳遞給eax;
壓棧操作,將eax從棧頂壓入;
將將ebp-8位置上的值移動到ecx的位置上,即將ebp-8位置上的值傳遞給ecx;
壓棧操作,將ecx從棧頂壓入;
同時這里操作,也是我們Add函數傳參的操作
②我們對Add函數的反匯編語言進行分析:
call操作的執行,我們會壓棧壓入call指令的下一條指令的地址
這一步執行的原因,是當我們Add函數執行完之后, 我們返回結束時,要繼續執行我們的下一條指令,所以我們進行記錄我們Add函數的下一條指令的地址
我們在這里按F11進入Add函數,顯示如下:
我們對Add函數的反匯編語言進行分析:
當我們進入Add函數之后,我們發現Add函數和main函數步驟相同,需要先為Add函數進行初始化,ebp、esp的函數棧幀維護等,也就是需要創建Add函數所需要的棧幀,數據類型初始化。
①壓棧,壓入ebp,其中這個ebp就是當前main函數的ebp
②mov操作,將esp的值傳遞給ebp,那么ebp就指向當前的esp指向的位置
③sub操作,將esp減去0CCh,使esp指針上移;push操作,壓棧壓入ebx、esi、edi三個元素
④lea、mov、mov、rep stos的執行步驟與main函數的初始化步驟相同
這時,我們對代碼的內容近一步分析
這里的兩個地址,我們通過圖解分析,可以得出,之前的ecx、eax就是實現我們的傳參步驟,分別為形參a’,b’;
⑤此時,我們將ebp+8位置的值傳遞給eax,此時eax = 20;然后再將ebp+0Ch位置的值添加到eax中,此時eax = 30;再然后,將eax的值傳遞到ebp-8的位置;
引言問題的解決:
形參是實參的一份臨時拷貝,當我們在還沒有在main函數中執行到Add()函數時,我們已將b,a的數值壓棧進入棧中,而這兩個壓棧進入棧中的數據,就是b、a的一份臨時拷貝,當我們執行到Add函數時,我們就找回了之前壓棧存儲的b、a的值,所以我們說形參是實參的一份臨時拷貝
當我們在函數中改變這份臨時拷貝的數值時,對我們原本的實參并不會改變,因為函數中改變的只是實參的一份臨時拷貝,所以我們說值傳遞的形參改變,并不會改變實參
⑥將ebp-8的值傳遞給eax中
⑦pop操作,將棧頂元素彈走;這三行代碼的執行效果為,依次將棧頂的元素彈到edi寄存器中、esi寄存器中,ebx寄存器中
此時的棧頂情況如圖:
即將edi元素彈回,存儲到edi寄存器中,esi、ebx同理。
執行三次pop操作之后
此時,我們的Add函數的任務已經執行結束,獲得的return值也已經存儲到eax中,那么這個時候,Add函數的函數棧幀就沒有存在的必要了,我們需要對這一段空間進行回收
⑧讓esp指針指向ebp的位置
⑨將當前棧頂的元素彈出,彈到ebp寄存器中
而當前的棧頂元素存儲的內容就是main函數的ebp,那么效果就相當于
此時,Add函數的棧幀已經收回,我們又回到了main函數的棧幀中
⑩前面的過程經歷之后,我們的call指令就已經執行完畢,但我們任然需要繼續call函數之后的步驟,而此時ret操作的執行,就是讓我們從call指令執行完之后,返回到我們之前存儲的call指令的下一個指令地址的地方。繼續執行call指之后的指令。
現在我們又回到了call指令之后此時main函數指針中的形參20、10就沒有用處了,那么我們就要將這兩個空間進行回收
將"esp"+8,即就是將esp指針下移八個字節
此時,也就是形參銷毀的真實時刻
將eax的值傳遞給ebp-20h中
我們回顧main函數中的棧幀
那么將eax的值傳遞給ebp-20h中,則
這里我們可以得出,當我們調用返回函數時,返回值都是先存放到寄存器中,然后當我們真的返回到調用函數中時,再從寄存器中讀取這個返回值
到此,余下的部分關于main函數的棧幀空間的銷毀與Add函數相同,就不在此敘述了,執行的步驟與前面分析內容形似。
以上就是我對函數中棧幀的創建與銷毀的個人理解
上述內容如果有錯誤的地方,還麻煩各位大佬指教【膜拜各位了】【膜拜各位了】
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/120822.html
摘要:這里分塊講解六函數棧幀的銷毀過程一解析的作用是將棧頂的數據彈出,彈出數據儲存到相應寄存器中。 ?前言? 讀完這篇博客,你可以明白什么? ①局部變量到底是怎么在棧上創建的? ②為什么局部變量不初始化為隨機值? ③函數是怎么傳參的?傳參的先后順序是什么? ④形參和實參是什么關系? ⑤函數調用是怎...
摘要:目錄前言由于作者水平有限,文章難免存在謬誤之處,敬請讀者斧正,俚語成篇,懇望指教前言由于作者水平有限,文章難免存在謬誤之處,敬請讀者斧正,俚語成篇,懇望指教作者新曉故知作者新曉故知那些代碼背后的故事那些代碼背后的故事通過 目錄 前言:●由于作者水平有限,文章難免存在謬誤之處,敬請讀者斧正,俚...
閱讀 2891·2021-10-14 09:42
閱讀 1244·2021-09-24 10:32
閱讀 2952·2021-09-23 11:21
閱讀 2839·2021-08-27 13:10
閱讀 3327·2019-08-29 18:41
閱讀 2194·2019-08-29 15:16
閱讀 1193·2019-08-29 13:17
閱讀 892·2019-08-29 11:22