函數調用棧

我們常用函數,知道使用函數時會跳到函數定義的代碼段去執行,然後執行完後再返回到調用函數去,但以下的一些問題卻仍不清楚。

這個調用過程的原理是什麼
調用函數前要做什麼事情
函數的參數是如何傳遞的
如何跳轉到被調用函數
執行完被調函數後如何返回調用函數並且保證能接着運行

要知道這些,需要結合代碼的反彙編來看。
寫了一段簡單的函數調用的代碼
函數調用堆棧


以下爲main函數的反彙編
函數調用堆棧
其中ebp爲棧底指針,esp爲棧頂指針。
可以看到我們所說的指令 比如int a=15;這個指令,它靠的是ebp棧底指針的偏移量來確定某處有個4個字節的地方存儲15這個值的。
具體彙編就是
00EA2C2E  mov    dword ptr [a],0Fh 

00EA2C2E是 mov  dword ptr [a],0Fh 這個指令在代碼段的地址,這個地址是虛擬地址空間地址。並非物理地址
0Fh就是15的十六進制
word代表2個字節,dword代表4個字節 
ptr[a]就是a的地址處,其實這個的真正模樣應該是ptr[ebp-4]

這句彙編的意思就是執行將0Fh這個值移動到ebp-4這個地址,佔用4個字節,簡單來講就是給棧底上的4個字節後賦值15,也佔4個字節。


後面
     int b=10;
00EA2C35  mov         dword ptr [b],0Ah  
   int result=0;
00EA2C3C  mov         dword ptr [result],0  
就是分別壓入了2個整型的0入棧

重點來了
到了函數調用這塊了,我們看看反彙編

result=sum(a,b);
00EA2C43  mov         eax,dword ptr [b]  
00EA2C46  push        eax  
00EA2C47  mov         ecx,dword ptr [a]  
00EA2C4A  push        ecx  
00EA2C4B  call        sum (0EA143Dh)  
00EA2C50  add         esp,8  
00EA2C53  mov         dword ptr [result],eax

其中call就表示跳到被調用函數指令地址。 然而其實call裏面分爲兩步,我們稍後再說
重點是call的前4行彙編是做什麼的?
eax ecx都指的是寄存器。
那麼前四行意思就是給eax寄存器賦值爲b的值,然後eax壓棧,再給ecx寄存器賦值爲a的值,然後ecx壓棧。
這樣看來就是先後把b和a的值壓入棧頂。而我們可以發現b和a就是sum函數所需要的實參。
目前來看是這樣的


之前說了call其實包含了兩步,分別是
1.把調用方 使用調用函數這條指令的下一條指令的地址push壓棧
2.跳到call指令裏那個的指令地址,即被調函數的指令地址。

00EA2C4B  call        sum (0EA143Dh)  
00EA2C50  add         esp,8  
00EA2C53  mov         dword ptr [result],eax

代到這塊的彙編就是將00EA2C50壓棧,然後再跳轉到0EA143Dh
我們看看0EA143Dh  是什麼。

00EA143D  jmp         sum (0EA4450h) 

   括號裏  0EA4450h就是我們剛看的sum定義的地方
