注: 本文摘自互聯網,本人看了按理解加了些東西
為了弄清楚函數的堆棧,Google了一下,找到下面代碼作為實驗
1
#include <stdio.h>
2
#include <string.h>
3
4
void func1(int input1, int input2)
5

{
6
int j;
7
char c;
8
short k;
9
10
j = 0;
11
c = 'a';
12
k = 1;
13
14
printf("sum=%d\n", input1+input2);
15
16
return;
17
}
18
19
int main()
20

{
21
char output[8] = "abcdef";
22
int i, j;
23
24
i=2;
25
j=3;
26
func1(i,j);
27
28
printf("%s\r\n", output);
29
30
return 0;
31
}
32
本人在 vc6 + winxp 進行調試
vc6 中在 fun1 設置斷點,按F5, 到達斷點后,選擇菜單 view -> Disassembly 就可以看到代碼對應的匯編語句
調用 func1() 之前
26: func1(i,j);
004010D7 mov ecx,dword ptr [ebp-10h]
004010DA push ecx
004010DB mov edx,dword ptr [ebp-0Ch]
004010DE push edx
004010DF call @ILT+0(func1) (00401005)
004010E4 add esp,8
------------------------------------------------
EAX = 00000000 EBX = 7FFDE000 ECX = 00006665 EDX = 00370E00
ESI = 00000000 EDI = 0012FF80
EIP = 004010D7 ESP = 0012FF24 EBP = 0012FF80 EFL = 00000246
------------------------------------------------
i,j 分別存放在棧中,地址分別是
------------------------------------------------
ebp-10h = 0x0012FF80h - 0x10h = 0x0012FF70h
ebp-0Ch = 0x0012FF80h - 0x0Ch = 0x0012FF74h
0012FF70 03 00 00 00 02 00 00 .......
0012FF77 00 61 62 63 64 65 66 .abcdef
0012FF6D CC CC CC 03 00 00 00 燙.....
0012FF74 02 00 00 00 61 62 63 ....abc
從內存存放的內容可知, i, j 分別存放于 0x0012FF70H, 0x0012FF74H
分別占了四個字節
在調用 func1() 之前, 先將 i, j 壓入堆棧, 壓入堆棧的順序是 i, j
(故出棧的順序是j, i, 請記住, 這里的 傳遞函數參數 默認是 __cdecl)
接著調用了 call 指令
執行 call 指令之前的寄存器狀態
------------------------------------------------
EAX = 00000000 EBX = 7FFDE000 ECX = 00000003 EDX = 00000002
ESI = 00000000 EDI = 0012FF80
EIP = 004010DF ESP = 0012FF1C EBP = 0012FF80 EFL = 00000246
------------------------------------------------
執行 call 指令之后的寄存器狀態
EAX = 00000000 EBX = 7FFDE000 ECX = 00000003 EDX = 00000002
ESI = 00000000 EDI = 0012FF80
EIP = 00401005 ESP = 0012FF18 EBP = 0012FF80 EFL = 00000246
------------------------------------------------
變化的是 EIP 與 ESP
棧頂指針寄存器ESP:保存棧頂地址(指針)
此時 ESP 向低地址偏移了四個字節
可見 call 指令執行了一個 push 操作
查看 ESP 對象的地址 0x0012FF18 的內容
0012FF18 E4 10 40 00 02 00 00 ..@....
0012FF1F 00 03 00 00 00 00 00 .......
push 進去的數是 0x004010E4, 再看回調用 call 之后的代碼
26: func1(i,j);
004010D7 mov ecx,dword ptr [ebp-10h]
004010DA push ecx
004010DB mov edx,dword ptr [ebp-0Ch]
004010DE push edx
004010DF call @ILT+0(func1) (00401005)
004010E4 add esp,8
push 進去的數剛好是 call 指令后面的指令地址 0x004010E4
為什么沒有看到push指令呢?
原來在CALL操作中,隱式地把call指令后續第一條指令的地址0x004010E4 入棧,
然后再無條件地跳轉到func1()函數繼續執行。
@ILT+0(?func1@@YAXHH@Z):
00401005 jmp func1 (00401020)
004010DF call @ILT+0(func1) (00401005)
奇怪的事情是 call 后面的操作數應該是 func1 的地址 0x00401020 才對, 為何是 0x00401005?
還是一個 @ILT+0 的東東是什么?
在DEBUG版本中,VC匯編程序會產生一個函數跳轉指令表,
該表的每個表項存放一個函數的跳轉指令。
程序中的函數調用就是利用這個表來實現跳轉到相應函數的入口地址。
ILT就是函數跳轉指令表的名稱,是Import Lookup Table的縮寫;
@ILT就是函數跳轉指令表的首地址。
在DEBUG版本中增加函數跳轉指令表,其目的是加快編譯速度,當某函數的地址發生變化時,只需要修改ILT相應表項即可,而不需要修改該函數的每一處引用。
注意:在RELEASE版本中,不會生成ILT,也就是說call指令的操作數直接是函數的入口地址,例如在本例中是這樣的:call 00401020
接下來, 應該看一下 jmp func1 后, 做了哪些東西
4: void func1(int input1, int input2)
5: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,4Ch
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-4Ch]
0040102C mov ecx,13h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
------------------------------------------------
此時各寄存器的值為
EAX = 00000000 EBX = 7FFDE000 ECX = 00000003 EDX = 00000002
ESI = 00000000 EDI = 0012FF80
EIP = 00401020 ESP = 0012FF18 EBP = 0012FF80 EFL = 00000246
ebp 為 main 函數層的 棧內存基地址
esp 為 當前的棧頂地址
00401020 push ebp
00401021 mov ebp,esp
這兩個語句 先把 ebp 壓入堆棧, 再把 esp 賦值給 ebp
這兩句的作用將在后面說明
00401023 sub esp,4Ch
然后,棧頂指針esp向低地址偏移76(0x4C)字節。這里相當于為func1()函數層分配了棧內存。
(為什么偏偏是 76 是字節?)
(題外話: 平時程序調試的 stack over 又是如何造成的?)
接著又把 ebx, esi, edi 分別入棧, 目的是為了保存 main 函數層的相關內容
00401026 push ebx
00401027 push esi
00401028 push edi
最后用0xCC初始化上述為func1()函數層所分配的棧內存的每個字節。這里每一步用F11單步跟蹤,棧內存的變化你會看得更清楚。
00401029 lea edi,[ebp-4Ch] ; 將有效的地址 [ebp-0x4Ch] 賦值到 edi
0040102C mov ecx,13h ;
00401031 mov eax,0CCCCCCCCh ;
00401036 rep stos dword ptr [edi] ;
stos指令:
字符串存儲指令 STOS
格式: STOS OPRD
其中OPRD為目的串符號地址.
功能: 把AL(字節)或AX(字)中的數據存儲到DI為目的串地址指針所尋址的存儲器單元中去.指針DI將根據DF的值進行自動調整.
由于上面的指令是 dword ptr 類型
dword 表示雙字 ptr 表示取首地址
那么 stos dword ptr [edi] 執行的操作就是
將 ES:[DI]←AX,DI←DI±4 (DI 加或減是由 DF 標志位確定的)
如果是 那么 stos word ptr [edi] 的話那么就是
將 ES:[DI]←AL,DI←DI±2 (DI 加或減是由 DF 標志位確定的)
不然推出 stos BYTE ptr [edi]
注:
DF:方向標志DF位用來決定在串操作指令執行時有關指針寄存器發生調整的方向。
重復前綴
格式: REP ;CX<>0 重復執行字符串指令
REP 每執行一次后面的字符串指令后, cx減1, 直至 cx 為0
在本例中, 每次拷貝 sizeof(DWORD) 四個字節, 而堆棧大小是 76(0x4C) 個字節, 故 只需要重復執行 76 / 4 = 19(0x13) 次就可以了
故
0040102C mov ecx,13h ;
現在終于清楚
00401029 lea edi,[ebp-4Ch] ; 將有效的地址 [ebp-0x4Ch] 賦值到 edi
0040102C mov ecx,13h ;
00401031 mov eax,0CCCCCCCCh ;
00401036 rep stos dword ptr [edi] ;
的作用就是把堆棧的數據置為 0xCC;
困了,先去睡下再寫第2集~~