1. 程式人生 > >C++反彙編學習筆記6——變數在記憶體中的位置和訪問方式

C++反彙編學習筆記6——變數在記憶體中的位置和訪問方式

兩年前寫的,歡迎大家吐槽!

轉載請註明出處。

1.   全域性變數和區域性變數的區別

具有初始值的全域性變數在原始碼連結時就被寫入所建立的PE檔案,當該檔案被執行時作業系統分析各個節中的資料填入對應的記憶體地址中,這時全域性變數就已經存在了,等PE檔案的分析和載入工作完成之後才執行入口點的程式碼。所以全域性變數不受作用域的影響,程式的任何位置都可以訪問。下面就來具體講述不同點。

全域性變數和常量類似都是被寫入檔案中,因此生命週期和模組相同,而其與區域性變數最大的不同之處便是生命週期。按照上面所說,全域性變數在執行第一條程式碼前便存在,知道程式退出銷燬,而區域性變數的生命週期則僅限於函式作用域內,超出作用域便會由棧平衡操作來釋放其空間。在訪問方式上,區域性變數是通過棧指標來訪問的,但是全部變數不在棧中,就無法用棧指標訪問。下面就來說說是如何訪問全域性變數的。還是先看一個例子(Debug版本):

    19: void main()

    20: {

;棧空間分配和初始化儲存環境略

    21:   scanf("%d", &g_nVariableType);

0042B45E  push       offset g_nVariableType (48C000h)  ;通過直接定址來直接訪問全域性變數

0042B463  push       offset string "%d, %d" (47CC80h) 

0042B468  call       @ILT+3005(_scanf) (429BC2h) 

0042B46D  add        esp,8 

    22:  printf("%d\r\n",g_nVariableType);

0042B470  mov        eax,dword ptr [g_nVariableType (48C000h)]  ;此處同上

0042B475  push       eax 

0042B476  push       offset string "%d %d\r\n" (47CC74h) 

0042B47B  call       @ILT+4085(_printf) (429FFAh) 

0042B480  add        esp,8 

    23: };棧平衡操作及環境恢復略

可以很清楚的看到全域性變數是用直接定址來訪問的,這是因為全域性變數儲存在檔案中,載入至記憶體時也會隨著檔案載入到記憶體的固定偏移位置,所以編譯器可以決定其用直接定址找到它。而區域性變數是儲存在棧中的,無法確定其地址,故無法使用直接定址方式,應採用相對定址。下面再來看一下多個全域性變數的情況(Debug版本):

    19: void main()

    20: {;棧空間分配和初始化儲存環境略

    21:   /*scanf("%d", &g_nVariableType);

    22:  printf("%d\r\n",g_nVariableType);*/

    23: //全域性變數與區域性變數對比

    24:  intnOne = 1;

0042D8EE                 mov     [ebp-8], 1 ;區域性變數的定義

    25:  intnTwo = 2;

0042D8F5                 mov     [ebp-14], 2  ;區域性變數的定義

    26: 

    27:  scanf("%d,%d", &nOne, &nTwo);

0042D8FC                 lea     eax, [ebp-14]  ;取區域性變數的地址,下同

0042D8FF                 push    eax

0042D900                 lea     ecx, [ebp+var_8]

0042D903                 push    ecx

0042D904                 push    offset "%d, %d"

0042D909  call       @ILT+3005(_scanf) (429BC2h) 

0042D90E  add        esp,0Ch 

    28:  printf("%d%d\r\n", nOne, nTwo);

0042D90E                 add     esp, 0Ch

0042D911                 mov     eax, [ebp-14]

0042D914                 push    eax

0042D915                 mov     ecx, [ebp-8]

0042D918                 push    ecx

0042D919                 push    offset "%d %d\r\n"

29:  scanf("%d, %d", &g_nVariableType,&g_nVariableType1);

0042D926  push       offset g_nVariableType1 (48C28Ch)  ;指令中的資料即為運算元的地址,書上說是利用立即數間接定址,我認為和直接定址一樣,下同

0042D92B  push       offset g_nVariableType (48C288h) 

0042D930  push       offset string "%d" (47CC80h) 

0042D935  call       @ILT+3005(_scanf) (429BC2h) 

0042D93A add         esp,0Ch 

    30:  printf("%d%d\r\n", g_nVariableType, g_nVariableType1);

0042D93D  mov        eax,dword ptr [g_nVariableType1 (48C28Ch)] 

0042D942  push       eax 

0042D943  mov        ecx,dword ptr [g_nVariableType (48C288h)] 

0042D949  push       ecx 

0042D94A push        offset string "%d\r\n"(47CC74h) 

0042D94F call        @ILT+4085(_printf)(429FFAh) 

0042D954  add        esp,0Ch 

31: };棧平衡操作及環境恢復略

