C語言中函數(shù)就是一些代碼的集合,實現(xiàn)相對單一的功能;應(yīng)該有名稱、參數(shù)、返回值。實際上函數(shù)應(yīng)該是能夠從調(diào)用程序中接受輸入,處理一定的邏輯,并最終能返回到調(diào)用程序的一段代碼的集合。
本主要討論windows下C函數(shù)如何實現(xiàn)的;主要包括:函數(shù)調(diào)用約定、參數(shù)傳遞和返回。
調(diào)用約定
Windows中默認(rèn)使用的cdecl調(diào)用約定,又叫C調(diào)用約定(不加任何修飾就是這種約定)。cdecl的調(diào)用約定意味著:
1) 參數(shù)從右向左壓入堆棧
2) 函數(shù)自身不清理堆棧;調(diào)用者負(fù)責(zé)清理,因此這種調(diào)用約定允許參數(shù)不固定
3) 函數(shù)名自動加前導(dǎo)的下劃線
例如:函數(shù)
Void TestFun(int a,int b);
等價于:
Void __cdecl TestFun(int a,int b);
如果有段代碼調(diào)用上面的函數(shù),例如:
TestFun(1,2);
那么轉(zhuǎn)變?yōu)閰R編的就是:
Push 2
Push 1
Call TestFun
Add esp 8 ;2個參數(shù)
假設(shè)esp的指針在調(diào)用函數(shù)之前為20,那么上面代碼行對應(yīng)的esp的值為:
Push 2 ;esp =16
Push 1 ;esp=12
Call TestFun ;esp=8
Add esp 8 ;esp=20
其中call指令會修改esp的值,即將函數(shù)的返回地址進(jìn)棧。
WINAPI或stdcall與cdecl不同之處TestFun自己修改堆棧;上面函數(shù)的匯編代碼是:
Push 2
Push 1
Call TestFun
參數(shù)傳遞
C函數(shù)的參數(shù)傳遞都是通過堆棧來進(jìn)行的。還是針對上面的函數(shù)來畫一下堆棧的內(nèi)容:
|
|
ßESP
|
|
返回地址
|
低地址
|
|
1(參數(shù)a)
|
|
|
2(參數(shù)b)
|
|
|
|
|
|
|
高地址
|
調(diào)用者將參數(shù)a和b放到堆棧中;一旦進(jìn)入函數(shù)體,函數(shù)需要讀取這些參數(shù)并進(jìn)行處理。讀取函數(shù)的方式就是通過ESP的偏移來完成。
Mov eax, [esp+4] ;a的值
Mov ebx,[esp+8] ;b的值
如果在進(jìn)入函數(shù)取參數(shù)之前先要保存某些寄存器,例如:
Push eax
那么esp的基地址發(fā)生了變化就是esp+4,從而取a和b的值就變成:
Mov eax,[esp+8] ;a的值
Mov ebx,[esp+12] ;b的值
函數(shù)返回
函數(shù)返回分為兩個部分:返回值的設(shè)置和返回調(diào)用者的位置。
返回值
一般函數(shù)的返回值都是通過eax這個寄存器傳遞給調(diào)用者的,因此如果函數(shù)有返回值都在eax中。但對于浮點數(shù)使用ST0而不是EAX寄存器。
因此函數(shù)體中在RET之前,需要做的是:
MOV EAX, XXX ;XXX返回值
在調(diào)用者中取返回值的做法就是:
MOV [ret], EAX ;ret存放返回值的變量
如果返回值超過4個字節(jié),高位放在EDX中。
函數(shù)返回
對于C約定的函數(shù)返回通過RET來完成。需要做的就是從堆棧中讀取返回地址,然后修改EIP寄存器的位置;ESP的值加4,即從堆棧中把返回值POP出來。
對于STDCALL的約定,需要給RET傳遞一個參數(shù),參數(shù)的內(nèi)容為傳遞進(jìn)來的參數(shù)占用堆棧的大小,目的是通過RET指令來修改ESP的棧頂位置;恢復(fù)到調(diào)用前的位置。
不管上面約定,最后EIP指向調(diào)用者調(diào)用該函數(shù)的下一行指令。