函數調用大家都不陌生,調用者向被調用者傳遞一些參數,然后執行被調用者的代碼,最后被調用者向調用者返回結果,還有大家比較熟悉的一句話,就是函數調用是在棧上發生的,那么在計算機內部到底是如何實現的呢?
對於程序,編譯器會對其分配一段內存,在邏輯上可以分為代碼段,數據段,堆,棧
代碼段:保存程序文本,指令指針EIP就是指向代碼段,可讀可執行不可寫
數據段:保存初始化的全局變量和靜態變量,可讀可寫不可執行
BSS:未初始化的全局變量和靜態變量
堆(Heap):動態分配內存,向地址增大的方向增長,可讀可寫可執行
棧(Stack):存放局部變量,函數參數,當前狀態,函數調用信息等,向地址減小的方向增長,非常非常重要,可讀可寫可執行
如圖所示
寄存器
EAX:累加(Accumulator)寄存器,常用於函數返回值
EBX:基址(Base)寄存器,以它為基址訪問內存
ECX:計數器(Counter)寄存器,常用作字符串和循環操作中的計數器
EDX:數據(Data)寄存器,常用於乘除法和I/O指針
ESI:源變址寄存器
DSI:目的變址寄存器
ESP:堆棧(Stack)指針寄存器,指向堆棧頂部
EBP:基址指針寄存器,指向當前堆棧底部
EIP:指令寄存器,指向下一條指令的地址
源代碼
int print_out(int begin, int end) { printf("%d ", begin++); int *p; p = (int*)(int(&begin) - 4); if(begin <= end) *p -= 5; return 1; } int add(int a, int b) { return a+b; } int pass(int a, int b, int c) { char buffer[4] = {0}; int sum = 0; int *ret; ret = (int*)(buffer+28); //(*ret) += 0xA; sum = a + b + c; return sum; } int main() { print_out(0, 2); printf("\n"); int a = 1; int b = 2; int c; c = add(a, b); pass(a, b, c); int __sum; __asm { mov __sum, eax } printf("%d\n", __sum); system("pause"); }
函數初始化
28: int main() 29: { 011C1540 push ebp //壓棧,保存ebp,注意push操作隱含esp-4 011C1541 mov ebp,esp //把esp的值傳遞給ebp,設置當前ebp 011C1543 sub esp,0F0h //給函數開辟空間,范圍是(ebp, ebp-0xF0) 011C1549 push ebx 011C154A push esi 011C154B push edi 011C154C lea edi,[ebp-0F0h] //把edi賦值為ebp-0xF0 011C1552 mov ecx,3Ch //函數空間的dword數目,0xF0>>2 = 0x3C 011C1557 mov eax,0CCCCCCCCh 011C155C rep stos dword ptr es:[edi] //rep指令的目的是重復其上面的指令.ECX的值是重復的次數. //STOS指令的作用是將eax中的值拷貝到ES:EDI指向的地址,然后EDI+4
一般所用函數的開頭都會有這段命令,完成了狀態寄存器的保存,堆棧寄存器的保存,函數內存空間的初始化
函數調用
30: print_out(0, 2); 013D155E push 2 //第二個實參壓棧 013D1560 push 0 //第一個實參壓棧 013D1562 call print_out (13D10FAh)//返回地址壓棧,本例中是013D1567,然后調用print_out函數 013D1567 add esp,8 //兩個實參出棧 //注意在call命令中,隱含操作是把下一條指令的地址壓棧,也就是所謂的返回地址
除了VS可能增加一些安全性檢查外,print_out的初始化與main函數的初始化完全相同
被調用函數返回
013D141C mov eax,1 //返回值傳入eax中 013D1421 pop edi 013D1422 pop esi 013D1423 pop ebx //寄存器出棧 013D1424 add esp,0D0h //以下3條命令是調用VS的__RTC_CheckEsp,檢查棧溢出 013D142A cmp ebp,esp 013D142C call @ILT+315(__RTC_CheckEsp) (13D1140h) 013D1431 mov esp,ebp //ebp的值傳給esp,也就是恢復調用前esp的值 013D1433 pop ebp //彈出ebp,恢復ebp的值 013D1434 ret //把返回地址寫入EIP中,相當於pop EIP
call指令隱含操作push EIP,ret指令隱含操作 pop EIP,兩條指令完全對應起來
寫到這里我們就可以分析一下main函數調用print_out函數前后堆棧(Stack)發生了什么變化,下面用一系列圖說明
接下來是返回過程,從上面的013D1431 行代碼開始
print_out函數調用前后,main函數的棧幀完全一樣,perfect!
下面我們來看看print_out函數到底做了什么事情
int *p; p = (int*)(int(&begin) - 4); if(begin <= end) *p -= 5;
根據上面調用print_out函數后的示意圖,可以知道p實際上是指向了函數的返回地址addr,然后把addr-5,這又會發生什么?
再回頭看一下反匯編的代碼,
013D1560 push 0 //第一個實參壓棧 013D1562 call print_out (13D10FAh)//返回地址壓棧,本例中是013D1567,然后調用print_out函數 013D1567 add esp,8 //兩個實參出棧
分析可知,返回地址addr的值是013D1567 ,addr-5為013D1562 ,把返回地址指向了call指令,結果是再次調用print_out函數,
從而print_out函數實現了打印從begin到end之間的所有數字,可以說是循環調用了print_out函數
對於add函數,主要是為了說明返回值存放於寄存器eax中。
另外,VS自身會提供一些安全檢查
CheckStackVar安全檢查
http://blog.csdn.net/masefee/article/details/5630154,通過ecx和edx傳遞參數, 局部變量有數組時使用
__security_check_cookie返回地址檢查, 數組長度大於等於5時使用
__RTC_CheckEsp程序棧檢查,printf函數用使用
