理解並觀測函式呼叫母函式做什麼,子函式做什麼
cdecl呼叫約定
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int __cdecl method(int x, int y)
{
return x + y;
}
int main()
{
__asm mov eax, eax; // 此處設定斷點
method(1, 2);
return 0;
}
可以看出__cdecl就是C語言預設的呼叫約定。
二、_stdcall呼叫約定
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int __stdcall method(int x, int y)
{
return x + y;
}
int main()
{
__asm mov eax, eax; // 此處設定斷點
method(1, 2);
return 0;
}
和__cdecl一樣都是從右往左入棧引數,不過該呼叫約定使用的平棧方式是內平棧,從下圖可以看到,這裡已經看不到堆疊的處理了。
F11不斷執行,直到進入call指令呼叫的method函式中:
平棧操作跑到函式內部了,__cdecl約定是呼叫者(main)函式進行平棧,而__stdcall約定是函式內部自身進行平棧。
三、_fastcall呼叫約定
這是一個比較特殊的呼叫約定,當函式引數為兩個或者以下時,該約定的效率遠遠大於上面兩種,當然隨著引數越來越多,該約定與上面兩種約定的差距逐漸縮小。
證明如下:
首先,我們使用__fastcall呼叫約定並傳入兩個引數。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int __fastcall method(int x, int y)
{
return x + y;
}
int main()
{
__asm mov eax, eax; // 此處設定斷點
method(1, 2);
return 0;
}
可以看出函式內部和外部都沒有清理堆疊的操作。這也就是__fastcall效率高的原因。因為暫存器就是屬於CPU的,然後堆疊是記憶體,使用CPU進行操作的效率肯定會大於使用記憶體,所以我們使用暫存器的效率肯定比push傳參效率高很多。
那麼為什麼沒有平棧操作呢?因為我們沒有使用堆疊,我們只是用了暫存器,並沒有使用堆疊操作。但是當我們傳入更多的引數的時候就需要用到堆疊了,因為__fastcall他只給我們提供了兩個暫存器ECX/EDX可以用來傳參。
使用四個引數:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int __fastcall method(int x, int y,int t,int u)
{
return x + y + t +u;
}
int main()
{
__asm mov eax, eax; // 此處設定斷點
method(1, 2, 3, 4);
return 0;
}
F11進入函式內部檢視:
透過四個引數的傳遞,證明了:
函式引數除了前兩個引數使用暫存器、其他的依舊使用堆疊從右往左傳參,並且是自身清理堆疊,不是呼叫者清理。
當引數越來越多的時候,__fastcall與其他呼叫約定的差距越來越小的原因是使用暫存器(CPU)的效率遠遠大於使用堆疊(記憶體),然而__fastcall約定也只能使用兩個暫存器,當函式引數只有兩個時,__fastcall可以完全使用暫存器進行函式傳參,所以這個時候他和__cdecl和__stdcall的差距最大。隨著引數越來越多,__fastcall依舊只能使用兩個暫存器,這樣一來引數越多,__fastcall使用記憶體的佔比就越大,所以效能差距也就越來越小。
2.平衡堆疊的3種模式,誰釋放引數空間
堆疊平衡是指在函式呼叫過程中,保證堆疊的棧頂指標(ESP)在函式返回前恢復到呼叫前的狀態,以避免堆疊的混亂或溢位1。堆疊平衡的三種模式分別是:
外平棧:在函式外部使用 add esp, xx 指令來清理堆疊中的引數,其中 xx 是引數所佔用的位元組數。這種模式的優點是可以適應不同的呼叫約定,缺點是需要額外的指令來調整堆疊。
; 外平棧模式
push param3 ; 將引數3推入堆疊
push param2 ; 將引數2推入堆疊
push param1 ; 將引數1推入堆疊
call myFunction ; 呼叫函式
add esp, 12 ; 清理堆疊
myFunction:
push ebp ; 儲存舊的基址
mov ebp, esp ; 設定新的基址
; 在函式內部使用 [ebp+xx] 來訪問引數
; 在函式內部執行一些操作
pop ebp ; 恢復舊的基址
ret ; 返回
內平棧:在函式內部使用 ret xx 指令來返回並清理堆疊中的引數,其中 xx 是引數所佔用的位元組數。這種模式的優點是可以節省一條指令,缺點是需要函式的定義和呼叫都遵循相同的約定。
; 內平棧模式
push param3 ; 將引數3推入堆疊
push param2 ; 將引數2推入堆疊
push param1 ; 將引數1推入堆疊
call myFunction ; 呼叫函式
myFunction:
push ebp ; 儲存舊的基址
mov ebp, esp ; 設定新的基址
; 在函式內部使用 [ebp+xx] 來訪問引數
; 在函式內部執行一些操作
pop ebp ; 恢復舊的基址
ret 12 ; 返回並清理堆疊
平衡堆疊:在函式內部使用 push 和 pop 指令來平衡堆疊的變化,使得函式返回前堆疊的狀態和呼叫前一致。這種模式的優點是可以保證堆疊的平衡,缺點是需要更多的指令來操作堆疊。
; 平衡堆疊模式
push param3 ; 將引數3推入堆疊
push param2 ; 將引數2推入堆疊
push param1 ; 將引數1推入堆疊
call myFunction ; 呼叫函式
myFunction:
push ebp ; 儲存舊的基址
mov ebp, esp ; 設定新的基址
; 在函式內部使用 [ebp+xx] 來訪問引數
; 在函式內部執行一些操作
; 在函式內部使用 pop 指令來平衡堆疊
pop ebp ; 恢復舊的基址
ret ; 返回
根據不同的模式,釋放引數空間的責任也不同。在外平棧模式中,呼叫者負責釋放引數空間;在內平棧模式中,被呼叫者負責釋放引數空間;在平衡堆疊模式中,呼叫者和被呼叫者都不需要釋放引數空間,因為堆疊已經平衡
3.debug模式下ebp定址,release模式下esp定址(工具Ida)
ebp定址與esp定址
原始碼:
#define _CRT_SECURE_NO_WARNINGS
// Function.cpp : Defines the entry point for the console application.
//
#include<stdio.h>
typedef void (*p)();
// ebp 與 esp訪問
void InNumber()
{
// 區域性變數定義
int nInt = 1;
scanf("%d", &nInt);
char cChar = 2;
scanf("%c", &cChar);
printf("%d %c\r\n", nInt, cChar);
}
// 兩數交換
void AddNumber(int nOne)
{
nOne += 1;
printf("%d \r\n", nOne);
}
int GetAddr(int nNumber)
{
int nAddr = *(int*)(&nNumber - 1);
return nAddr;
}
struct tagTEST
{
int m_nOne;
int m_nTwo;
};
tagTEST RetStruct()
{
tagTEST testRet;
testRet.m_nOne = 1;
testRet.m_nTwo = 2;
return testRet;
}
void AsmStack()
{
__asm
{
push eax
pop eax
}
int nVar = 0;
scanf("%d", &nVar);
printf("AsmStack %\r\n", nVar);
}
void main()
{
AsmStack();
tagTEST test;
test = RetStruct();
int nAddr = GetAddr(1);
int nReload = (nAddr + *(int *)(nAddr - 4)) - (int)GetAddr;
int nNumber = 0;
scanf("%d", &nNumber);
AddNumber(nNumber);
InNumber();
}
main proc near ArgList=byte ptr -0Ch Arglist=byte ptr -5var_4=dword ptr -4argc=dword ptr 8argv=dword ptr echenvp=dword ptr10h push ebp mov ebp,esp sub esp,0ch mov eax, security_cookie xor eax,ebp mov [ebp var_4],eax push eax pop eax lea eax,[ebp ArgList] mov dword ptr [ebp ArgList],e push eax ;Arglist push offset aD ;"%d" call sub401050 push dword ptr [ebp ArgList]ArgList push offset Format"AsmStack %r\n" call sub401626 lea eax,[ebp ArgList] mov dword ptr [ebp ArgList],1 push eax ;Arglist push offset aD ;"%dn call sub401050 lea eax,[ebp Arglist] mov [ebp Arglist],2 push eax ;Arglist push offset ac ;"%c" call sub 401050 movsx eax,[ebp Arglist] push eax push dword ptr [ebp ArgList]ArgList push offset aDC ;"%d%c\r\n" call sub401826 mov ecx,[ebp var_4] add esp,2Ch xor ecx,ebp call sub_4010FE
在使用了esp定址後,不必在每次進人函式後都調整棧底ebp,這樣既減少了ebp的使用又省去了維護ebp的相關指令,因此可以有效提升程式的執行效率。但是,缺少了 ebp 就無法儲存進入函式後的棧底指標,也就無法進行棧平衡檢測。
在每次訪向變數都需要計算,如果在函式執行過程中 esp 發生了改變,再次訪問變數就需要重新計算偏移,這真是個令人頭疼的問題。為了省去對偏移量的計算,方便分析,IDA在分析過程中事先將函式中的每個變數的偏移值計算出來,得出了一個固定偏移值,使用標號將其記錄。IDA是採用負數作為偏移值,將其作為標號,參與變數定址計算。