任何程式設計語言,函數都是很重要的一個概念。
算法要借助函數來實作。
面向過程程式設計的函數是其基本子產品。
面向對象程式設計的函數(方法)是類或對象對其屬性(資料的處理)。
函數式程式設計自然更是以函數為核心。
通常,程式的控制結構會確定一個入口、一個出口,支援函數的嵌套調用。如何確定代碼的正确流動(包括正确傳回原函數調用處)?編譯器會維護一個棧的記憶體結構。
另外,關于堆棧平衡,可由調用函數負責,也可由被調函數負責。當有多個參數時,參數按什麼順序計算?這些都可由調用約定來進行規定,如:
void __stdcall add(int a,int b);
函數聲明中的__stdcall就是關于調用約定的聲明。其中标準C函數的預設調用約定是__stdcall,C++全局函數和靜态成員函數的預設調用約定是__cdecl,類的成員函數的調用約定是__thiscall。剩下的還有__fastcall,__naked等。
調用約定指明了函數調用中的參數傳遞方式和堆棧平衡方式。
調用約定 堆棧平衡方式
__stdcall 函數自己平衡
__cdecl 調用者負責平衡
__thiscall 調用者負責平衡
__fastcall 調用者負責平衡
__naked 編譯器不負責平衡,由編寫者自己負責
簡單的一個函數調用語句,其實對于編譯器來說,是一個比較複雜的過程。
以下是一個函數嵌套調用的執行個體:
#include using namespace std;int combinations(int n, int k);int fact(int n);int main() { int n, k; cout << "Enter the number of objects (n): "; cin >> n; cout << "Enter the number to be chosen (k): "; cin >> k; cout << "C(n, k) = " << combinations(n, k) << endl; // 在這裡設一斷點 return 0;}int combinations(int n, int k) // C(n, k){ return fact(n) / (fact(k) * fact(n - k));}int fact(int n) // factorial of n{ int result = 1; for (int i = 1; i <= n; i++) result *= i; return result;}
整體流程如下:
編譯後在上述備注處插入一斷點(F9)→運作(F5),按提示輸入:
Enter the number of objects (n): 6Enter the number to be chosen (k): 2
運作至斷點處,調出反彙編調試視窗,跟蹤fact(6)的内部流程:
此時的調用堆棧是:
1 參數壓棧(傳參時,可能存在隐式類型轉換)
push eax,表示将eax的值壓和棧、記憶體棧,具體位置由寄存器esp給出。
寄存器ebp(base pointer )可稱為“幀指針”或“基址指針”,其實語意是相同的。ebp指向了本次函數調用開始時的棧頂指針,它也是本次函數調用時的“棧底”(這裡的意思是,在一次函數調用中,ebp向下是函數的臨時變量使用的空間)。在函數調用開始時,我們會使用mov ebp,esp,把目前的esp儲存在ebp中。
寄存器esp(stack pointer)可稱為“ 棧指針”。esp指向目前的棧頂,它是動态變化的,随着我們申請更多的臨時變量,esp值不斷減小(棧是向下生長的)。函數調用結束,我們使用mov esp,ebp,來還原之前儲存的esp。
在函數調用過程中,ebp和esp之間的空間被稱為本次函數調用的“棧幀”。函數調用結束後,處于棧幀之前的所有内容都是本次函數調用過程中配置設定的臨時變量,都需要被“返還”。這樣在概念上,給了函數調用一個更明顯的分界。
2 call fact(6)
0040193C call @ILT+165(fact) (004010aa)00401941 add esp,4
call 相當于 push+jmp。
2.1 push 傳回位址00401941
2.2 jmp (fact) (004010aa)
004010AA jmp fact (004019a0)
也就是,首先把call指令的下一條指令位址作為本次函數調用的傳回位址壓棧,然後使用jmp指令修改指令指針寄存器EIP,使cpu執行 fact函數的指令代碼。
指令指針寄存器也叫程式計數器,是用于存放下一條指令所在單元的位址的地方。
當執行一條指令時,首先需要根據PC中存放的指令位址,将指令由記憶體取到指令寄存器中,此過程稱為“取指令”。與此同時,PC中的位址或自動加1或由轉移指針給出下一條指令的位址。此後經過分析指令,執行指令。完成第一條指令的執行,而後根據PC取出第二條指令的位址,如此循環,執行每一條指令。
可以看到,程式計數器是一個cpu執行指令代碼過程中的關鍵寄存器:它指向了目前計算機要執行的指令位址,CPU總是從程式計數器取出目前指令來執行。當指令執行後,程式計數器的值自動增加,指向下一條将要執行的指令。
在x86彙編中,執行程式計數器功能的寄存器被叫做EIP,也叫作指令指針寄存器。
3 一些寄存器壓棧,儲存其狀态資訊
004019A0 push ebp004019A1 mov ebp,esp004019A3 sub esp,48h004019A6 push ebx004019A7 push esi004019A8 push edi
4 棧幀配置設定,并初始化
004019A9 lea edi,[ebp-48h]004019AC mov ecx,12h004019B1 mov eax,0CCCCCCCCh004019B6 rep stos dword ptr [edi]
rep指令的目的是重複其上面的指令,ECX的值是重複的次數。
STOS指令的作用是将eax中的值拷貝到ES:EDI指向的位址。
5 局部變量壓棧
004019B8 mov dword ptr [ebp-4],126: for (int i = 1; i <= n; i++)004019BF mov dword ptr [ebp-8],1
6 傳回值(或位址)儲存到寄存器eax
可以察看此時寄存器調試視窗:
傳回值傳回時,可能存在隐式資料類型轉換。
7 一些寄存器值從棧上恢複
004019E8 pop edi004019E9 pop esi004019EA pop ebx004019EB mov esp,ebp004019ED pop ebp
8 ret
ret = pop + jmp
004019EE ret
00401941 add esp,4
表示取出目前棧頂值,作為傳回位址,并将指令指針寄存器EIP修改為該值,實作函數傳回。
7 堆棧平衡
00401941 add esp,4
9 中間值存儲到寄存器
10 表達式的最後結果儲存在寄存器中
以下是combinations(int n, int k)整體的彙編代碼(不包括函數調用時的進入):
18: int combinations(int n, int k) // C(n, k)19: {00401920 push ebp00401921 mov ebp,esp00401923 sub esp,40h00401926 push ebx00401927 push esi00401928 push edi00401929 lea edi,[ebp-40h]0040192C mov ecx,10h00401931 mov eax,0CCCCCCCCh00401936 rep stos dword ptr [edi]20: return fact(n) / (fact(k) * fact(n - k));00401938 mov eax,dword ptr [ebp+8]0040193B push eax0040193C call @ILT+165(fact) (004010aa)00401941 add esp,400401944 mov esi,eax00401946 mov ecx,dword ptr [ebp+0Ch]00401949 push ecx0040194A call @ILT+165(fact) (004010aa)0040194F add esp,400401952 mov edi,eax00401954 mov edx,dword ptr [ebp+8]00401957 sub edx,dword ptr [ebp+0Ch]0040195A push edx0040195B call @ILT+165(fact) (004010aa)00401960 add esp,400401963 imul edi,eax00401966 mov eax,esi00401968 cdq00401969 idiv eax,edi21: }0040196B pop edi0040196C pop esi0040196D pop ebx0040196E add esp,40h00401971 cmp ebp,esp00401973 call __chkesp (00422890)00401978 mov esp,ebp0040197A pop ebp0040197B ret
-End-