這段代碼反匯編後,代碼是什麼呢?
#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
摘自 踏雪無痕