int sum(int  a,int  b)
{
00EA4450  push        ebp  
00EA4451  mov         ebp,esp  
00EA4453  sub         esp,0CCh  
00EA4459  push        ebx  
00EA445A  push        esi  
00EA445B  push        edi  
00EA445C  lea         edi,[ebp-0CCh]  
00EA4462  mov         ecx,33h  
00EA4467  mov         eax,0CCCCCCCCh  
00EA446C  rep stos    dword ptr es:[edi]  
   int r=0;
00EA446E  mov       
。。。。
。。。
。。

這樣是不是就很明晰了


然而函數的一開始又有一大串彙編指令,main函數也有,剛纔忽略沒講 ,現在來看,這些指令到底是做什麼的?
.......
00EA4450  push        ebp  
00EA4451  mov         ebp,esp  
00EA4453  sub         esp,0CCh  
00EA4459  push        ebx  
00EA445A  push        esi  
00EA445B  push        edi  
00EA445C  lea         edi,[ebp-0CCh]  
00EA4462  mov         ecx,33h  
00EA4467  mov         eax,0CCCCCCCCh
00EA446C  rep stos    dword ptr es:[edi]  
   int r=0;
.......  

 首先ebp壓入棧,這個ebp是main的棧底指針的值,然後esp的值給ebp,也就是讓棧底指針指向棧頂esp指向的地方,簡言這兩步就是爲了保存原先的main棧底地址,然後讓棧底指針ebp移到最上方,這就變成了開闢了新的棧了,新棧就是被調用函數的棧。

00EA4453  sub         esp,0CCh  
讓esp棧頂指針sub減等0cch,也就是讓新棧開闢了0xcc字節的空間,即204個字節。

之後push了三個寄存器 ebx esi edi 

然後lea       edi,[ebp-0CCh]  這句指令意思爲讓edi指向ebp-0cch處的地址,也就是讓edi寄存器存儲了新棧頂指針的值。
之後又給ecx 存儲了33h,eax存儲了0cccccccch。
33h的十進制爲51。是不是剛好51 *4=204,204是我們新棧開闢的大小。
所以說
00EA4462  mov         ecx,33h  
00EA4467  mov         eax,0CCCCCCCCh
00EA446C  rep stos    dword ptr es:[edi]  
這三行的意思,就是循環ecx次,edi從棧頂向棧底依次賦值爲eax。
也就是循環51次,從棧頂向棧底賦值4個字節的數據0cccccccch,直到edi走向棧底了,把棧內的數據全部賦值了。這就是每個函數開始後,創建了棧,把棧內數據全部清理爲0cccccccc,我們有時會遇到打印越界的數組出現 燙燙燙燙  其實一對 cc 對應的字符就是 燙。 



棧開闢完了之後,進入函數運算
.....
int r=0;
00EA446E  mov         dword ptr [r],0  
   r=a+b;
00EA4475  mov         eax,dword ptr [a]  
00EA4478  add         eax,dword ptr [b]  
00EA447B  mov         dword ptr [r],eax  
   return r;
00EA447E  mov         eax,dword ptr [r]  

}
00EA4481  pop         edi  
00EA4482  pop         esi  
00EA4483  pop         ebx  
00EA4484  mov         esp,ebp  
00EA4486  pop         ebp  

}
00EA4487  ret  
.......



我們先看這部分
 r=a+b;
00EA4475  mov         eax,dword ptr [a]  
00EA4478  add         eax,dword ptr [b]  
00EA447B  mov         dword ptr [r],eax  
可以看到 這個r=a+b的過程是這樣的。把實參a的值先賦值給eax寄存器,然後再讓eax寄存器加等實參b的值。
也就是a+b的結果先一步計算好了存儲在eax中,然後再把eax裏的值賦值給棧中的r。


return r;
00EA447E  mov         eax,dword ptr [r]  
返回r,可以看到是把r中的值給了寄存器,通過寄存器帶回調用方函數的。


重點又來了
看看棧的銷燬是怎麼做的
00EA4481  pop         edi  
00EA4482  pop         esi  
00EA4483  pop         ebx  
00EA4484  mov         esp,ebp  
00EA4486  pop         ebp  

}
00EA4487  ret  
首先3個寄存器出棧。
然後讓esp的值變爲ebp,也就是讓棧頂指針指向棧底。然後ebp出棧,意思就把存儲的main的原先的ebp的值出棧,並賦值給ebp。這樣,ebp就重新指向main的棧底了。
然後ret指令就是讓棧頂的值出棧,現在的棧頂就是存儲那個下一條指令地址的值,出棧就可以跳回到調用方剛執行完函數的地方。就實現了回退並連接上次運行地方的功能。

然後轉到主函數彙編

.......
00EA2C4B  call        sum (0EA143Dh)  
00EA2C50  add         esp,8  
00EA2C53  mov         dword ptr [result],eax  
........
讓esp加等8,意思就是把兩個4個字節累積8個字節的棧幀捨棄。然後將eax裏保存的return的結果賦值給result。
流程圖如下,紅色代表順序



還有一個遺留問題是剛纔的sum函數的參數只有兩個四字節數據,因此用的是寄存器帶的數據,可是寄存器非常有限的,如果我的實參是個結構體類型,大小遠遠大於四個字節呢,這是參數該如何帶呢?
   小於4個字節時用1個寄存器,大於4小於8時 用2個寄存器
    如果大於8個字節那就不能用寄存器了,而是直接讓棧頂指針esp減等參數的大小,然後類似開闢棧時,循環拷貝0ccccccc那樣,用2個寄存器。一個記錄調用方函數的那個實參的其實地址。一個記錄拷貝循環次數。這樣循環拷貝進行傳參。

   返回值也是通樣,如果返回的值大於8個字節時,將在調用方函數的棧內開闢一塊返回值臨時量區域,然後把return的值循環拷貝回調用方。所以在新棧開闢的時候會多壓入一個臨時量的地址。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章