Stack Frame解析
這篇文章以VC6.0 Debug模式下的Disassembly程式來做說明,要看Disassembly視窗,步驟如下:
(1)按《F10F11》進入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動作是互相對應的,將pushstack程式執行前暫存器的值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之後接著要執行的程式位置記錄起來(pushstack),在此範例中
就是statement的位置,所以在func()stack frame裡面,最後esp會指向return address
接下來若還是不清楚的話,將MSDN中這部份的介紹轉載下來做加強:

Figure 1. Stack Frame Creation

The compiler generates code to access the parameters passed to the function using positive offsets to BP ([BP + XXXX]). Negative offsets from BP ([BP – XXXX]) access the function's local variables. This happens for all C functions—near functions, far functions, and functions compiled with the /Gw option.

 

接著開始要從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,40h40h這是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,espesp的值設定給ebp之後,stack frameebp才真正指向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-8array[8]ebp-0ch,…
array[10]顯然已經超出陣列的邊界,而array[10]的位址是ebp-4,即是變數num的位址,所以印出來是0
那你加入array[11]=0;這一行試試。編譯器會告知你該記憶體不能為"written"。
基本上stack frame裡,你只裡使用ebp-4ebp-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 

Hosted by www.Geocities.ws

1