Stack Frame解析
這篇文章以VC6.0 Debug模式下的Disassembly程式來做說明,要看Disassembly視窗,步驟如下: (1)按《F10或F11》進入Debug模式 (2)選擇《View》→《Debug Windows》→《Disassembly》 當你要知道C/C++程式碼被VC翻成什麼樣的assembly時,你就可以這樣觀察。 來介紹Stack Frame Layout,首先是standard prolog code:
:::::::::::::::::::::::::::::::::::::::::::::: <1> push ebp ; Save ebp <2> mov ebp, esp ; Set stack frame pointer <3> sub esp, localbytes ; Allocate space for locals <4> push <registers> ; Save registers :::::::::::::::::::::::::::::::::::::::::::::: [說明] (1)ebp暫存器在stack frame中的功用是用來指向stack frame的起始處,又稱為frame pointer。 故將ebp暫存器推入stack中,儲存ebp暫存器的值,避免影響其它程式。這是一種很重要的習慣,保持程式 呼叫前後暫存器的值不變,以免破壞其它程式的執行。所以<4>的目的也是如此。 (2)程式<3>的目的是用來配置出區域變數及程式執行中所需要用來儲存臨時物件的空間,稍後會有更清楚介紹。
接著介紹standard epilog code: :::::::::::::::::::::::::::::::::::::::::::::: <1> pop <registers> ; Restore registers <2> mov esp, ebp ; Restore stack pointer <3> pop ebp ; Restore ebp <4> ret ; Return from function :::::::::::::::::::::::::::::::::::::::::::::: [說明] (1)pop的動作與push動作是互相對應的,將push進stack程式執行前暫存器的值pop回暫存器,使得離開程式之後, 暫存器的值回復到原本程式執行前的值。目的就是用來保持程式呼叫前後暫存器的值不變。 (2)程式<2>將配置出來的space解除掉,相當於區域變數的生命週期結束
接下來利用圖解的方式來說明以上的步驟:
以上的圖示是standard prolog code的部份,所產生的space就是區域變數及臨時物件的記憶體空間。
以上的圖示是standard epilog code的部份,注意esp的位置,圖中的return address是在caller呼叫此函數
程式下一個程式的位置。
舉個例子:
:::::::::::::::::::::::::::::::::::::::::::::: void func() { return ; } void main() { func(); statement; } :::::::::::::::::::::::::::::::::::::::::::::: 當main()呼叫func()時,將func()return之後接著要執行的程式位置記錄起來(即push到stack中),在此範例中 就是statement的位置,所以在func()的stack frame裡面,最後esp會指向return address。
接下來若還是不清楚的話,將MSDN中這部份的介紹轉載下來做加強:
Figure
1. Stack Frame Creation
接著開始要從VC中去說明,先看個最基本C語言的程式:
:::::::::::::::::::::::::::::::::::::::::::::: #include <stdio.h> void main() { return; } :::::::::::::::::::::::::::::::::::::::::::::: 從Disassembly中查到以下的:
:::::::::::::::::::::::::::::::::::::::::::::: 3: void main() 4: { 00401010 push ebp 00401011 mov ebp,esp 00401013 sub esp,40h 00401016 push ebx 00401017 push esi 00401018 push edi 00401019 lea edi,[ebp-40h] 0040101C mov ecx,10h 00401021 mov eax,0CCCCCCCCh 00401026 rep stos dword ptr [edi] 5: return; 6: } 00401028 pop edi 00401029 pop esi 0040102A pop ebx 0040102B mov esp,ebp 0040102D pop ebp 0040102E ret :::::::::::::::::::::::::::::::::::::::::::::: [說明] (1)push的指令不是我們要說明的重點,重點已在前面敘述過 (2)sub esp,40h:40h這是compiler配置出來用來儲存臨時物件,這是函數最基本的stack space (3)push esi , push edi , ... , mov eax,0CCCCCCCCh ,rep stos dword ptr [edi] 將[ebp-4]到[ebp-40h]填入0CCCCCCCCh,那0CCCCCCCCh是什麼東東呢?在MSDN中,有這樣的描述:
There are a
few things to note about location:
·
The first is that the routine in the runtime is memmove, which is
perhaps not what you would expect. The Microsoft Visual C++ compiler is very
smart when it comes to code generation and will take the easiest route. While
writing this article, I called memset to set 20 bytes to a value. Rather than
have the overhead of a loop, the compiler did five long movs.
·
The second thing to note is that the parameters are quite clear—0x4d2
is our old friend 1234. However, cccccccc is less familiar. That is the
uninitialized pointer q—it has taken on that value because the memory
that represents the pointer was uninitialized. We can clearly see that the
values being passed into the runtime are invalid in this case, and it is worth
looking at other patterns that you might see while debugging:
Table
1. Potential patterns
Pattern |
Description |
0xFDFDFDFD |
No
man's land (normally outside of a process) |
0xDDDDDDDD |
Freed
memory |
0xCDCDCDCD |
Uninitialized
(global) |
0xCCCCCCCC |
Uninitialized
locals (on the stack) |
These values
are undocumented and subject to change, but any sort of a signpost can be
helpful while debugging. Just because you don't see those values doesn't mean
that the values in memory are valid, of course. These are just some of the
common patterns.
0CCCCCCCCh表示目前的資料是未初始化的,有助於你在Debug時瞭解目前資料是否在程式中已初始化,或是不正當的處理。
當你在程式有初始化區域變數時,這個值就會因初始化而覆蓋掉。
來看看有點料的程式:
::::::::::::::::::::::::::::::::::::::::::::::
#include <stdio.h>
void main()
{
int num = 0;
int array[10];
array[9]=1;
printf("%d\n",array[10]);
return;
}
::::::::::::::::::::::::::::::::::::::::::::::
Debug開始前,請從【View】→【Debug
Windows】開啟【Memory】、【Registers】、【Disassembly】
畫面如下(排版方式隨意):
需要注意的地方已經用藍色地方框選出來了! (1)Registers視窗的【EBP】 (2)Memory視窗的【Address】 (3)Disassembly視窗的【黃色箭頭】 現在在Disassembly的視窗中(點選黃色箭頭一下或是隨意按Disassembly視窗一下),按《F11》,直到黃色箭頭通過 mov ebp,esp,esp的值設定給ebp之後,stack frame的ebp才真正指向stack frame的起始處,畫面:
做以下的動作: (1)將Address的欄位,改為與EBP的值一樣 (2)然後將Memory視窗中的與EBP值相同的位址,調整捲軸,將它調整約離Address欄位約13行。 目的是為了觀看填補0CCCCCCCCh的動作。 然後將游鼠在Disassembly視窗內按一下(意思是將滑鼠的游標轉移到Disassembly視窗,若沒有使游標轉移到Disassembly視 窗中而按F11的話,會真接跳到下一個行C/C++程式的組語指令),然後開始按F11,當黃色箭頭指到rep stos時,注意觀察 Memory視窗的動作,會出現CC的紅色字眼。若沒有的話,表示Address欄位錯了!怎麼從記憶體看資料會了之後,現在來 觀察程式的code:
:::::::::::::::::::::::::::::::::::::::::::::: 00401021 mov eax,0CCCCCCCCh 00401026 rep stos dword ptr [edi] 5: int num = 0; 00401028 mov dword ptr [ebp-4],0 6: int array[10]; 7: array[9]=1; 0040102F mov dword ptr [ebp-8],1 8: printf("%d\n",array[10]); 00401036 mov eax,dword ptr [ebp-4] 00401039 push eax 0040103A push offset string "%d\n" (0042001c) 0040103F call printf (004010a0) 00401044 add esp,8 //restore stack 9: return; 10: } ::::::::::::::::::::::::::::::::::::::::::::::
array[9]是ebp-8,array[8]是ebp-0ch,… array[10]顯然已經超出陣列的邊界,而array[10]的位址是ebp-4,即是變數num的位址,所以印出來是0 那你加入array[11]=0;這一行試試。編譯器會告知你該記憶體不能為"written"。 基本上stack frame裡,你只裡使用ebp-4到ebp-localbytes,其它只能讀取,但不能修改。 系統知道那些記憶體是你有權利使用,而那些記憶體沒有,若使用到未授權的記憶體空間時,此時就會發生錯誤。
以上使用的模式是在Debug Version若是Release Version的話,就無法得到資訊。 若想更進一步瞭解更多的資訊,可在閱讀《C++ Reverse Disassembly》 參考: (1)Assembly Language For Intel-Based Computers 4/E (2)MSDN C++ Language Reference:Considerations for Writing Prolog/Epilog Code C Language Reference:Considerations when Writing Prolog/Epilog Code Troubleshooting Common Problems with Applications: Debugging in the Real World Microsoft Windows and the C Compiler Options
Written By James On 2004/02/08