首先由下面簡單的**我們來考慮兩個問題:
1.main函式呼叫sum,sum執行完以後,怎麼知道回到哪個函式中?
2.sum函式執行完,回到main以後,怎麼知道從哪一行指令繼續執行的?
int
sum(
int a,int b)
intmain()
首先,大家都知道函式執行的時候要在棧幀上開闢空間。乙個函式的呼叫要先壓引數且從右向左壓,現在我們對這個**從彙編的角度進行更深的剖析:
在main函式中我們定義的引數
int a=10;實際相當於彙編中的mov dword ptr[edp-4], 0ah
int b=20;實際相當於彙編中的mov dword ptr[edp-8], 14h
esp指的是堆疊指標且始終指向棧頂
ebp指的是基址指標
那麼第三個引數ret我們該如何去理解它呢?
int ret =sum(a,b);
我們這裡ret的初值是不知道的,它等於sum函式的返回值;那麼乙個函式的呼叫呢首先會做的事就是壓棧,首先會把b壓入棧,如下圖
最上面的一塊就是sum函式形參變數b的記憶體從這裡我們就知道函式呼叫過程中形參變數記憶體的開闢是在呼叫方函式這裡就已經開闢好了的,因為是呼叫方的函式實參要壓棧。隨著壓棧產生了push指令:
mov eax,dword ptr[ebp-8]
push eax
mov eax,dword ptr[ebp-4]
push eax
這個函式呼叫引數已經壓完棧了,接下來就是call指令了:
首先call sum做的第一件事就是把這行指令的下一行指令的位址壓棧
我們假設位址為0x08124458
第二件事情就是進入sum:
在sum裡面我們不能覺得函式進來第一句話執行的就是int temp =0;我們千萬不能忽略了「{」和「}」,它也是有指令生成的:
push ebp
mov ebp, esp
sub esp, 4ch
以上指令就相當於在給我們的sum函式開闢棧幀空間,
這在我們gcc g++下面函式 「{ 」進來以後這三行指令並沒有對棧進行乙個初始化的操作,而在我們windows下vs系列的編譯器裡面,我們給函式開闢棧幀,開闢完之後我們都會有:
rep stos
for這兩個指令就相當於我們**中for迴圈的指令,會把我們的棧幀都全部初始化成0xcccccccc。那我們一定見過以下這個例子:
int a;
cout<
//-858993460 0xcccccccc
這就是為什麼乙個區域性變數沒有初始化去列印它會列印出這個結果。
接下來到sum函式內的三條**:
int temp =
0;//mov dword ptr[ebp-4],0
temp = a+b;
//mov eax,dword ptr[ebp+8] mov dword ptr[ebp-4],eax
return temp;
//mov eax,dword ptr[ebp-4]
然後到了「}」:它做的第一件事就是
mov esp , ebp
就是相當於回退棧幀,把這個棧空間交還給系統,棧內的資料並沒有清空。
下面這個例子可以讓我們更好的理解:
int
*func()
intmain()
那麼我們肯定可以評判上面func()這個**肯定是不安全的,因為函式執行完棧幀回退之後這個棧空間已經交還給系統了,只不過我們僅僅還能再main函式中列印這個*p(因為棧幀回退並沒有對棧上的資料進行清理)。所以我們還是盡量不要寫上面的這種**。
再往下看:
pop ebp ;出棧並把出棧的值賦給ebp
接下來是ret它做了兩個操作
首先是乙個出棧操作,把出棧的內容放入cpu的pc暫存器裡面(pc暫存器存放的是下一條指令的位址)。所以我們cpu執行完這個ret指令就直接跳到這個0x08124458這個位址上去執行下一條指令了,就跳到下圖這個地方來了:
所以呢 sum函式執行完它是怎麼回到main函式,那麼main函式又是怎麼知道從
add esp,8 這條指令繼續執行的呢?
就是因為它在進入sum函式的時候早已經把call指令下一行指令的位址入棧了,當這個sum函式呼叫完,「{ 」最後乙個指令ret的時候出棧把下一行指令的位址直接放入pc暫存器裡面,相當於我們看到的回到sum函式並且從剛才呼叫函式的下一條指令開始執行了
這時 add esp,8 執行之後
這是把剛才壓棧的兩個形參變數a和b的記憶體交還給系統了
然後esp又回到了main函式剛才的棧頂了。
以上就是整個函式呼叫堆疊的詳細過程。
從彙編的角度分析函式呼叫過程(1)
函式的引數傳遞有2種方式 堆疊方式 暫存器方式。如果是堆疊方式傳遞的,就需要定義函式引數在堆疊中的傳遞順序,並約定函式被呼叫之後,由誰來平衡堆疊 如果是暫存器方式傳遞的,就需要確定引數存放在哪個暫存器中。每一種方式都有其優缺點,而且與使用的程式語言有關係,不存在哪種方式好與壞。我們在開發中經常遇到呼...
從彙編的角度分析函式呼叫過程(2)
include int add int a,int b int main 我們使用visual studio 2017編譯上面 並在在工程配置中將函式呼叫約定設定為 cdecl。在程式除錯過程中,可以在visual studio的反彙編視窗中看到c 對應的彙編 以及暫存器視窗中看到各個暫存器的值。m...
函式呼叫堆疊分析
理解呼叫棧最重要的兩點是 棧的結構,ebp暫存器的作用。首先要認識到這樣兩個事實 1 乙個函式呼叫動作可分解為 零到多個push指令 用於引數入棧 乙個call指令。call指令內部其實還暗含了乙個將返回位址 即call指令下一條指令的位址 壓棧的動作。2 幾乎所有本地編譯器都會在每個函式體之前插入...