從上面的例子可以看到,全域性變數的記憶體分配是從低地址開始的,通俗點說就是先定義的變數在低地址,後定義的在高地址。同時這裡也可以清楚的看到區域性變數和全域性變數的不同。

2.   區域性靜態變數

全域性靜態變數的訪問及生命週期和全域性變數相同,只是限制了不能從其他檔案訪問該變數,因此這裡不做介紹。區域性靜態變數的生命週期和全域性變數相同,並且都是在編譯連結時就寫入檔案之中,但是其作用域確是和區域性變數相同。事實上,區域性靜態變數剛開始是作為全域性變數處理,而它的初始化僅僅是對它進行賦值操作,但是編譯器是如何做到在多次執行函式時賦值操作只進行一次呢?下面通過一個例子來了解(Debug版本):

Main函式:

    30: void main()

    31: {;先前程式碼略

    32: ;區域性靜態變數被初始化為常量,下面會有詳細介紹原因

    33:      staticint g_snOne = 1;

    34:   printf("%d \r\n", g_snOne);

004272BE  mov        eax,dword ptr [g_snOne (480008h)] 

004272C3  push        eax 

004272C4  push        offset string "%d \r\n" (471C6Ch) 

004272C9  call        @ILT+3875(_printf) (425F28h) 

004272CE  add        esp,8 

    35:    ;多次對區域性靜態變數初始化

    36:   for (int i = 0; i < 5; i++)

004272D1  mov        dword ptr [i],0 

004272D8  jmp        main+43h (4272E3h) 

004272DA  mov        eax,dword ptr [i] 

004272DD  add        eax,1 

004272E0  mov        dword ptr [i],eax 

004272E3  cmp        dword ptr [i],5 

004272E7  jge        main+57h (4272F7h) 

    37:   {

    38:      ShowStatic(i);

004272E9  mov        eax,dword ptr [i] 

004272EC  push       eax 

004272ED  call       ShowStatic (42571Ch) 

004272F2  add         esp,4 

    39:   }

004272F5  jmp         main+3Ah (4272DAh) 

40:};儲存環境及棧平衡操作略

下面是被呼叫的ShowStatic函式:

    11: void ShowStatic(int nNumber)

    12: {;先前程式碼略

    13:   static int g_snNumber1 = nNumber;

0042721E  mov        eax,dword ptr [$S1 (481328h)]  ; 0x00481328空間儲存的就是g_snNumber1

;判斷標誌中的第一位是否為1,若為1則表明已經被初始化,無需再次進行初始化

00427223  and        eax,1 

00427226  jne        ShowStatic+3Dh (42723Dh) 

00427228  mov        eax,dword ptr [$S1 (481328h)] 

0042722D  or         eax,1  ;若標誌位不為1則置1

00427230  mov        dword ptr [$S1 (481328h)],eax  ;將標誌位元組寫回記憶體

00427235  mov        eax,dword ptr [nNumber] 

00427238  mov        dword ptr [g_snNumber1 (481324h)],eax ;變數賦值

14:    staticint g_snNumber2 = nNumber;

;這個區域性靜態變數的賦值操作和上面的一樣

0042723D  mov        eax,dword ptr [$S1 (481328h)]

;判斷標誌中的第二位是否為1,若為1則表明已經被初始化,無需再次進行初始化

00427242  and        eax,2 

00427245  jne        ShowStatic+5Ch (42725Ch) 

00427247  mov        eax,dword ptr [$S1 (481328h)] 

0042724C  or          eax,2 ;若第二位標誌位不為1則置1

0042724F  mov         dword ptr [$S1 (481328h)],eax 

00427254  mov        eax,dword ptr [nNumber] 

00427257  mov        dword ptr [g_snNumber2 (481320h)],eax 

    15:   printf("%d \r\n", g_snNumber1);

0042725C  mov         eax,dword ptr [g_snNumber1(481324h)] 

00427261  push       eax 

00427262  push       offset string "%d \r\n" (471C6Ch) 

00427267  call       @ILT+3875(_printf) (425F28h) 

0042726C  add         esp,8 

    16:   printf("%d \r\n", g_snNumber2);

0042726F  mov         eax,dword ptr [g_snNumber2(481320h)] 

00427274  push       eax 

00427275  push       offset string "%d \r\n" (471C6Ch) 

0042727A  call        @ILT+3875(_printf) (425F28h) 

0042727F  add         esp,8 

    17: };後續程式碼略

