函數調用過程&棧幀&調用約定

函數調用過程 Procedure

參數傳遞

考慮函數調用:func(1, 2)
需要把1和2這兩個參數進行傳遞,這裏的參數傳遞可以通過兩種方式:
1.參數入棧(內存)
2.參數傳遞到寄存器
大多數情況下,也是C\C++的默認形式是通過棧進行傳遞,因爲雖然寄存器傳遞方式快但是寄存器數量有限
參數壓入棧中(內存),CS:IP指向下一條指令地址需要進行跳轉到函數入口就需要進行原地址的保存,這也是通過壓入棧中來解決
注意這裏,由於傳遞哪些參數調用者是知道的,所以參數壓入必須由調用者來進行。
通過call指令來跳轉同時把原地址壓棧保存
跳轉後,進入到函數的入口地址,必須做的兩件事情:

  1. 將BP寄存器(棧底地址也叫棧基址)設定爲SP地址,因爲SP始終指向棧頂位置當進行函數調用時,需要建立一個新的棧幀(Stack Frame)這時將基址設定爲棧頂即可,但是同時原始的棧基址必須入棧進行保存,所以將先進行push壓入BP內容,這是通過sub指令進行
  2. 預留棧空間給局部變量,也就是將SP內容進行設定,設定的值由編譯器根據局部變量,臨時變量數量自動計算

這兩件事情做完後,進行函數體的執行,然後進入到函數返回階段
函數的返回需要進行2件事情:

  1. 將SP恢復爲BP內容,因爲在函數進入之初,SP給了BP,然後進行預留空間導致SP在函數退出階段,和BP中間差了預留空間的長度。所以爲了進行“平衡”必須恢復到函數進入之初
  2. 恢復BP內容爲上一個棧幀基址,由於在進入之初push了BP所以只需要pop即可

最後進行ret,彈出返回地址並置cs:ip

這時已經返回到調用者的位置,由於之前參數進行了壓棧,所以需要把參數都彈出,也就是把SP加上參數佔用的空間即可

棧幀 Stack Frame


維護了一個函數調用的信息,一個函數的棧幀並不包含傳入的參數,傳入的參數在上一個棧幀後面

調用約定 Call Convention

有3種調用約定:

調用約定 參數入棧順序 參數清理負責 備註
__cdecl 從右向左 調用者負責,調用者負責就是調用者需要把SP地址退回到參數之前,由於每次這種約定每次調用的地方都會多一條指令,所以會導致代碼膨脹 C\C++默認調用約定,由於調用者負責堆棧平衡所以支持可變參數…
__stdcall 從右向左 被調用者負責堆棧平衡,也就是被調用函數的最後ret指令需要加上一個數值指定SP回退的地址空間,由於在被調用者內,所以不會導致額外代碼空間的開銷 由於被調用者負責清理,所以不能支持可變參數,Windows API大多都是該調用約定
__fastcall 前兩個參數利用寄存器進行參數傳遞,後面參數利用棧 被調用者 由於藉助寄存器,所以速度較快

什麼是堆棧平衡

堆棧平衡指能正確維護SP寄存器也就是棧頂的位置,例如在函數即將退出之際,由於參數的傳遞利用了棧,所以函數調用完成後,必須把參數佔用的空間給“還”回去。也就是SP指針加上一個數值,到達第一個參數之前的內存位置。
不同的函數調用約定,進行堆棧平衡的角色時不同的,參考上表

函數返回值

利用AX寄存器進行返回值傳遞

可變參數爲什麼必須是__cdecl

由於可變參數壓入參數的個數不固定,如果讓被調用者負責進行堆棧平衡,由於被調用者並不知道壓入參數的個數,所以無法進行堆棧平衡。因爲ret指令後面得使用一個常數,由於是常數所以必須事先確定參數個數
而cdecl是調用者負責平衡,所以常數可以任意,只需放在函數調用返回後即可

實踐

__fastcall

int __fastcall Add(int a, int b, int d)
{
	int c = a + b;
	return c;
}

彙編代碼:

push        ebp  
mov         ebp,esp  
sub         esp,0E4h
...
mov         esp,ebp  
pop         ebp  
ret         4 

如上,由於前兩個參數通過寄存器傳遞,所以ret只需加上4個字節即可完成堆棧平衡,這裏是被調用者進行平衡
再看調用的地方:

push        4  
mov         edx,3  
mov         ecx,2  
call        Add (02B1361h)  
mov         dword ptr [a],eax  

如上push 4把參數(常量參數)入棧,前面兩個參數使用寄存器傳遞,從右向左

__stdcall

int __fastcall Add(int a, int b, int d)
{
	int c = a + b;
	return c;
}

由於fastcall和stdcall都是被調用這負責推展平衡,所以彙編代碼類似

__cdecl

int __cdecl Add(int a, int b, int d)
{
	int c = a + b;
	return c;
}
pop         edi  
pop         esi  
pop         ebx  
mov         esp,ebp  
pop         ebp  
ret

如上,函數返回的ret指令不加參數,直接返回

push        4  
push        3  
push        2  
call        Add (0117136Bh)  
add         esp,0Ch  
mov         dword ptr [a],eax  

調用者這裏使用add指令來進行堆棧平衡

參考

https://zhuanlan.zhihu.com/p/27339191 非常詳細的調用過程分析
https://blog.csdn.net/luoweifu/article/details/52425733
https://www.cnblogs.com/33debug/p/6773059.html
https://www.jianshu.com/p/b666213cdd8a

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