函式的堆疊呼叫
讓我們帶著問題來閱讀本篇文章
- 型參在哪裡開闢記憶體?
- 型參的入棧順序?
- 函式返回值怎麼帶出來?
- 函式的返回值為什麼會回退到棧裡?
- 函式呼叫結束為什麼會沿著呼叫點繼續執行?
我們先來了解一下堆與棧是怎樣的一種存在
什麼是棧?
棧用於維護函式呼叫的上下文,離開棧,函式就沒有辦法實現。棧通常在使用者空間的最高地址處分配,通常有數兆位元組大小。
棧在程式執行中具有舉足輕重的地位。最重要的是,棧儲存了一個函式呼叫所需要的的維護資訊,這常常被稱為堆幀棧或者活動記錄。堆疊幀一般包括以下幾個內容:
- 幀棧是一個main函式的活動空間範圍。
- 函式的返回地址和引數。
- 臨時變數:包括函式的非靜態區域性變數以及編輯器自動生成的其他臨時變數。
- 儲存的上下文:包括在函式呼叫前後需要保持不變暫存器。
什麼是堆?
堆是用來容納應用程式動態分配記憶體的記憶體區域,當程式使用malloc或new分配記憶體時,得到的記憶體來自堆裡,堆也可能沒有固定統一的儲存區域。堆一般比棧大很多,可能有幾十數百兆自己的容量。
認識函式堆疊呼叫的一些簡單指令
mov 移值指令
lea 移地址
push 用棧
pop 出棧
call
- 壓入下一行指令地址
- jump到被呼叫方函式
認識暫存器的存在
暫存器是CPU內部的元件,暫存器擁有非常高的讀寫速度,所以在暫存器之間的資料傳送非常快。
暫存器的用途:
1.可將暫存器內的資料執行算術及邏輯運算。
2.存於暫存器內的地址可用來指向記憶體的某個位置,即定址。
3.可以用來讀寫資料到電腦的周邊裝置。
eax 是"累加器"(accumulator), 它是很多加法乘法指令的預設暫存器。
add eax,4; eax+=4
sub eax,4; eax-=4
ebx 是"基地址"(base)暫存器, 在記憶體定址時存放基地址。
ecx 是計數器(counter), 是重複(REP)字首指令和LOOP指令的內定計數器。
edx 總是被用來放整數除法產生的餘數。
ebp 棧底指標暫存器
esp 棧頂指標暫存器
pc 下一行指令暫存器
在i386中,一個函式的活動記錄用ebp(棧底指標暫存器)和esp(棧頂指標暫存器)這兩個暫存器劃定範圍。esp暫存器始終指向棧的頂部,同時也就指向了當前函式的活動記錄的頂部。而相對的,ebp的暫存器指向了函式活動記錄的一個固定範圍。
函式堆疊呼叫的實質
用下邊的簡單程式碼舉例瞭解一下
int Add(int a,int b)
{
int c = a + b;
return c;
}
int main()
{
int a=10;
int b=20;
int c = Add(a,b);
printf("%d",c);
return 0;
}
型參開闢記憶體:呼叫方(如下程式碼)
main 函式棧佈局:
int main()
{
011943A0 push ebp
011943A1 mov ebp,esp
011943A3 sub esp,0E4h
011943A9 push ebx
011943AA push esi
011943AB push edi
011943AC lea edi,[ebp-0E4h]
011943B2 mov ecx,39h
011943B7 mov eax,0CCCCCCCCh
011943BC rep stos dword ptr es:[edi]
int a=10;
011943BE mov dword ptr [a],0Ah //在棧上開闢空間,存放[a]
int b=20;
011943C5 mov dword ptr [b],14h //開闢空間,存放[b]
int c = Add(a,b);
011943CC mov eax,dword ptr [b] //將[b]移入到暫存器eax中
011943CF push eax //將eax壓棧
011943D0 mov ecx,dword ptr [a] //將a的值移入到暫存器ecx中,
011943D3 push ecx //將ecx壓棧
011943D4 call Add (0119110Eh) //call:呼叫被呼叫方函式(Add函式),壓入下一行指令地址
011943D9 add esp,8 //棧頂指標移動
011943DC mov dword ptr [c],eax
printf("%d",c);
011943DF mov esi,esp
011943E1 mov eax,dword ptr [c] //將[c]的值放入eax中
011943E4 push eax //eax壓棧
011943E5 push 119CC70h
011943EA call dword ptr ds:[11A03B8h]
011943F0 add esp,8
011943F3 cmp esi,esp
011943F5 call __RTC_CheckEsp (011912D5h)
return 0;
011943FA xor eax,eax
}
被呼叫方:(如下程式碼)
Add函式棧幀開闢:
int Add(int a,int b)
{
011928F3 sub esp,0CCh
011928F9 push ebx
011928FA push esi
011928FB push edi
011928FC lea edi,[ebp-0CCh]
01192902 mov ecx,33h
01192907 mov eax,0CCCCCCCCh
0119290C rep stos dword ptr es:[edi]
int c = a + b; //將運算結果儲存到區域性變數中
0119290E mov eax,dword ptr [a]
01192911 add eax,dword ptr [b]
01192914 mov dword ptr [c],eax
return c;
01192917 mov eax,dword ptr [c] //通過暫存器eax將返回值帶回
}
0119291A pop edi
0119291B pop esi
0119291C pop ebx
0119291D mov esp,ebp //釋放Add函式的棧幀空間
0119291F pop ebp
01192920 ret
函式呼叫過程
- 開闢型參的記憶體並初始化為0xcccccccc
- 壓入下一行指令地址
- 壓入呼叫方棧底指標的值
- 開闢區域性變數所需要的棧空間並初始化
函式呼叫完成後的清棧
- 清理棧開闢的區域性變數
- pop ebp(棧底指標暫存器) ebp回退到呼叫方棧底
- ret(pop pc)
ret包含兩個動作:
1、出棧 ,棧頂指標向下移。出棧元素是下一行指令地址,賦給PC暫存器,PC暫存器永遠存放下一行指令的地址。
2、ret執行完以跳轉到下一行指令的地址
清棧的意義:告訴暫存器記憶體可以再次被分配。
函式的呼叫約定:
(1)thiscall: 類成員方法的呼叫約定,this指標存放於CX暫存器,引數從右到左壓。
內建型別的呼叫約定(Calling convention):
(2)_cdecl:C標準的呼叫。型參由呼叫方開闢,被呼叫方清理。每一個呼叫它的函式都包含清空堆疊的程式碼,所以產生的可執行檔案大小會比呼叫_stdcall函式的大。
(3)__stdcall:windows標準的呼叫約定,型參由呼叫方開闢,被呼叫方清理。
(4)__fastcall:快速呼叫約定,它是通過暫存器來傳送引數的(實際上,它用ECX和EDX傳送前兩個雙字(DWORD)或更小的引數,剩下的引數仍舊自右向左壓棧傳送,被呼叫的函式在返回前清理傳送引數的記憶體棧)。
約定的意義:
1)約定了函式符號的生成。(不同的函式約定,函式符號的生成不一樣)
2)約定了函式引數的壓棧順序。
3)約定了型參的開闢和清理方式。
接下來我們解決文章開頭的問題???
1、型參在哪裡開闢記憶體?
型參在棧上開闢記憶體,由呼叫方開闢。
2、型參的入棧順序?
型參從右向左入棧,如果從左至右的話,不知道實參的傳遞個數。
3、函式返回值怎麼帶出來?
函式值存放到暫存器中,由暫存器帶回
eax帶回返回值:
(1)0<返回值<=4 由eax帶回
(2)4<返回值<=8 由eax edx 兩個暫存器帶回
(3)8<返回值 由臨時量帶回(非類型別)
非類型別的返回方式:
- 臨時量地址壓棧
- 返回值拷到臨時量裡
- 臨時量帶回返回值
4、函式的返回值為什麼會回退到棧裡?
因為壓棧壓的是呼叫方的棧底指標
5、函式執行完成後怎麼控制沿用點繼續執行?
通過call指令繼續。
call指令的作用:
(1)壓入下一行指令的地址
(2)jump到被呼叫方函式
Tips:
實參與型參:
- 型參:用來接收呼叫該方法時傳遞的引數,被呼叫時分配空間,呼叫結束後就釋放。
- 實參:傳遞給被呼叫方法的值,預先建立並賦予確定值。
int Add(int a,int b)//型參,用來接收呼叫該方法時傳遞的引數
{
int c = a + b;
return c;
}
int main()
{
int a=10;//實參,傳遞給被呼叫方法的值
int b=20;
int c = Add(a,b);
printf("%d",c);
return 0;
}
函式定義與函式說明:
- 宣告:說明函式作用
int Add(int a,int b) //求兩個整型數之和
- 定義:定義函式的實現功能,實現函式功能時,函式的名稱,返回值,引數表必須要與此函式宣告時一致。
int Add(int a,int b)//實現函式
{
int c = a + b;
return c;
}
- 當一個函式的定義在這個函式的呼叫之前,則不用宣告,否則需要宣告這個函式。
不需要宣告
int Add(int a,int b)
{
int c = a + b;
return c;
}
int main()
{
int a=10;
int b=20;
int c = Add(a,b);
printf("%d",c);
return 0;
}
需要宣告
int Add(int a,int b);//函式宣告,需要加分號
int main()//被呼叫的函式在呼叫之後
{
int a=10;
int b=20;
int c = Add(a,b);
printf("%d",c);
return 0;
}int Add(int a,int b)
{
int c = a + b;
return c;
}