摘要:這里分塊講解六函數棧幀的銷毀過程一解析的作用是將棧頂的數據彈出,彈出數據儲存到相應寄存器中。
讀完這篇博客,你可以明白什么?
①局部變量到底是怎么在棧上創建的?
②為什么局部變量不初始化為隨機值?
③函數是怎么傳參的?傳參的先后順序是什么?
④形參和實參是什么關系?
⑤函數調用是怎么實現的?
⑥函數調用后是怎么返回的?? ?
?在這篇博客里,我將帶領大家利用反匯編從底層上理解,不用擔心,都是零基礎入門的。當你學完這篇博客去面試,面試官會非常高興,覺得這小伙子真??。所以學起來吧!
???作者概況:? 就讀南京郵電大學努力學習的大一小伙
???聯系方式:2879377052(QQ小號)? ? ? ? ? ? ?
???資源推薦:C語言從入門到進階
???今日書籍分享:??《深入理解計算機系統》? ? ? ? ? ? ? ? ???
目錄
在C語言中我們可以把寄存器當成指針來看待,他可以指向一塊空間,也可以用來存儲數據。現在向大家介紹以下幾種基本寄存器
寄存器名稱 | 功能 |
①eax | "累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。 |
②ebx | "基地址"(base)寄存器, 在內存尋址時存放基地址。 |
③ecx | 計數器(counter),計數寄存器,用于循環操作,比如重復的字符存儲操作,或者數字統計。 |
④edx | 作為EAX的溢出寄存器,總是被用來放整數除法產生的余數。 |
⑤esi | 源變址寄存器,主要用于存放存儲單元在段內的偏移量。通常在內存操作指令中作為“源地址指針”使用。 |
⑥edi | 目的變址寄存器,主要用于存放存儲單元在段內的偏移量。 |
⑦esp? | 棧指針寄存器(extended stack pointer),其內存放著一個指針,該指針永遠指向棧最上面一個棧幀的棧頂。esp用于堆棧操作,被形象地稱為棧頂指針。堆棧的頂部是地址小的區域,壓入堆棧的數據越多,esp也就越來越小。在32位平臺上,esp每次減少4字節。 |
⑧ebp? | 基址指針寄存器(extended base pointer),其內存放著一個基址指針,該指針永遠指向系統棧最上面一個棧幀的底部。基址指針,又被形參稱作棧底指針,一般與esp 配合使用,可以存取某時刻的esp ,這個時刻就是進入一個函數內后,CPU會將esp 的值賦給ebp ,此時就可以通過ebp 對棧進行操作。 |
main函數其實也是被其他函數調用的,函數調用關系如下:
mainCRTStartup? →? ?__tmainCRTStartup? →? main?
?
(在調用堆棧中即可觀察到)?
①首先我們寫一段簡單的代碼,將代碼的每個過程拆分的盡可能簡單以便于我們對過程的觀察
int add(int x,int y){ int z = 0; z = x + y; return z;}int main(){ int a = 10; int b = 20; int c = add(a,b); return 0;}
?②在調試狀態下轉入反匯編模式
?
(完整連續反匯編代碼請看文章結尾。這里分塊講解)?
?看不懂沒關系,現在逐一解釋
我們知道,函數的調用都需要在棧區上開辟空間,那么我們先來解答幾個問題:
1.什么是棧?
【答】棧是一種數據結構,它按照后進先出的原則存儲數據,先進入的數據被壓入棧底,最后的數據在棧頂,需要讀數據的時候從棧頂開始彈出數據。棧區內存空間的使用是從高地址向低地址處使用的。
2.什么是壓棧?什么是出棧?
【答】一個形象的比喻就是機槍彈夾。壓棧的過程就是壓入一個元素,相當于向機槍彈夾壓入子彈;出棧的過程就是彈出一個元素,相當于子彈彈出來的過程。這正好對應了棧的結構特點——先進入的數據被壓在棧底,后進入的數據在棧頂。
3.為什么一個數據在內存中是“倒著”存放的(注意顯示的是16進制)?
【答】數據的存儲涉及大小端問題:
①什么是大小端
>大端(存儲)模式是指數據的低位保存在內存的高地址中,而數據的高位,保存在內存的低地址中。
>小端(存儲)模式是指數據的低位保存在內存的低地址中,而數據的高位,,保存在內存的高地址中。
?
?②為什么有大小端
存放的內容大于1個字節,必然存在著如何將多字節安排的問題。因此就出現了大小端。具體大小端取決于編譯器,一般為小端存儲。?
1.我們之前提到,main函數是由__tmianCRTStartup函數調用的,所以在創建main函數棧幀前,ebp和esp寄存器維護--tmainCRTStartup的棧區,分別存放指向棧幀的棧頂和棧底。
2.同時注意,棧區上內存的使用是從高地址向低地址處使用的
?
【過程一:??push? ?ebp?】
?【解釋】push指令的作用:它首先減小esp的值,再將源操作數復制到棧地址,每次esp地址減去四字節。最終效果就是在棧頂壓入一個元素,元素的值為ebp的地址。(4個字節)
【補充】內容不隨指令執行而變化的操作數為源操作數,內容隨執行指令而改變的操作數為目標操作數。
?
?
?
?【過程二:? mov? ? ebp,esp?】
?【解釋】mov指令作用:將一個數據從源地址傳送到目標地址,源操作地址的內容不變。最終效果是將esp的地址賦值給ebp。寄存器指向的空間發生改變。
?
?
?【過程三:? ?sub? ? esp,0E4h】
?【解釋】sub指令的作用:減操作指令,地址減去相應的數值。最終效果就是esp的地址減去0E4h。
?
?
?【過程四:push ebx,esi,edi】
?【解釋】前面我們提到過push的過程就壓入元素的過程。那壓入這三個元素有什么用呢?等會就明白。
?
??【過程五:lea? edi,[ebp- 0E4hh]? /? mov ? ecx,9? /? mov ? eax, 0CCCCCCCCh】
【解釋】Load Effective Address,即裝入有效地址的意思,它的操作數就是地址。在這里的效果就是將ebp+FFFFFF1Ch的值賦給edi。勾選“顯示符號名后可以發現:
?
?ebp - 0E4h不正是當初esp - 0E4h時的地址嗎?
?
?執行mov指令后,我們可以發現寄存器eax,ecx的值發生變化。
?
?【過程六:?rep stos ? ?dword ptr es:[edi]?】???
?【解釋】rep指令的作用是:重復后面的指令。stos指令的作用是將eax中的值拷貝到es:edi指向的地址。ecx表示重復操作的次數。dword表示4個字節。所以整句指令的作用是:從edi開始,向高地址方向,將ecx個4字節內存全部修改為eax的值。
?
?(可以看到,執行操作后edi上相應數量的四字節內存被賦值為cc cc cc cc。這也解釋了為什么我們不初始化,變量默認的初始值為cc cc cc cc)
?
?
(完整連續反匯編代碼請看文章結尾。這里分塊講解)??
【過程七:mov? ? ?dword? ptr? [ebp - 14h] ,14h】?
【解析】將ebp - 14地址處的四字節內容修改為14h(10進制中的20),也就是完成了給b賦值為20的動作。下一條語句同理。
?
?
??至此main函數的棧幀創建的準備階段完成。我們準備進入add函數
【過程八:mov? ?eax,dword ptr [ebp -?8h]? ?/? ?push ? ?eax】
【解析】將ebp - 14地址處的數值儲存到eax處;壓棧,將一個數值與eax相等的元素壓入棧中。ebp - 14這個地址好像似曾相識,沒錯,這正是b的地址,所以這條語句的作用實際上即使將b的值傳到eax中。同理,a的值被存儲到ecx中去。
(實際上,上述過程解釋了函數究竟是如何傳參的。傳參的順序和變量創建的順序恰好相反,先傳b再傳a。同時注意函數傳參并不是在add函數棧幀內完成的,而是在main函數的棧幀內完成,通過寄存器eax和ecx實現變量的傳遞)
?
?【過程九:call? ? 00AD1023】?
?【解析】call指令的作用是:將下一條的指令的ip壓入棧中,并轉移到即將被調用的子程序。相當于push ip +? jmp near ptr 標號。我們現在來觀察棧頂的變化。
??在call指令一行們按F11觀察call的作用
?
我們驚奇的發現棧頂自動壓入了一個元素,元素的值為——call指令的下一條指令的地址?。那壓入這個元素有什么用呢?試想,當call指令調用add函數后,我們跳轉到add函數內部,那函數結束后如何保證我們從add函數后面的語句繼續執行呢?靠的就是棧頂壓入的地址,根據這個地址我們可以回到call指令的下一條語句。
?
再次按下F11后我們就跳轉到add函數內部。
?
(完整連續反匯編代碼請看文章結尾。這里分塊講解)??
?這一部分的操作和main函數內完全一致,都是為棧幀的創建做準備。畫一個動圖,不再贅述。
?
?我們現在研究接下來指令。?
?
(完整連續反匯編代碼請看文章結尾。這里分塊講解)??
?【過程一:mov? ? dword ptr [ebp-8],0】
?【解析】將ebp - 8 的地址賦值為0。也就代表著將Z初始化為0
?
?
【過程二:mov? ? eax, dword ptr [ebp+8]】
?【解析】將 ebp + 8 (從上圖觀察)處的值賦值給eax。此時eax儲存著形參a的值
【過程三:add ? ? eax, dword ptr [ebp+0Ch]】
【解析】將eax加上 ebp + 12 處的值。此時eax表示這a+b的值
【過程四:mov? ?? dword ptr [ebp-8], eax】
【解析】將eax儲存的a+b的值傳送到ebp - 8的地址處,也就是賦值給變量Z
【總結】從上面我們也可以加深這樣的認識:形參只是實參的一個臨時拷貝,所以修改形參當然不會影響實參。?
?
?
【過程五:mov? ? ?eax, dword ptr [ebp-8]】
【解析】我們知道函數內創建的臨時變量出函數后被銷毀,那返回值是如何被帶回主函數的呢?靠的就是寄存器eax。這一步的操作就是 return返回 值的過程。
?
(完整連續反匯編代碼請看文章結尾。這里分塊講解)??
【過程一:pop ? ?edi / esi / ebx】
【解析】pop的作用是將棧頂的數據彈出,彈出數據儲存到相應寄存器中。每次pop過程中esp的地址自動加4字節。
?
【過程二:mov? ? esp, ebp?】
?【解析】一句話就回收了為add函數開辟的內存
?
【過程三:pop ebp】
【解析】彈出棧頂的元素,并將彈出的數據儲存到ebp寄存器中。由于此時的棧頂元素事先存入main函數中ebp的地址,所以pop ebp時,將main函數中的ebp元素的地址存入ebp寄存器中
?
?【過程四:ret】
?【解析】ret指令的作用實際相當于 pop IP。在這里實際就是彈出了棧頂事先存儲的add下一條指令的地址,并跳轉到該地址處。
?
?
?
(完整連續反匯編代碼請看文章結尾。這里分塊講解)??
?【過程五:mov ? ? dword ptr [ebp-20h],eax】
?
?【解析】監視我們可以發現ebp - 20所指向的對象就是c,所以這條語句的作用就是將返回值存儲到c中
?
?
(完整連續反匯編代碼請看文章結尾。這里分塊講解)??
?如何理解之后的語句呢,其實和add函數棧幀的銷毀基本是一致的,因為我們前面提到,main函數也是被其他函數調用的。以此類推。
int main(){00031410 push ebp 00031411 mov ebp,esp 00031413 sub esp,0E4h 00031419 push ebx 0003141A push esi 0003141B push edi 0003141C lea edi,[ebp-0E4h] 00031422 mov ecx,39h 00031427 mov eax,0CCCCCCCCh 0003142C rep stos dword ptr es:[edi] int a = 10;0003142E mov dword ptr [a],0Ah int b = 20;00031435 mov dword ptr [b],14h int c = add(a,b);0003143C mov eax,dword ptr [b] 0003143F push eax 00031440 mov ecx,dword ptr [a] 00031443 push ecx 00031444 call _add (0310E6h) 00031449 add esp,8 0003144C mov dword ptr [c],eax return 0;0003144F xor eax,eax }00031451 pop edi 00031452 pop esi 00031453 pop ebx 00031454 add esp,0E4h 0003145A cmp ebp,esp 0003145C call __RTC_CheckEsp (03113Bh) 00031461 mov esp,ebp 00031463 pop ebp 00031464 ret
int add(int x, int y){000313C0 push ebp 000313C1 mov ebp,esp 000313C3 sub esp,0CCh 000313C9 push ebx 000313CA push esi 000313CB push edi 000313CC lea edi,[ebp-0CCh] 000313D2 mov ecx,33h 000313D7 mov eax,0CCCCCCCCh 000313DC rep stos dword ptr es:[edi] int z = 0;000313DE mov dword ptr [z],0 z = x + y;000313E5 mov eax,dword ptr [x] 000313E8 add eax,dword ptr [y] 000313EB mov dword ptr [z],eax return z;000313EE mov eax,dword ptr [z] }000313F1 pop edi 000313F2 pop esi 000313F3 pop ebx 000313F4 mov esp,ebp 000313F6 pop ebp 000313F7 ret
?【注】這是后面補充的便于大家對知識的掌握有一個連貫性,所以在地址上有些不同。關注重要的指令即可。
? ? ? ? 恭喜你學完了函數棧幀的創建和銷毀!覺得不錯可以點個贊。大家最近是不是有一大波考試呢?祝大家考的都會,蒙的全對。
?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/125080.html
摘要:語言深層理解函數中棧幀的創建與銷毀引言引言問題一引言問題二引言問題三一棧的簡單認識內存的簡單了解棧的簡單了解棧的定義棧的結構二寄存器與簡單的匯編指令寄存器的定義寄存器的分類簡單的匯編指令三棧幀的創建于銷毀調試調用堆棧調 ...
摘要:目錄前言由于作者水平有限,文章難免存在謬誤之處,敬請讀者斧正,俚語成篇,懇望指教前言由于作者水平有限,文章難免存在謬誤之處,敬請讀者斧正,俚語成篇,懇望指教作者新曉故知作者新曉故知那些代碼背后的故事那些代碼背后的故事通過 目錄 前言:●由于作者水平有限,文章難免存在謬誤之處,敬請讀者斧正,俚...
摘要:在位機器上,如果有個地址線,那一個指針變量的大小是個字節,才能存放一個地址。就是一個指針變量,也有自己的類型,指針變量的類型我們可以發現指針的定義方式是類型星號。也就是說存儲什么變量類型就用什么指針變量類型。 ...
閱讀 3072·2021-11-25 09:43
閱讀 2251·2021-09-07 10:28
閱讀 3542·2021-08-11 11:14
閱讀 2777·2019-08-30 13:49
閱讀 3544·2019-08-29 18:41
閱讀 1161·2019-08-29 11:26
閱讀 1975·2019-08-26 13:23
閱讀 3371·2019-08-26 10:43