
目錄
- 前言
- 暫存器
- 主函式是被誰呼叫的?呼叫邏輯是什么
- 主函式堆疊幀的創建
- 堆疊頂指標,堆疊底指標
- 函式堆疊幀創建的預備作業
- Add函式是怎么被呼叫的
前言
學習函式堆疊幀之前我們得了解一下什么是暫存器,因為關于函式堆疊幀的知識是需要了解暫存器的知識做一個內容鋪墊的,每次測驗采用的環境是在VS2017下
暫存器
暫存器與函式堆疊幀的關系
ebp,esp這兩個暫存器存放的是地址,這兩個地址是用來維護函式堆疊幀的,簡單了解一下暫存器呢有六個

本次測驗代碼
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x,int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = 0;
ret = Add(a,b);
printf("%d", ret);
return 0;
}
首先我們得明白每一次呼叫函式都會為該函式創建一個函式堆疊幀的,那么這塊空間由誰來維護呢,前面我們講到函式堆疊幀是兩個暫存器esp 和 ebp
來維護的,下面看記憶體圖

通常我們把ebp 呢我們稱之為堆疊底指標,esp
呢我們又稱之為堆疊頂指標,為什么有堆疊頂指標和堆疊底指標的說法呢?我們都知道堆疊的使用習慣是先使用高地址空間,再使用低地址空間,每次函式呼叫壓堆疊的時候,我的兩個指標都會指向這塊空間,
每次開辟空間的時候,在堆疊中是不是總是向上使用的,那么堆疊頂指標是不是就總指向這塊空間的頭,而堆疊底指標就是指向這塊空間的底,
主函式是被誰呼叫的?呼叫邏輯是什么
接下來先從main函式開始開刀,我們都知道寫好的代碼都要放到主函式中去運行,然道主函式就這么牛?總是得經過他得同意?答案并不是,其實main函式的上頭也有老大呼叫main,接下來我們通過除錯觀察他的呼叫邏輯,有獵奇心理的小伙伴請跟著來

這里我們等待主函式回傳到被呼叫處,去尋找他的源頭

在這里我們把呼叫堆疊打開就行

在這個地方我們和直觀的就看到了主函式被呼叫處,另外要說明的是在VS2017上好像不能觀察到主函式的呼叫邏輯,博主目前采用的是VC2010學習版,回到主題


不知道大家觀察到沒有,這里的__tmainCRTStartup()其實就是我的mainret函式,其實很明顯在我們的呼叫堆疊中就可以看出
在這里我們選中這一行雙擊跳轉到mainret函式的被呼叫處

我們再往上翻

原來是這個函式呼叫了我們的__tmainCRTStartup(),從而函式的呼叫邏輯我們就搞明白了,
我們再梳理一遍呼叫邏輯

主函式堆疊幀的創建
既然我們知道了這一點,來么就可以再完善一下剛剛的記憶體圖

直到這里大家把對函式堆疊幀的理解開始建立一個大致的輪廓就行,接下來我們通過反匯編觀察里面的秘密

下面就是整個程式的部分匯編代碼

之前說過每呼叫一次函式都會為這個函式分配一個堆疊幀,main函式是由__tmainCRTStartup()函式呼叫的,那么__tmainCRTStartup這個函式的堆疊幀就已經創建好了,

回到反匯編,看這一行的 push 一個叫 ebp 的暫存器進來

堆疊頂指標,堆疊底指標
push進來一個元素那么我的esp是不是就得指向這個元素,因為esp是我的堆疊頂指標嘛

接下來,來到除錯視窗,這行指令還沒執行前esp的地址是0x00b5f964

這一行一執行來到了下一行會發現此時的esp的地址已經發生了變化由原來的地址(0x00b5f964)變化到了(0x00b5f960)差了4位元組,這說明了其實就是壓入了一個元素進去,因為此時的esp指向的是壓入堆疊中的那一塊空間,因為堆疊的使用習慣是先使用高地址再使用低地址,由原來的地址(0x00b5f964)減去4位元組得到的就是(0x00b5f960),此時的esp指向的就是(0x00b5f960)這還不足以證明嗎?是不是就對應了上面的這張記憶體圖

函式堆疊幀創建的預備作業
接下來再看mov這一條指令,這條指令的作用是將esp賦值給我的ebp

很明顯我的esp和ebp此時指向的是同一塊空間,因為是同一個地址嘛

如果要用記憶體圖來描述的話,這張就最通俗了

回到匯編代碼,接下來程式該執行sub這條指令了,這條指令的作用就是讓esp 減去 0E4h
這樣子esp就會指向新的空間了,再啰嗦一句就是堆疊的使用習慣是先使用高地址再使用低地址,很明顯esp
指向一個比他還小的地址,那么肯定是又開辟了一塊空間

很明顯此時的esp 又指向了一塊新的空間,因為他的地址發生了變化

此時的記憶體布局是這樣子的

所以主函式空間的大小就是ebp指標減去esp指標的大小,因為在一塊連續的空間里地址 - 地址得到的結果就是這塊空間的大小
接著回到匯編代碼,可以看到程式又壓進去3個值,這個我們先不用關心,知道記憶體圖就可以

此時的堆疊頂指標發生變化,直到這里大家對堆疊頂指標對應每一次壓堆疊都會指向這一塊新空間這里已經很熟悉了吧

當我們程式來到這一行看到了一個 lea,那么這個lea是個什么意思呢?lea的全稱是(load effective
address)意思就是加載有效地址,相當于給edi這個暫存器里面放入一個地址

為了便于大家觀察這博主調整一下編譯器,顯示符號名一勾上便于后我們觀察

