平衡堆疊

风花赏秋月發表於2024-10-18

理解並觀測函式呼叫母函式做什麼,子函式做什麼

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是採用負數作為偏移值,將其作為標號,參與變數定址計算。

相關文章