函數調用--函數棧

函數調用你們都不陌生,調用者向被調用者傳遞一些參數,而後執行被調用者的代碼,最後被調用者向調用者返回結果,還有你們比較熟悉的一句話,就是函數調用是在棧上發生的,那麼在計算機內部究竟是如何實現的呢?
 
對於程序,編譯器會對其分配一段內存,在邏輯上能夠分爲代碼段,數據段,堆,棧
代碼段:保存程序文本,指令指針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函數用使用
相關文章
相關標籤/搜索