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