00401028 push edi
00401027 push esi
00401026 push ebx
00401029 lea edi,[ebp-40h] Edi Esp
00401023 sub esp,40h 40h的空間
0040102C mov ecx,10h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
進入func後00401020 push ebp Esp Ebp
隱含把call的下一條指令入棧
0040107A push 1
00401078 push 2
00401068 push edi由於00401069 lea edi,[ebp-40h]看下面
00401067 push esi
00401066 push ebx
00401063 sub esp,40h (Edi) Esp
00401063 sub esp,40h 40h字節的數據空間
0040106C mov ecx,10h 00401071 mov eax,0CCCCCCCCh
00401076 rep stos dword ptr [edi] ;
00401060 push ebp 00401061 mov ebp,esp Esp Ebp
轉】C函數調用與堆棧
這幾天突然很想弄明白c函數調用時棧的使用情況,上網查了一下資料,自已也研究了一下,這篇blog就把我的所得記錄下來吧。
這篇blog試圖講明當一個c函數被調用時,一個棧幀(stack frame)是如何被建立,又如何被消除的。這些細節跟操作系統平臺及編譯器的實現有關,下面的描述是針對運行在Intel奔騰芯片上Linux的gcc編譯器而言。c語言的標準並沒有描述實現的方式,所以,不同的編譯器,處理器,操作系統都可能有自己的建立棧幀的方式。
一個典型的棧幀
ESP==>| : |
| . |
+-------------------------+
| 被調用者保存的寄存器現場 |
| EBX,ESI和EDI(根據需要)|
+-------------------------+
| 臨時空間 |
+-------------------------+
| 局部變量#2 | [EBP - 8]
+-------------------------+
| 局部變量#1 | [EBP - 4]
+-------------------------+
EBP==>| 調用者的EBP |
+-------------------------+
| 返回地址 |
+-------------------------+
| 實際參數#1 | [EBP + 8]
+-------------------------+
| 實際參數#2 | [EBP + 12]
+-------------------------+
| 實際參數#3 | [EBP + 16]
+-------------------------+
| 調用者保存的寄存器現場 |
| EAX,ECX和EDX(根據需要)|
+-------------------------+
| : |
| . |
圖1
圖1是一個典型的棧幀,圖中,棧頂在上,地址空間往下增長。
這是如下一個函數調用時的棧的內容:
int foo(int arg1, int arg2, int arg3);
並且,foo有兩個局部的int變量(4個字節)。在這個簡化的場景中,main調用foo,而程序的控制仍在foo中。這裏,main是調用者(caller),foo是被調用者(callee)。
ESP被foo使用來指示棧頂。EBP相當於一個“基準指針”。從main傳遞到foo的參數以及foo本身的局部變量都可以通過這個基準指針爲參考,加上偏移量找到。
由於被調用者允許使用EAX,ECX和EDX寄存器,所以如果調用者希望保存這些寄存器的值,就必須在調用子函數之前顯式地把他們保存在棧中。另一方面,如果除了上面提到的幾個寄存器,被調用者還想使用別的寄存器,比如EBX,ESI和EDI,那麼,被調用者就必須在棧中保存這些被額外使用的寄存器,並在調用返回前回復他們。也就是說,如果被調用者只使用約定的EAX,ECX和EDX寄存器,他們由調用者負責保存並回復,但如果被調用這還額外使用了別的寄存器,則必須有他們自己保存並回復這些寄存器的值。
傳遞給foo的參數被壓到棧中,最後一個參數先進棧,所以第一個參數是位於棧頂的。foo中聲明的局部變量以及函數執行過程中需要用到的一些臨時變量也都存在棧中。
小於等於4個字節的返回值會被保存到EAX中,如果大於4字節,小於8字節,那麼EDX也會被用來保存返回值。如果返回值佔用的空間還要大,那麼調用者會向被調用者傳遞一個額外的參數,這個額外的參數指向將要保存返回值的地址。用C語言來說,就是函數調用:
x = foo(a, b, c);
被轉化爲:
foo(&x, a, b, c);
注意,這僅僅在返回值佔用大於8個字節時才發生。有的編譯器不用EDX保存返回值,所以當返回值大於4個字節時,就用這種轉換。
當然,並不是所有函數調用都直接賦值給一個變量,還可能是直接參與到某個表達式的計算中,如:
m = foo(a, b, c) + foo(d, e, f);
有或者作爲另外的函數的參數, 如:
fooo(foo(a, b, c), 3);
這些情況下,foo的返回值會被保存在一個臨時變量中參加後續的運算,所以,foo(a, b, c)還是可以被轉化成foo(&tmp, a, b, c)。
讓我們一步步地看一下在c函數調用過程中,一個棧幀是如何建立及消除的。
函數調用前調用者的動作
在我們的例子中,調用者是main,它準備調用函數foo。在函數調用前,main正在用ESP和EBP寄存器指示它自己的棧幀。
首先,main把EAX,ECX和EDX壓棧。這是一個可選的步驟,只在這三個寄存器內容需要保留的時候執行此步驟。
接着,main把傳遞給foo的參數一一進棧,最後的參數最先進棧。例如,我們的函數調用是:
a = foo(12, 15, 18);
相應的彙編語言指令是:
push dword 18
push dword 15
push dword 12
最後,main用call指令調用子函數:
call foo
當call指令執行的時候,EIP指令指針寄存器的內容被壓入棧中。因爲EIP寄存器是指向main中的下一條指令,所以現在返回地址就在棧頂了。在call指令執行完之後,下一個執行週期將從名爲foo的標記處開始。
圖2展示了call指令完成後棧的內容。圖2及後續圖中的粗線指示了函數調用前棧頂的位置。我們將會看到,當整個函數調用過程結束後,棧頂又回到了這個位置。
| |
+-------------------------+
ESP==>| 返回地址 |
+-------------------------+
| 實際參數#1 = 12 |
+-------------------------+
| 實際參數#2 = 15 |
+-------------------------+
| 實際參數#3 = 18 |
+-------------------------+
| 調用者保存的寄存器現場 |
|EAX,ECX和EDX(根據需要) |
+=========================+
| : |
EBP==>| . |
圖2
被調用者在函數調用後的動作
當函數foo,也就是被調用者取得程序的控制權,它必須做3件事:建立它自己的棧幀,爲局部變量分配空間,最後,如果需要,保存寄存器EBX,ESI和EDI的值。
首先foo必須建立它自己的棧幀。EBP寄存器現在正指向main的棧幀中的某個位置,這個值必須被保留,因此,EBP進棧。然後ESP的內容賦值給了EBP。這使得函數的參數可以通過對EBP附加一個偏移量得到,而棧寄存器ESP便可以空出來做其他事情。如此一來,幾乎所有的c函數都由如下兩個指令開始:
push ebp
mov ebp, esp
此時的棧入圖3所示。在這個場景中,第一個參數的地址是EBP加8,因爲main的EBP和返回地址各在棧中佔了4個字節。
| |
+-------------------------+
ESP=EBP==>| main的EBP |
+-------------------------+
| 返回地址 |
+-------------------------+
| 實際參數#1 = 12 | [EBP + 8]
+-------------------------+
| 實際參數#2 = 15 | [EBP + 12]
+-------------------------+
| 實際參數#3 = 18 | [EBP + 16]
+-------------------------+
| 調用者保存的寄存器現場 |
|EAX,ECX和EDX(根據需要) |
+=========================+
| : |
圖3
下一步,foo必須爲它的局部變量分配空間,同時,也必須爲它可能用到的一些臨時變量分配空間。比如,foo中的一些C語句可能包括複雜的表達式,其子表達式的中間值就必須得有地方存放。這些存放中間值的地方同城被稱爲臨時的,因爲他們可以爲下一個複雜表達式所複用。爲說明方便,我們假設我們的foo中有兩個int類型(每個4字節)的局部變量,需要額外的12字節的臨時存儲空間。簡單地把棧指針減去20便爲這20個字節分配了空間:
sub esp, 20
現在,局部變量和臨時存儲都可以通過基準指針EBP加偏移量找到了。
最後,如果foo用到EBX,ESI和EDI寄存器,則它f必須在棧裏保存它們。結果,現在的棧如圖4所示。
| : |
+-------------------------+
ESP==>|被調用者保存的寄存器現場 |
|EBX,ESI和EDI(根據需要) |
+-------------------------+
| 臨時空間 | [EBP - 20]
| |
+-------------------------+
| 局部變量#2 | [EBP - 8]
+-------------------------+
| 局部變量#1 | [EBP - 4]
+-------------------------+
EBP==>| main的EBP |
+-------------------------+
| 返回地址 |
+-------------------------+
| 實際參數#1 | [EBP + 8]
+-------------------------+
| 實際參數#2 | [EBP + 12]
+-------------------------+
| 實際參數#3 | [EBP + 16]
+-------------------------+
| 調用者保存的寄存器現場 |
|EAX,ECX和EDX(根據需要) |
+=========================+
| : |
圖4
foo的函數體現在可以執行了。這其中也許有進棧、出棧的動作,棧指針ESP也會上下移動,但EBP是保持不變的。這意味着我們可以一直用[EBP+8]找到第一個參數,而不管在函數中有多少進出棧的動作。
函數foo的執行也許還會調用別的函數,甚至遞歸地調用foo本身。然而,只要EBP寄存器在這些子調用返回時被恢復,就可以繼續用EBP加上偏移量的方式訪問實際參數,局部變量和臨時存儲。
被調用者返回前的動作
在把程序控制權返還給調用者前,被調用者foo必須先把返回值保存在EAX寄存器中。我們前面已經討論過,當返回值佔用多於4個或8個字節時,接收返回值的變量地址會作爲一個額外的指針參數被傳到函數中,而函數本身就不需要返回值了。這種情況下,被調用者直接通過內存拷貝把返回值直接拷貝到接收地址,從而省去了一次通過棧的中轉拷貝。
其次,foo必須恢復EBX,ESI和EDI寄存器的值。如果這些寄存器被修改,正如我們前面所說,我們會在foo執行開始時把它們的原始值壓入棧中。如果ESP寄存器指向如圖4所示的正確位置,寄存器的原始值就可以出棧並恢復。可見,在foo函數的執行過程中正確地跟蹤ESP是多麼的重要————也就是說,進棧和出棧操作的次數必須保持平衡。
這兩步之後,我們不再需要foo的局部變量和臨時存儲了,我們可以通過下面的指令消除棧幀:
mov esp, ebp
pop ebp
其結果就是現在棧裏的內容跟圖2中所示的棧完全一樣。現在可以執行返回指令了。從棧裏彈出返回地址,賦值給EIP寄存器。棧如圖5所示:
| |
+-------------------------+
ESP==>| 實際參數#1 |
+-------------------------+
| 實際參數#2 |
+-------------------------+
| 實際參數#3 |
+-------------------------+
| 調用者保存的寄存器現場 |
|EAX,ECX和EDX(根據需要) |
+=========================+
| : |
EBP==>| . |
圖5
i386指令集有一條“leave”指令,它與上面提到的mov和pop指令所作的動作完全相同。所以,C函數通常以這樣的指令結束:
leave
ret
調用者在返回後的動作
在程序控制權返回到調用者(也就是我們例子中的main)後,棧如圖5所示。這時,傳遞給foo的參數通常已經不需要了。我們可以把3個參數一起彈出棧,這可以通過把棧指針加12(=3個4字節)實現:
add esp, 12
如果在函數調用前,EAX,ECX和EDX寄存器的值被保存在棧中,調用者main函數現在可以把它們彈出。這個動作之後,棧頂就回到了我們開始整個函數調用過程前的位置,也就是圖5中粗線的位置。
轉自:http://hubeihuyanwei.blog.163.com/blog/static/28205284200821873911607/
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
這段代碼反彙編後,代碼是什麼呢?
#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
跟一個朋友談堆棧的時候 就寫下了這段文字,順便發到這裏給需要的看看吧
彙編初學者比較頭痛的一個問題
////////////////////////////////////////////////////////////////////
比如 我們有這樣一個C函數
1#include<stdio.h>
2long test(int a,int b)
3{
4 a = a + 1;
5 b = b + 100;
6 return a + b;
7}
8void main()
9{
10 printf("%d",test(1000,2000));
11}
12
寫成32位彙編就是這樣
1;/**///////////////////////////////////////////////////////////////////////////////////////////////////////
2.386
3.module flat,stdcall ;這裏我們用stdcall 就是函數參數 壓棧的時候從最後一個開始壓,和被調用函數負責清棧
4option casemap:none ;區分大小寫
5
6includelib msvcrt.lib ;這裏是引入類庫 相當於#include<stdio.h>了
7printf PROTO C:DWORD,:VARARG ;這個就是聲明一下我們要用的函數頭,到時候 匯編程序會自動到msvcrt.lib裏面找的了
8 ;:VARARG 表後面的參數不確定 因爲C就是這樣的printf(const char *, );
9 ;這樣的函數要注意 不是被調用函數負責清棧 因爲它本身不知道有多少個參數
10 ;而是有調用者負責清棧 下面會詳細說明
11.data
12szTextFmt BYTE '%d',0 ;這個是用來類型轉換的,跟C的一樣,字符用字節類型
13a dword 1000 ;假設
14b dword 2000 ;處理數值都用雙字 沒有int 跟long 的區別
15
16;/**//////////////////////////////////////////////////////////////////////////////////////////
17.code
18
19_test proc A:DWORD,B:DWORD
20 push ebp
21 mov ebp,esp
22 mov eax,dword ptr ss:[ebp+8]
23 add eax,1
24 mov edx,dword ptr ss:[ebp+0Ch]
25 add edx,100
26 add eax,edx
27 pop ebp
28 retn 8
29_test endp
30
31_main proc
32 push dword ptr ds:b ;反彙編我們看到的b就不是b了而是一個[*****]數字dword ptr 就是我們在ds(數據段)把[*****]
33 ;開始的一個雙字長數值取出來
34 push dword ptr ds:a ;跟她對應的還有byte ptr ****就是取一個字節出來 比如這樣mov al,byte ptr ds:szTextFmt
35 ;就把% 取出來 而不包括d
36 call _test
37 push eax ;假設push eax的地址是×××××
38 push offset szTextFmt
39 call printf
40 add esp,8
41 ret
42_main endp
43end _main
44
45;/**/////////////////////////////////////////////////////////////// 下面介紹堆棧的變化
46
首先要明白的是 操作堆棧段ss 只能用esp或ebp寄存器 其他的寄存器eax ebx edx等都不能夠用 而esp永遠指向堆棧棧頂ebp用來 在堆棧段
裏面尋址
push 指令是壓棧ESP=ESP-4
pop 指令是出棧ESP=ESP+4
我們假設main函數一開始堆棧定是ESP=400
push dword ptr ds:b ;ESP-4=396 ->裏面的值就是2000 就是b的數值
push dword ptr ds:a ;ESP-4=392 ->裏面的值就是1000 就是a的數值
call test ;ESP-4=388->裏面的數值是什麼?這個太重要了 就是我們用來找遊戲函數的原理所在。
裏面的數值就是call test 指令下一條指令的地址->即push eax的地址×××××
到了test函數裏面
push ebp ;ESP-4=384->裏面保存了當前ebp的值 而不是把ebp清零
mov ebp,esp ;這裏ESP=384就沒變化了,但是ebp=esp=384,爲什麼要這樣做呢 因爲我們要用ebp到堆棧裏面找參數
mov eax,dword ptr ss:[ebp+8] ;反彙編是這樣的 想想爲什麼a就是[ebp+8]呢
;我們往上看看堆棧裏地址392處就保存着a的值 這裏ebp=384 加上8正好就是392了
;這樣就把傳遞過來的1000拿了出來eax=1000
add eax,1 ;相當於a+1了eax=1001
mov edx,dword ptr ss:[ebp+0Ch] ; 0Ch=12 一樣道理這裏指向堆棧的地址是384+12=396 就是2000了edx=2000
add edx,100 ;相當於b+100 edx=2100
add eax,edx ;eax=eax+edx=1001+2100=3101 這裏eax已經保存了最終的結果了
;因爲win32彙編一般用eax返回結果 所以如果最終結果不是在eax裏面的話 還要把它放到eax
;比如假設我的結果保存在變量nRet裏面 最後還是要這樣mov eax,dword ptr nRet
pop ebp ;ESP=384+4=388 而保存在棧頂384的值 保存到ebp中 即恢復ebp原來的值
;因爲一開始我們就把ebp的值壓棧了,mov ebp,esp已經改變了ebp的值,這裏恢復就是保證了堆棧平衡
retn 8 ;ESP+8->396 這裏retn是由系統調用的 我們不用管 系統會自動把EIP指針指向 原來的call的下一條指令
;由於是系統自動恢復了call那裏的壓棧所以 真正返回到的時候ESP+4就是恢復了call壓棧的堆棧
;到了這個時候ESP=400 就是函數調用開始的堆棧,就是說函數調用前跟函數調用後的堆棧是一樣的
;這就是堆棧平衡
由於我們用stdcall上面retn 8就是被調用者負責恢復堆棧的意思了,函數test是被調用者,所以負責把堆棧加8,call 那裏是系統自動恢復的
push eax ;ESP-4=396->裏面保存了eax的值3101
;上面已經看到了eax保存着返回值,我們要把它傳給printf也是通過堆棧傳遞
push offset szTextFmt ;ESP-4=392->裏面保存了szTextFmt的地址 也就是C裏面的指針 實際上沒有什麼把字符串傳遞的,我們傳的都是地址
;無論是在彙編或C 所以在彙編裏沒有什麼字符串類型 用最多的就是DWORD。嘿嘿遊戲裏面傳遞參數 簡單多了
call printf ;ESP-4=388->裏面保存了下一條指令的地址
add esp,8 ;ESP+8=400 恢復了調用printf前的堆棧狀態
;上面說了由於printf後面參數是:VARARG 這樣的類型是有調用者恢復堆棧的 所以printf裏面沒有retn 8之類的指令
;這是由調用者負責清棧main是調用者 所以下面一句就是add esp,8 把堆棧恢復到調用printf之前
;而call printf那裏的壓棧 是由系統做的 恢復的工作也是系統完成 我們不用理 只是知道里面保存是返回地址就夠
;了
ret ;main 函數返回 其他的事情是系統自動搞定 我們不用理 任務完成
補充一下:上述初始化堆(esp-40)h~esp段的一段存儲單元,edi初始指向(esp-40)h處,dword ptr [edi]每次指向一處,每執行一次stos dword ptr [edi], 初始化一個存儲單元,內容在eax中(參考stos指令),然後edi自增,重複執行上述指定直到ecx計數減爲0,這裏爲12h