程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 函數調用堆棧

函數調用堆棧

編輯:C++入門知識

這段代碼反匯編後,代碼是什麼呢?

#include <stdio.h>

long test(int a,int b)
{
     a = a + 3;
     b = b + 5;
     return a + b;
}

int main(int argc, char* argv[])
{
    printf("%d",test(10,90));
    return 0;
}


先來看一個概貌

16:   int main(int argc, char* argv[])
17:   {
00401070   push        ebp
00401071   mov         ebp,esp
00401073   sub         esp,40h
00401076   push        ebx
00401077   push        esi
00401078   push        edi
00401079   lea         edi,[ebp-40h]
0040107C   mov         ecx,10h
00401081   mov         eax,0CCCCCCCCh
00401086   rep stos    dword ptr [edi]
18:       printf("%d",test(10,90));
00401088   push        5Ah
0040108A   push        0Ah
0040108C   call        @ILT+0(test) (00401005)
00401091   add         esp,8
00401094   push        eax
00401095   push        offset string "%d" (0042201c)
0040109A   call        printf (004010d0)
0040109F   add         esp,8
19:       return 0;
004010A2   xor         eax,eax
20:   }
 

下面來解釋一下
\

開始進入Main函數  esp=0x12FF84   ebp=0x12FFC0
完成橢圓形框起來的部分
00401070   push        ebp     ebp的值入棧,保存現場(調用現場,從test函數看,如紅線所示,即保存的0x12FF80用於從test函數堆棧返回到main函數)
00401071   mov         ebp,esp     此時ebp=0x12FF80 此時ebp就是“當前函數堆棧”的基址 以便訪問堆棧中的信息;還有就是從當前函數棧頂返回到棧底

00401073   sub      esp,40h  
函數使用的堆棧,默認64個字節,堆棧上就是16個橫條(密集線部分)此時esp=0x12FF40
在上圖中,上面密集線是test函數堆棧空間,下面是Main的堆棧空間    (補充,其實這個就叫做 Stack Frame)

00401076   push        ebx
00401077   push        esi
00401078   push        edi    入棧

00401079   lea         edi,[ebp-40h]
0040107C   mov         ecx,10h
00401081   mov         eax,0CCCCCCCCh
00401086   rep stos    dword ptr [edi]     
初始化用於該函數的棧空間為0XCCCCCCCC  即從0x12FF40~0x12FF80所有的值均為0xCCCCCCCC

18:       printf("%d",test(10,90));
00401088   push        5Ah    參數入棧 從右至左 先90  後10
0040108A   push        0Ah

0040108C   call        @ILT+0(test) (00401005)  
函數調用,轉向eip 00401005 
注意,此時仍入棧,入棧的是call test 指令下一條指令的地址00401091   下一條指令是add esp,8

@ILT+0(?test@@YAJHH@Z):
00401005   jmp         test (00401020)  
即轉向被調函數test

8:    long test(int a,int b)
9:    {
00401020   push        ebp
00401021   mov         ebp,esp          
00401023   sub         esp,40h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-40h]
0040102C   mov         ecx,10h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]       //這些和上面一樣
10:        a = a + 3;                                   
00401038   mov         eax,dword ptr [ebp+8]     //ebp=0x12FF24 加8 [0x12FF30]即取到了參數10
0040103B   add         eax,3
0040103E   mov         dword ptr [ebp+8],eax
11:        b = b + 5;
00401041   mov         ecx,dword ptr [ebp+0Ch]
00401044   add         ecx,5
00401047   mov         dword ptr [ebp+0Ch],ecx
12:        return a + b;
0040104A   mov         eax,dword ptr [ebp+8]
0040104D   add         eax,dword ptr [ebp+0Ch]  //最後的結果保存在eax, 結果得以返回
13:   }
00401050   pop         edi                
00401051   pop         esi
00401052   pop         ebx
00401053   mov         esp,ebp     //esp指向0x12FF24, test函數的堆棧空間被放棄,從當前函數棧頂返回到棧底
00401055   pop         ebp           //此時ebp=0x12FF80, 恢復現場  esp=0x12FF28
00401056   ret                          ret負責棧頂0x12FF28之值00401091彈出到指令寄存器中,esp=0x12FF30