為了區分全域性變數和靜態區域性變數,這裡採用了一個標誌位元組,由於有8位,故最多可表示8個靜態區域性變數的初始化狀態,並且這個標誌位一般都在最先定義的區域性靜態變數附近。如果變數超過了8個,那麼再定義一個標誌位元組,這個位元組一般在第9個變數的附近。在兩個變數都定義完成之後可以檢視記憶體中的標誌位元組0x00481328 03,可以很清楚的看到變成了03,也就是0x00000011,表示前兩個靜態區域性變數均已被初始化。

現在再來看main函式中的靜態區域性變數為何會變成全域性變數。用WinHex十六進位制編輯器開啟相應的.obj檔案,可以在最後找到如下資料片段:

可以看到有g_snOne字串,但是又有點不同,這裡是經過名稱粉碎之後的名字,以此來確定變數的作用域不會超出其範圍。

3.   堆變數

C/C++中使用malloc/free或new/delete來分配和釋放堆空間。在申請空間時會返回堆空間的首地址,若堆空間沒有得到及時地釋放,則會造成記憶體洩漏。只要在程式的反彙編程式碼中發現有如下特點的程式碼,那麼就很容易識別出堆變數:

.text:004282FE                 push    0Ah

.text:00428300                 call    j_malloc

.text:00428305                 add     esp, 4

.text:00428308                 mov     [ebp+var_8], eax

以及

.text:0042831B                 push    0Ah

.text:0042831D                 call    j_operator_new

等,delete和free的呼叫也是如此。當然也要注意他們申請和釋放的地址必須對應起來看,否則會判斷錯誤。

瞭解了堆變數的申請及銷燬,再來看看編譯器是如何管理堆空間的。堆結構的每一次分配形成一個結點,每個節點都是使用雙向連結串列儲存的,結點的資料結構如下定義:

typedef struct _CrtMemBlockHeader

{

        struct_CrtMemBlockHeader * pBlockHeaderNext;

        struct_CrtMemBlockHeader * pBlockHeaderPrev;

        char*                      szFileName;

        int                         nLine;

#ifdef _WIN64

        /* Theseitems are reversed on Win64 to eliminate gaps in the struct

         * and ensure that sizeof(struct)%16 ==0, so 16-byte alignment is

         * maintained in the debug heap.

         */

        int                         nBlockUse;

        size_t                      nDataSize;

#else  /* _WIN64 */

        size_t                      nDataSize;//堆空間的資料大小

        int                         nBlockUse;

#endif  /* _WIN64 */

        long                        lRequest;//堆空間的申請次數

        unsignedchar               gap[nNoMansLandSize];//堆空間資料,第一個資料的指標就是//申請的變數的指標

        /* followedby:

         * unsigned char          data[nDataSize];

         * unsigned char          anotherGap[nNoMansLandSize];

         */

}_CrtMemBlockHeader;

如上面的結構所示,#ifdef後面的是Win64程式的定義,此處用不到。#else後面定義的是用到的資料項。pBlockHeaderNext和pBlockHeaderPrev分別指向這個結點的後繼結點(指向前一次申請的堆空間)和前導結點(指向後一次申請的堆空間)。下面就來看看記憶體中的一個堆結點:

char * pCharMalloc = (char*)malloc(10);語句分配了10個char型別的空間給pCharMalloc指標。



在0x00392A10處發現10個為0的位元組,這就是分配給pCharMalloc的10個char空間。這段空間的前後都有0xfdfdfdfd資料,這是在Debug版本下的越界檢查標誌。往前四個位元組是0x0000002d表示的是堆的申請次數,0x00392A00處的資料是堆空間的大小,這裡是10所以為0x0a,0x003929F0後面的兩個資料則是前面說的兩個儲存指標的變數的值。空間釋放後,這個結點所在的記憶體變成了如下狀況:


這樣一來程式下次便可以再次分配這塊空間。