這行匯編代碼 edi,[ebp-0E4h] 意思是什么呢?就是用ebp暫存器里此時保存的地址減去0E4h得到的新的地址賦值給我的edi,

這三行指令一執行完,到底干了什么事,真正能夠產生效果的是這句指令 rep stos dword ptr
es:[edi],這句話的意思就是要把從edi這個位置開始向下走的39h次(ecx中的內容),每次將dword的空間初始化為0CCCCCCCCh(eax中的內容)
注意:(一個word占2位元組 d就是雙倍的意思,也就是4個位元組)

這三行代碼將主函式的空間全都初始化為0CCCCCCCCh

Add函式是怎么被呼叫的
以上匯編指令執行完,初步任務就已經完成了,main函式的堆疊幀的開辟就已經準備完了,接下來就可以執行有效代碼了

程式走到這一步,開始執行我們的C語言代碼 int a = 10;

這一行匯編代碼的意思是將0Ah(十進制的10) 這個值,放到 [ebp-8]這個地址指向的這塊空間處初始化這 dword(四位元組空間)
黃色小塊ebp-8這個地址指向的這塊空間就是變數a所在的記憶體單元,在這個時候已經被初始化為10,額外插一句如果沒有將10賦值給a
這塊空間,那么這塊空間不就還是(ccccc)了?答案:是的,每次開辟堆疊幀的時候系統都會給這塊記憶體空間給隨機值,隨機值是什么值?就是這個(ccccc),每次如果程式員只定義區域變數但是沒有給區域變數賦值,那么區域變數的隨機值不是(燙燙燙燙)嗎?只是針對區域變數,因為只有區域變數才會使用堆疊空間,那如果是全域變數呢?是不是就不會啊?因為全域變數沒有用到堆疊的記憶體空間,


斷點停在這一行表示還沒執行這條陳述句,此時a的內容就是0xcccccc
c,那么執行這條陳述句出現的結果又是什么呢呢,讓我們接著觀察

變紅的部分表示變數a已經初始化了,不再是隨機值了,那么這潭訓編代碼是不是就驗證了上面的解釋,由于編譯器采用的是小端存盤(小端在左大端在右),所以在記憶體中是倒著存放的實際上應該是
0x 00 00 00 0a,對應的就是10
那當我們有了對上面這個代碼的理解,在理解這潭訓編代碼是不是就簡單多了啊,這里就不再啰嗦,下面我們直接看記憶體圖

當我的 [ebp-14h] 一執行,其實就已經跳到
0x00B5F94C這個地址處了,也就是對應的變數b的地址處,在這個時候可以看到變數b也已經有初始值了,不再是隨機值了


綠色箭頭標識的是變數b,這種做法希望能便于大家理解,在這個地方呢我們可以觀察到兩個區域變數是差了2個整形的,下面是他們對應的地址,當然這個跟編譯器有關,有的編譯器是差1個整形,有的編譯器是差2個整形,在liunx中差的是1個整形,而在VS2017中差兩個整形,關于這個內容在我的這篇博客中有過解釋,https://blog.csdn.net/m0_53421868/article/details/118726690?spm=1001.2014.3001.5501

接著往下看,這就不用多說了

這行陳述句一執行完ret在記憶體中也被初始化好了

并且我們也能觀察到這3個區域變數都是差了2個整形

此時的記憶體布局,橙色箭頭所指向的就是變數c所在的記憶體塊

接下來就輪到函式呼叫了 mov一執行 eax的存放的就是變數b的地址,接著執行完push再壓堆疊eax


此時此刻esp就指向了我的eax,明顯esp發生了變化


將變數a的值存放到ecx暫存器中

ecx此時此刻存放的是10

push指令一執行

esp指向了新的地址

記憶體分布再發生變化

F11一按進入到了我的Add函式中,這是Add函式對應的匯編指令

push一執行完esp又指向了新壓堆疊的一塊空間

對應的記憶體圖

mov一執行完,esp指標和ebp指標又指向同一個地方去了


當sub一執行完esp又指向另外一個地址處去了,那么這樣子以來的目的是不是就又為了Add函式分配記憶體呢


再接著又是3次push壓入3個暫存器


而以上這一塊代碼的執行又是為了Add函式堆疊幀的創建做準備

緊接著再把x 和 y變數存放到暫存器eax中,執行完add指令后,再將x + y的結果存放到eax中,最后將eax去初始化變數z

其實x變數和y變數并不是在函式堆疊幀中創建的而是在傳參的時候就已經記錄了他們的值

在函式呼叫完了之后,return z 會將變數z的值放入到暫存器中,回傳的是暫存器的值,所以在變數z會銷毀了之后并不影響函式的回傳值

這3行代碼會將edi、esi、ebx給彈堆疊并將esp逐漸往高地址去指向,

當再執行mov指令的時候,兩個指標又重疊了

回看記憶體圖,此時此刻這個Add函式堆疊幀就被銷毀了
而當pop esp pop
ebp這兩條指令一執行后,我的esp又回到最開始指向主函式的edi處去了,而我的ebp又指向到最開始的ebp處去了,請看下面的記憶體圖

而函式一呼叫完就會回到call指令的下一條指令,回到主函式程式繼續執行

當執行當add指令后esp + 8,這個程序會將原來的兩個臨時變數x和y給釋放掉

最后將原本存放在eax暫存器中的值帶回給我的ret

計算的結果就是30

以上的所有流程博主就交代完了,如果有什么不足的請指教,感謝

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/292544.html
標籤:其他
上一篇:長達萬字的git常用指令總結!!!適合小白及在作業中想要對git基本指令有所了解的人群(建議收藏)
下一篇:UP-Growth演算法