因為win32匯編一般用eax返回結果 所以如果最終結果不是在eax裡面的話 還要把它放到eax

注意,從被調函數返回時,是彈出EBP,恢復堆棧到函數調用前的地址,彈出返回地址到EIP以繼續執行程序。

從test函數返回,執行
00401091   add         esp,8      
清棧,清除兩個壓棧的參數10 90 調用者main負責
(所謂__cdecl調用由調用者負責恢復棧,調用者負責清理的只是入棧的參數,test函數自己的堆棧空間自己返回時自己已經清除,靠!一直理解錯)

00401094   push       eax          入棧,計算結果108入棧,即printf函數的參數之一入棧
00401095   push        offset string "%d" (0042201c)     入棧,參數 "%d"  當然其實是%d的地址
0040109A   call        printf (004010d0)      函數調用 printf("%d",108) 因為printf函數時
0040109F   add         esp,8       清棧,清除參數 ("%d", 108)
19:       return 0;          
004010A2   xor         eax,eax     eax清零
20:   }

main函數執行完畢 此時esp=0x12FF34   ebp=0x12FF80
004010A4   pop         edi
004010A5   pop         esi
004010A6   pop         ebx
004010A7   add         esp,40h    //為啥不用mov esp, ebp? 是為了下面的比較
004010AA   cmp         ebp,esp   //比較,若不同則調用chkesp拋出異常
004010AC   call        __chkesp (00401150)  
004010B1   mov         esp,ebp  
004010B3   pop         ebp          //ESP=0X12FF84  EBP=0x12FFC0 塵歸塵 土歸土 一切都恢復最初的平靜了  :)
004010B4   ret

1. 如果函數調用方式是__stdcall 不同之處在於
main函數call 後面沒有了 add esp, 8
test函數最後一句 是 ret 8   (由test函數清棧, ret 8意思是執行ret後,esp+8)

2. 運行過程中0x12FF28 保存了指令地址 00401091是怎麼保存的?
棧每個空間保存4個字節(粒度4字節) 例如下一個棧空間0x12FF2C保存參數10 
因此
0x12FF28 0x12FF29 0x12FF2A 0x12FF2B  
  91              10            40           00      
little-endian  認為其讀的第一個字節為最小的那位上的數

3. char a[] = "abcde" 
對局部字符數組變量(棧變量)賦值,是利用寄存器從全局數據內存區把字符串“abcde”拷貝到棧內存中的

4. int szNum[5] = { 1, 2, 3, 4, 5 }; 棧中是如何分布的?
     00401798   mov         dword ptr [ebp-14h],1
     0040179F   mov         dword ptr [ebp-10h],2
     004017A6   mov         dword ptr [ebp-0Ch],3
     004017AD   mov         dword ptr [ebp-8],4
     004017B4   mov         dword ptr [ebp-4],5
可以看出來 是從右邊開始入棧,所以是 5 4 3 2 1 入棧

 int *ptrA = (int*)(&szNum+1);
 int *ptrB = (int*)((int)szNum + 1);
 std::cout<< ptrA[-1] << *ptrB << std::endl;
結果如何?
28:       int *ptrA = (int*)(&szNum+1);
004017BB   lea         eax,[ebp]
004017BE   mov         dword ptr [ebp-18h],eax
&szNum是指向數組指針;加1是加一個數組寬度;&szNum+1指向移動5個int單位之後的那個地方, 就是把EBP的地址賦給指針
ptrA[-1]是回退一個int*寬度,即ebp-4
29:       int *ptrB = (int*)((int)szNum + 1);
004017C1   lea         ecx,[ebp-13h]
004017C4   mov         dword ptr [ebp-1Ch],ecx
如果上面是指針算術,那這裡就是地址算術,只是首地址+1個字節的offset,即ebp-13h給指針

實際保存是這樣的
01               00           00      00           02           00      00      00
ebp-14h     ebp-13h                      ebp-10h
注意是int*類型的,最後獲得的是 00 00 00 02
由於Little-endian, 實際上邏輯數是02000000   轉換為十進制數就為33554432
最後輸出533554432

 


摘自 踏雪無痕

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved