*** 本文是《老碼識途》第一章的讀書筆記 ***html
例子代碼以下所示:函數
int Add(int x, int y) { int sum; sum = x + y; return sum; } void main() { int z; z = Add(1, 2); printf("z=%d\n", z); }
下面分析一下 Add函數的調用過程。佈局
首先斷點在z = Add(1, 2);處, 反彙編以下所示:3d
int z; z = Add(1, 2); 002C141E 6A 02 push 2 002C1420 6A 01 push 1 002C1422 E8 60 FC FF FF call 002C1087 002C1427 83 C4 08 add esp,8 002C142A 89 45 F8 mov dword ptr [ebp-8],eax
首先壓入參數1和2:指針
002C141E 6A 02 push 2 002C1420 6A 01 push 1
經過觀察ESP能夠看到參數從右到左依次入棧,ESP往低內存方向移動8字節:code
ESP=0025FCCC ... 0x0025FCAA 00 00 78 4c 33 00 bc fc 25 00 a9 fe aa 0f 78 4c 33 00 c8 fc 25 00 3d 5a b2 0f *** 01 00 00 00 02 00 00 00 *** 0x0025FCCC 00 00 00 00
而後執行:htm
002C1422 E8 60 FC FF FF call 002C1087
call指令執行時,首先壓入call指令的返回地址,即add esp,8這一句的地址002C1427:blog
0x0025FCAA 00 00 78 4c 33 00 bc fc 25 00 a9 fe aa 0f 78 4c 33 00 c8 fc 25 00 *** 27 14 2c 00 *** 01 00 00 00 02 00 00 00
而後跳轉到02C1087。02C1087處爲jmp語句,跳轉到Add函數入口地址002C13C0:ip
int Add(int x, int y) { 002C13C0 55 push ebp 002C13C1 8B EC mov ebp,esp 002C13C3 81 EC CC 00 00 00 sub esp,0CCh 002C13C9 53 push ebx 002C13CA 56 push esi 002C13CB 57 push edi 002C13CC 8D BD 34 FF FF FF lea edi,[ebp+FFFFFF34h] 002C13D2 B9 33 00 00 00 mov ecx,33h 002C13D7 B8 CC CC CC CC mov eax,0CCCCCCCCh 002C13DC F3 AB rep stos dword ptr es:[edi] int sum; sum = x + y; 002C13DE 8B 45 08 mov eax,dword ptr [ebp+8] 002C13E1 03 45 0C add eax,dword ptr [ebp+0Ch] 002C13E4 89 45 F8 mov dword ptr [ebp-8],eax return sum; 002C13E7 8B 45 F8 mov eax,dword ptr [ebp-8] } 002C13EA 5F pop edi 002C13EB 5E pop esi 002C13EC 5B pop ebx 002C13ED 8B E5 mov esp,ebp 002C13EF 5D pop ebp 002C13F0 C3 ret
目前爲止,棧上的狀況以下圖所示,從上往下內存地址從高到低:內存
+----------------+ | 2 | +----------------+ | 1 | +----------------+ ESP | return address | +------> +----------------+
此時參數可由ESP + 4,ESP + 8得到。可是因爲程序執行時ESP會變化,爲了方便定位棧上的數據,引入EBP(Extended Base Pointer,擴展基址指針寄存器),保存進入函數時ESP的值。
因爲函數能夠嵌套調用,因此在進入函數時必須將EBP的舊值保存起來,以防覆蓋EBP致使函數返回後沒法恢復EBP。這裏經過將EBP壓入棧來保存舊值。如:
002C13C0 55 push ebp 002C13C1 8B EC mov ebp,esp ... 002C13EF 5D pop ebp
因此在函數開頭有以下代碼:
int Add(int x, int y) { 002C13C0 55 push ebp 002C13C1 8B EC mov ebp,esp
此時棧上的內存佈局以下圖所示:
+----------------+ | 2 | +----------------+ | 1 | +----------------+ | return address | +----------------+ ESP | ebp | +------> +----------------+
取出1,2參數的代碼以下所示:
int sum; sum = x + y; 002C13DE 8B 45 08 mov eax,dword ptr [ebp+8] 002C13E1 03 45 0C add eax,dword ptr [ebp+0Ch]
其中ebp+8取出參數1,ebp+0Ch取出參數2(0C爲十進制的12),而後計算結果放在EAX中。
接下來有以下代碼段,將esp下移0CC,而後push ebx,esi,edi這三個寄存器:
002C13C3 81 EC CC 00 00 00 sub esp,0CCh // 1 002C13C9 53 push ebx 002C13CA 56 push esi 002C13CB 57 push edi
其中語句1是爲了給局部變量分配足夠大的棧空間,而後再保存三個寄存器的值。局部變量利用ebp定位,存於ebp和OCCh之間。
ebx,esi,edi的做用以下所示來源:
寄存器%ebx、%esi和%edi爲被調函數保存寄存器(callee-saved registers),即被調函數在覆蓋這些寄存器的值時,必須先將寄存器原值壓入棧中保存起來,並在函數返回前從棧中恢復其原值,由於主調函數可能也在使用這些寄存器。
而後運行:
002C13CC 8D BD 34 FF FF FF lea edi,[ebp+FFFFFF34h] 002C13D2 B9 33 00 00 00 mov ecx,33h 002C13D7 B8 CC CC CC CC mov eax,0CCCCCCCCh 002C13DC F3 AB rep stos dword ptr es:[edi]
首先,經過lea指令,將ebp+FFFFFF34h地址(棧幀的底部)寫入edi。而後設置ecx和eax,最後運行rep stos語句。
rep stos dword ptr es:[edi]語句的意思是:將棧上從ebp+FFFFFF34h開始的位置向高地址方向的內存賦值eax(0xCCCCCCCC),次數重複ecx(0x33, 51)次。每運行一次edi的值會增長。注意0xCCCCCCCC表明着未被初始化(int3中斷)。這樣作的緣由是防止分配好的局部變量空間中的代碼被意外執行。
示例代碼中,x+y的結果保存在局部變量sum中,由以下代碼可知,sum分配在棧上。
int sum; sum = x + y; 002C13DE 8B 45 08 mov eax,dword ptr [ebp+8] 002C13E1 03 45 0C add eax,dword ptr [ebp+0Ch] 002C13E4 89 45 F8 mov dword ptr [ebp-8],eax <<==== 結果保存在局部變量sum中
目前棧的分配狀況以下所示:
+----------------+ | 2 | +----------------+ | 1 | +----------------+ | return address | +----------------+ | ebp | +----------------+ | ? | +----------------+ ESP | sum | +------> +----------------+
「?」處的4字節是編譯器爲了防止溢出攻擊而設置的。
函數返回處的代碼以下:
002C13EA 5F pop edi 002C13EB 5E pop esi 002C13EC 5B pop ebx 002C13ED 8B E5 mov esp,ebp 002C13EF 5D pop ebp 002C13F0 C3 ret
函數返回時須要考慮兩件事情:恢復棧和保存返回值。
首先,經過pop棧恢復edi,esi和ebx的值,而後將esp恢復到ebp處,而後pop ebp,將ebp恢復舊值。此時esp指向return address:
+----------------+ | 2 | +----------------+ | 1 | +----------------+ ESP | return address | +------>+----------------+ | ebp | +----------------+ | ? | +----------------+ | sum | +----------------+
接下來運行ret指令。ret指令會將棧頂保存的地址壓入指令寄存器EIP,至關於pop eip。運行後EIP和ESP都會有變化。
而後程序跳轉到return address處,以下所示:
int z; z = Add(1, 2); 002C141E 6A 02 push 2 002C1420 6A 01 push 1 002C1422 E8 60 FC FF FF call 002C1087 002C1427 83 C4 08 add esp,8 // ret跳轉到此處 002C142A 89 45 F8 mov dword ptr [ebp-8],eax
其中add esp,8語句的目的是將1,2參數出棧,將棧恢復到函數調用以前的狀態。接下來即可以從eax中取出返回值:
002C142A 89 45 F8 mov dword ptr [ebp-8],eax
因爲ebp已被恢復,故其中ebp-8即爲臨時變量z的地址