1. 程式人生 > >C++反彙編學習筆記7——陣列和指標以及他們的定址

C++反彙編學習筆記7——陣列和指標以及他們的定址

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

轉載請註明出處。

1.      陣列在函式內

先通過一個簡單的例子來看一下陣列和普通變數在初始化時的不同之處:

這是陣列初始化:

    42:   int nArry[5] = {1, 2, 3, 4, 5};

0042B758  mov        dword ptr [ebp-1Ch],1 

0042B75F mov         dword ptr[ebp-18h],2 

0042B766  mov        dword ptr [ebp-14h],3 

0042B76D  mov         dword ptr [ebp-10h],4 

0042B774  mov        dword ptr [ebp-0Ch],5

下面是變數的初始化:

    43:  charcChar = 'A';

0042B77B  mov        byte ptr [ebp-25h],41h 

    44:  floatfFloat = 1.0f;

0042B77F fld1 

0042B781  fstp       dword ptr [ebp-34h] 

    45:  short sShort = 1;

0042B784  mov        eax,1 

0042B789  mov        word ptr [ebp-40h],ax 

    46:  intnInt = 2;

0042B78D  mov        dword ptr [ebp-4Ch],2 

    47:   double dDouble = 2.0f;

0042B794  fld        qword ptr [[email protected] (47DC90h)] 

0042B79A fstp        qword ptr[ebp-5Ch] 

陣列中每個元素的資料型別是相同的,並且地址是連續的,通過這一點可以很容易的區別出在函式中的陣列,而全域性陣列則在後面會講到。Release版本的沒有多大變化,和區域性變數一樣優化,但是陣列不會因為被賦值常量而進行常量傳遞,程式碼如下:

.text:0042B758                 mov     [ebp+nArray], 1

.text:0042B75F                mov     [ebp+nArray+4], 2

.text:0042B766                 mov    [ebp+nArray+8], 3

.text:0042B76D                 mov     [ebp+nArray+0Ch], 4

.text:0042B774                 mov     [ebp+nArray+10h], 5

現在再來看一下字串,其實字串就是字元陣列,只是最後一個元素為‘\0’作為字串結束標誌而已。而字串的初始化就是複製位元組的過程,書上說VC++6.0編譯器是通過暫存器來賦值的,因為每個暫存器擁有4位元組,所以每次最多可以複製4位元組的內容。但是我電腦上只有VS2010,它的編譯器選擇了直接將字串常量寫進檔案然後將指標直接指向字串常量裝載入記憶體的地址。下面看這個例子使理解更加深刻一些:

    55:   char *szHello = "Hello world";

0042B74E  mov        dword ptr [szHello],offset string "Hello world" (47DD10h) 

通過監視可以看到變數所在地址:

szHello的地址中儲存的就是"Hello world"所在檔案中的地址,再根據這個地址可以找到字串:

2.      陣列作為引數

首先來看個例子(Debug版本):

這是main函式裡面陣列定義、初始化以及作為引數呼叫函式的程式碼

    57:  charszHello[20] = {0};

0042B758  mov        byte ptr [ebp-1Ch],0  ;這個位元組是陣列的首地址

0042B75C xor         eax,eax 

;下面則是陣列的剩餘19個元素的初始化,利用eax暫存器進行賦值

0042B75E  mov         dword ptr [ebp-1Bh],eax 

0042B761  mov        dword ptr [ebp-17h],eax 

0042B764  mov        dword ptr [ebp-13h],eax 

0042B767  mov        dword ptr [ebp-0Fh],eax 

;當不足4位元組時編譯器會做相應處理,這裡的3位元組被拆成2位元組和1位元組

0042B76A mov         word ptr [ebp-0Bh],ax  

0042B76E  mov        byte ptr [ebp-9],al 

    58:   Show(szHello);

0042B771  lea        eax,[ebp-1Ch] 

0042B774  push       eax  ;取出陣列首地址併入棧作為引數傳到函式裡面

0042B775  call       Show (429FA5h) 

0042B77A add         esp,4 

下面是show函式的定義:

     9: void Show(char szBuff[])

    10: {;先前程式碼略

    11:   strcpy(szBuff, "Hello World");

0042B66E  push       offset string "Hello World" (47DC6Ch)  ;獲取字串常量的首地址

0042B673  mov        eax,dword ptr [esp+8]  ;獲取引數

0042B676  push       eax  ;將其作為字串複製函式的引數入棧

0042B677  call       @ILT+1770(_strcpy) (4296EFh) 

0042B67C add         esp,8 

    12:   printf(szBuff);

;printf函式程式碼略

    13: };後續程式碼略

從以上的例子可以很清楚的看到,陣列作為函式的引數時是將陣列的首地址入棧作為引數,然後根據首地址找到陣列的每一個元素。

字串處理函式在Debug版本下非常容易識別,但是在Release版本中字串處理函式內聯到程式中,沒有call指令對函式進行呼叫,因此識別這些函式有一定的困難,但是可以根據反彙編的程式碼確定其內聯程式碼的功能,並非必須還原出原函式。下面就用一個例子來具體說明:

先是C原始碼:

main()

{

    charszHello[20] = {0};

    Show(szHello);

}

void Show(charszBuff[])

{

    strcpy(szBuff, "HelloWorld");

    printf(szBuff);

}

首先對main函式中的程式碼用IDA P ro進行反彙編得到如下程式碼:

.text:00401030 aHello          = byte ptr -18h ;這是一個數組,可以看到大小為0x14

.text:00401030 var_4           = dword ptr -4

.text:00401030

.text:00401030                 push    ebp

.text:00401031                 mov     ebp, esp

.text:00401033                 sub     esp, 18h

.text:00401036                 mov     eax, dword_40B014

.text:0040103B                 xor     eax, ebp

.text:0040103D                 mov     [ebp+var_4], eax

.text:00401040                 xor     eax, eax

;和Debug版本一樣,下面是對陣列的初始化

.text:00401042                 mov     [ebp+aHello], al

.text:00401045                 mov     dword ptr [ebp+aHello+1], eax

.text:00401048                 mov     dword ptr [ebp+aHello+5], eax

.text:0040104B                 mov     dword ptr [ebp+aHello+9], eax

.text:0040104E                 mov    dword ptr [ebp+aHello+0Dh], eax

.text:00401051                 mov     word ptr [ebp+aHello+11h], ax

.text:00401055                 mov     [ebp+aHello+13h], al

;取出陣列首地址作為函式引數入棧

.text:00401058                 lea     eax, [ebp+aHello]

.text:0040105B                 push    eax

;呼叫函式

.text:0040105C                 call    sub_401000

;後續程式碼略

下面再來看看show函式的內部程式碼:

.text:00401000 arg_0           = dword ptr  8

.text:00401000

.text:00401000                 push    ebp

.text:00401001                 mov     ebp, esp

;這是非常關鍵的一步,可以看到資料段408140偏移的4位元組資料複製到ecx暫存器

.text:00401003                 mov     ecx, ds:dword_408140

;接著再將陣列首地址存入eax,雖然它本來就存著首地址,但是我也不知道為什麼要這樣

.text:00401009                 mov     eax, [ebp+arg_0]

;可以看到這一步實現了字串的前四個位元組的賦值,開啟記憶體發現這正是“Hell”

.text:0040100C                 mov     [eax], ecx

;與上同理,此處不再贅述

.text:0040100E                 mov     edx, ds:dword_408144

.text:00401014                 mov     [eax+4], edx

.text:00401017                 mov     ecx, ds:dword_408148

;到此為止,所有字串複製完畢,下面的程式碼便是printf函式呼叫和環境恢復了

.text:0040101D                 push    eax

.text:0040101E                 mov     [eax+8], ecx

.text:00401021                 call    sub_401074

.text:00401026                 add     esp, 4

.text:00401029                 pop     ebp

.text:0040102A                 retn

由於這段程式碼和書上給的反彙編程式碼完全不同,因此幾乎都是我自己分析得到的,所以我花了好幾個小時才完全弄明白,從這裡可以看到,按照書上的東西似乎覺得自己都懂了,但是完全憑藉自己來解決問題的時候卻發現非常困難。這裡僅僅是一段小小的字串複製的程式碼就要花如此長的時間,足以顯現反彙編是多麼的不容易,至少對於我這種新手來說。所以這大概就是現在完全自動化的反編譯器很少或是質量不高的原因,因為編譯是一個不可逆的過程,想要正確地由二進位制程式碼轉換成彙編程式碼再轉換成高階程式碼是很困難的。通過這段程式碼的獨立分析,我還要不斷實踐以增加自己的經驗。技術的進步正是靠著不斷的積累!

看完了上面的程式碼,再來看個簡單的(Release版本strlen函式):

還是一樣,貼出C原始碼和反彙編程式碼,如下:

函式呼叫:

printf("%d \r\n", GetLen("Hello"));

函式定義:

//strlen函式分析

int GetLen(charszBuff[])

{

    returnstrlen(szBuff);

}

main函式:

push   offset aHello   ;"Hello";字串常量儲存在檔案中,裝載到記憶體指定位置

call   sub_401000 ;呼叫GetLen函式

push   eax

push   offset aD       ; "%d\r\n"

call   sub_40103B;呼叫printf函式

add    esp, 0Ch

xor    eax, eax

retn

下面是GetLen函式定義的反彙編程式碼:

.text:00401000 arg_0           = dword ptr  8;這裡就是字串的首地址

.text:00401000

.text:00401000                 push    ebp

.text:00401001                 mov     ebp, esp

.text:00401003                 mov     eax, [ebp+arg_0];將首地址存入eax中

.text:00401006                 lea     edx, [eax+1];取出字串的第二個元素的地址

.text:00401009                 lea     esp, [esp+0];不知這條語句是何用意

.text:00401010

.text:00401010 loc_401010:                      ; CODE XREF: sub_401000+15j

.text:00401010                 mov    cl, [eax];取出eax儲存的地址中的位元組

.text:00401012                 inc     eax;地址加1

.text:00401013                 test    cl, cl;相當於邏輯與,但不改變cl中的值

.text:00401015                 jnz     short loc_401010

;test與jnz共用,當cl為0時會使得標誌暫存器FR中的標誌位ZF=0,導致不跳轉,迴圈結束,表示字串中所有元素均已遍歷,此時下一條語句只需將現在的地址eax減去原來的地址edx即為字串的長度,相減後儲存在eax作為返回值

.text:00401017                 sub     eax, edx

.text:00401019                 pop     ebp

.text:0040101A                 retn

總結:字串處理函式經常內聯到程式中,所以在看到相關程式碼時只需將其功能恢復即可。

3.      區域性陣列作為返回值

區域性變數可以作為返回值,這是因為區域性變數雖然也在函式的棧中,但是他會在函式結束之前將值存入某個暫存器中(一般是eax),但是區域性陣列作為返回值往往只會將他的首地址存入某個暫存器,這樣一來當暫存器儲存的地址內的區域裡面的內容發生改變時便會造成原資料的改變。下面來舉個例子方便理解(Debug版本):

main函式中的呼叫:

    74:   printf("%s\r\n", RetArray());

0042B81E  call       RetArray (42A324h) 

0042B823  push       eax 

0042B824  push       offset string "%s\r\n" (47DC90h) 

0042B829  call       @ILT+4370(_printf) (42A117h) 

0042B82E  add        esp,8 

RetArray函式定義:

    22:  /*區域性陣列作為返回值*/

    23: char* RetArray()

    24:  {;先前程式碼略

25:    char szBuff[] = {"Hello World"};

;字串複製

0042B708  mov        eax,dword ptr [string "Hello World" (47DC6Ch)] 

0042B70D  mov        dword ptr [ebp-14h],eax 

0042B710  mov        ecx,dword ptr ds:[47DC70h] 

0042B716  mov        dword ptr [ebp-10h],ecx 

0042B719  mov        edx,dword ptr ds:[47DC74h] 

0042B71F mov         dword ptr[ebp-0Ch],edx 

    26:  returnszBuff;

0042B722  lea        eax,[ebp-14h]  ;將陣列的首地址存入暫存器作為返回值

    27: };後續程式碼略

從上面的程式碼看,貌似並不會出現什麼問題,在呼叫printf函式之前將eax的值作為引數入棧,然後將其所在的記憶體空間中的內容以字串的形式輸出。eax是安全的,但是以eax的值作為地址的記憶體空間並不安全。下面來看一下記憶體中的值的變化會更容易理解:

還沒退出函式之前可以看到,檢視其對應的記憶體:,可以看到裡面的值就是Hello World。那麼現在來看一下輸出是多少,,其中的記憶體變成了。是的,這就是輸出結果,和原來的字串一點關係都沒有,那這是什麼原因呢?這裡可以看到字串是利用printf函式輸出的,凡是函式在執行到時必定會有自己的棧,而程式的棧空間是每個函式都可以使用的,RetArray函式所使用的棧空間在後面又分配給了printf函式使用,此時很有可能導致字串所在記憶體的不穩定,容易發生錯誤。因此以區域性陣列或是指標作為返回值是極不明智的,因為其所在區域在函式退出後不會有任何保護,非常容易造成最後的結果是錯誤的。

為了儘量避免上述所說的錯誤,可以使用全域性陣列、靜態區域性陣列或上層函式中定義的區域性陣列來儲存被調函式中需要返回的陣列。下面就來簡單的瞭解一下全域性陣列和靜態區域性陣列。

全域性陣列的用法其實和全域性變數相同。舉一個簡單的例子:

int g_nArry[5] = {1, 2, 3, 4, 5};

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

{

    printf("%d", g_nArry[i]);

}

用IDA Pro反彙編出的結果如下:

.text:0042DE0E                 mov     [ebp+var_8], 0 ;變數i賦初值

.text:0042DE15                 jmp     short loc_42DE20

.text:0042DE17 ;---------------------------------------------------------------------------

.text:0042DE17

;loc_42DE17這塊程式碼段是在第二次及以後的迴圈中做i++操作

.text:0042DE17loc_42DE17:                             ;CODE XREF: main+4Ej

.text:0042DE17                 mov     eax, [ebp+var_8]

.text:0042DE1A                 add     eax, 1

.text:0042DE1D                 mov     [ebp+var_8], eax

.text:0042DE20

.text:0042DE20loc_42DE20:                             ;CODE XREF: main+25j

.text:0042DE20                 cmp     [ebp+var_8], 5   ;和5比較大小

.text:0042DE24                 jge     short loc_42DE40  ;大於則跳出迴圈

.text:0042DE26                 mov     eax, [ebp+var_8]

.text:0042DE29                 mov     ecx, g_nArry[eax*4] ;利用下標定址找到相應的資料

.text:0042DE30                 push    ecx

.text:0042DE31                 push    offset [email protected] ;"%d"

.text:0042DE36                 call    j_printf

.text:0042DE3B                 add     esp, 8 ;棧平衡

.text:0042DE3E                 jmp     short loc_42DE17

.text:0042DE40 ;---------------------------------------------------------------------------

.text:0042DE40

.text:0042DE40loc_42DE40:                     ; CODEXREF: main+34j;這裡就出了迴圈

再來看看全域性陣列:

.data:0048D000 g_nArry         dd 1, 2, 3, 4, 5        ;這裡我把它壓縮到了一起形成一個數組

瞭解完了全域性陣列,再來簡單的看一下靜態區域性陣列,還是看一個例子:

    83:  intnOne;

    84:  intnTwo;

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

;scanf函式程式碼略

86:    static int g_snArry[5] = {nOne, nTwo, 0};

;檢視變數g_snArry的地址為0x0048D464,所以下面的[$S1 (48D460h)]代表的就是標誌位元組

;將標誌位元組存入eax中

0042B5C3 mov         eax,dword ptr [$S1(48D460h)] 

0042B5C8 and         eax,1 

;這裡判斷靜態區域性陣列是否曾經被初始化,若是被初始化則跳過下面的程式碼

0042B5CB  jne        main+70h (42B600h) 

0042B5CD  mov        eax,dword ptr [$S1 (48D460h)] 

0042B5D2  or         eax,1  ;若沒有被初始化那將其標誌位置為1並在後面的程式碼中將其初始化

0042B5D5  mov        dword ptr [$S1 (48D460h)],eax 

0042B5DA  mov        eax,dword ptr [nOne] 

0042B5DD  mov        dword ptr [g_snArry (48D464h)],eax 

0042B5E2  mov        eax,dword ptr [nTwo] 

0042B5E5  mov        dword ptr [g_snArry+4 (48D468h)],eax 

0042B5EA  mov        dword ptr [g_snArry+8 (48D46Ch)],0 

0042B5F4 xor         eax,eax 

0042B5F6 mov         dword ptr[g_snArry+0Ch (48D470h)],eax 

0042B5FB  mov        dword ptr [g_snArry+10h (48D474h)],eax 

可以看到無論靜態區域性陣列有多少個元素,都只進行一次初始化。

4.      下標定址和指標定址

陣列可以通過下標和指標兩種方式進行定址。下標方式較為常見,效率也更高,指標方式不方便,效率也較低,因為它在訪問陣列時需要先取出指標中的地址,然後再對其進行偏移得到陣列元素,而陣列名則是一個常量地址,直接可以進行偏移計算。還是來看例子:

    95:  char *pChar = NULL;

0042B5B8  mov        dword ptr [ebp-0Ch],0 

    96:  charszBuff[10] = {0};

0042B5BF  mov        byte ptr [ebp-20h],0 

0042B5C3 xor         eax,eax 

0042B5C5 mov         dword ptr[ebp-1Fh],eax 

0042B5C8 mov         dword ptr[ebp-1Bh],eax 

0042B5CB  mov        byte ptr [ebp-17h],al 

    97:  scanf("%9s",szBuff);

0042B5CE  lea        eax,[ebp-20h] 

0042B5D1  push       eax 

0042B5D2  push       offset string "%9s" (47CC78h) 

0042B5D7  call       @ILT+3015(_scanf) (429BCCh) 

0042B5DC  add        esp,8 

    98:  pChar =szBuff;

0042B5DF  lea        eax,[ebp-20h] 

0042B5E2  mov        dword ptr [ebp-0Ch],eax 

    99:  printf("%c",*pChar);

0042B5E5  mov        eax,dword ptr [ebp-0Ch]  ;取出指標變數中的地址

0042B5E8  movsx      ecx,byte ptr [eax]  ;在這個地址處取出資料

0042B5EB  push       ecx 

0042B5EC  push       offset string "%c"(47CC94h) 

0042B5F1 call        @ILT+4110(_printf) (42A013h) 

0042B5F6 add         esp,8 

   100:   printf("%c", szBuff[0]);

0042B5F9 movsx       eax,byte ptr[ebp-20h]  ;直接取出資料

0042B5FD  push       eax 

0042B5FE  push       offset string "%c" (47CC94h) 

0042B603  call       @ILT+4110(_printf) (42A013h) 

0042B608  add        esp,8 

從上面的程式碼可以看到,指標和下標定址有著很明顯的區別,指標需要進行兩次定址,而下標只需要一次,所以下標定址的效率比較高,但是更為靈活,可以改變指標變數中的地址來訪問其他記憶體,而陣列在不越界的情況下是無法訪問到陣列以外的資料的。

這裡再來講講下標值的不同型別會帶來什麼區別:

4.1下標值為整形常量

    64:    int nArry[5] = {1, 2, 3, 4, 5};

00428488  mov        dword ptr [ebp-1Ch],1 

0042848F  mov         dword ptr [ebp-18h],2 

00428496  mov        dword ptr [ebp-14h],3 

0042849D  mov        dword ptr [ebp-10h],4 

004284A4  mov         dword ptr [ebp-0Ch],5 

    65:   

    66:    printf("%d \r\n", nArry[2]);

004284AB  mov        eax,dword ptr [ebp-14h] 

;printf函式分析略

需要將陣列元素作為引數時直接計算出其下標值

4.2   下標值為為整形變數

    69:    printf("%d \r\n", nArry[argc]);

004284BC  mov        eax,dword ptr [ebp+8]  ;首先取出argc的值,ebp+8為argc的地址

004284BF  mov        ecx,dword ptr [ebp+eax*4-1Ch]  ;取出相應的陣列元素

4.3   下標值為整形表示式

    71:    printf("%d \r\n", nArry[argc * 2]);

004284D1  mov        eax,dword ptr [ebp+8] 

004284D4  shl        eax,1  ;這裡先計算表示式的值

004284D6  mov        ecx,dword ptr [ebp+eax*4-1Ch] ;這裡和變數一樣

當整形表示式的計算結果可以計算出來時,編譯器會選擇常量摺疊,將表示式用常量代替。上面的是先進行表示式計算,然後再找到相應的陣列元素。

以上就是陣列的三種定址方式,這三種方式都可以運用在指標定址中。

4.4陣列下標越界

C/C++中並不會對陣列的越界訪問進行檢查,所以在使用下標時要時刻注意不要越界訪問記憶體中的其他資料,從而造成程式崩潰甚至更嚴重的後果。越界下標的使用和正常下標一樣,這裡不再贅述。

5.      多維陣列

多維陣列其實在記憶體中也是線性儲存的,只是在程式中編譯器將其做了相應的處理而已。在學習C等高階語言時都已經學習了多維陣列的原理,這裡就來個簡單的例子鞏固一下:

   103:  int i =0;

0042B5B8  mov        dword ptr [ebp-0Ch],0 

   104:  int j =0;

0042B5BF  mov        dword ptr [ebp-18h],0 

   105:  intnArray[4] = {1, 2, 3, 4};

;一維陣列的初始化

0042B5C6 mov         dword ptr[ebp-30h],1 

0042B5CD  mov        dword ptr [ebp-2Ch],2 

0042B5D4  mov        dword ptr [ebp-28h],3 

0042B5DB  mov        dword ptr [ebp-24h],4 

   106:  intnTwoArray[2][2] = {{1, 2},{3, 4}};

;二維陣列的初始化

0042B5E2  mov        dword ptr [ebp-48h],1 

0042B5E9  mov        dword ptr [ebp-44h],2 

0042B5F0 mov         dword ptr[ebp-40h],3 

0042B5F7 mov         dword ptr[ebp-3Ch],4 

   107:  scanf("%d%d", &i, &j);

;scanf函式呼叫略

   108:  printf("nArray= %d\r\n", nArray[i]);

0042B613  mov        eax,dword ptr [ebp-0Ch]  ;取出i

0042B616  mov        ecx,dword ptr [ebp+eax*4-30h]  ;計算偏移並取出其中元素

0042B61A push        ecx 

0042B61B  push       offset string "nArray = %d\r\n" (47CCA8h) 

0042B620  call       @ILT+4110(_printf) (42A013h) 

0042B625  add        esp,8 

   109:   printf("nTwoArray = %d\r\n", nTwoArray[i][j]);

0042B628  mov        eax,dword ptr [ebp-0Ch]  ;取i

0042B62B  lea        ecx,[ebp+eax*8-48h]  ;8為每一個一維陣列的大小,這裡是取得第i個一維陣列的首地址

0042B62F mov         edx,dword ptr [ebp-18h]  ;取j

0042B632  mov        eax,dword ptr [ecx+edx*4]  ;取出其中的元素

0042B635  push       eax 

0042B636  push       offset string "nTwoArray = %d\r\n" (47CC94h) 

0042B63B  call       @ILT+4110(_printf) (42A013h) 

0042B640  add        esp,8 

這裡一維陣列和二維陣列有著明顯的區別,一維陣列直接根據偏移量取出陣列中的元素,二維陣列則需要先計算第i個一維陣列的首地址然後根據j偏移量來得到元素。

當有一個下標為常量時:

   112:  int i =0;

0042B5B8  mov        dword ptr [ebp-0Ch],0 

   113:  intnTwoArray[2][2] = {{1, 2},{3, 4}};            //二維陣列

0042B5BF  mov        dword ptr [ebp-24h],1 

0042B5C6 mov         dword ptr[ebp-20h],2 

0042B5CD  mov        dword ptr [ebp-1Ch],3 

0042B5D4  mov        dword ptr [ebp-18h],4 

   114:  scanf("%d",&i);

;scanf函式程式碼略

   115:  printf("nTwoArray= %d\r\n", nTwoArray[1][i]);

;這裡ebp-1Ch是由ebp-24h+1*8計算而來的,利用了常量摺疊

0042B5EC  mov        eax,dword ptr [ebp-0Ch] 

0042B5EF  mov        ecx,dword ptr [ebp+eax*4-1Ch] 

0042B5F3 push        ecx 

0042B5F4 push        offset string"nTwoArray = %d\r\n" (47CC94h) 

0042B5F9 call        @ILT+4110(_printf) (42A013h) 

0042B5FE  add        esp,8 

上面分析了Debug版本的一維陣列和二維陣列之間的區別,現在來看看經編譯器優化後的陣列初始化以及Release版本下的陣列定址過程:

var_2C=dword ptr -2Ch  ;j

var_28= dword ptr -28h  ;i

var_24= dword ptr -24h  ;二維陣列首地址

var_20= dword ptr -20h

var_1C=dword ptr -1Ch

var_18= dword ptr -18h

var_14= dword ptr -14h  ;一維陣列首地址

var_10= dword ptr -10h

var_C= dword ptr -0Ch

var_8= dword ptr -8

var_4= dword ptr -4  ;不知道這個是幹嘛用的,從後面的程式碼分析應該是檢驗或是校正之類的

push   ebp

mov    ebp, esp

sub    esp, 2Ch

mov    eax, dword_40D014

xor    eax, ebp

mov    [ebp+var_4], eax

;以上程式碼不知道是幹嘛的

xor    eax, eax  ;將eax置0

mov    [ebp+var_28], eax ;i置0

mov    [ebp+var_2C], eax;j置0

mov    eax, 4

mov    ecx, 3

;兩個陣列的最後一個元素均被置為4,其他元素初始化同理

mov    [ebp+var_8], eax

mov    [ebp+var_18], eax

lea    eax, [ebp+var_2C] ;取j作為scanf的引數

mov    [ebp+var_C], ecx;置為3

mov    [ebp+var_1C], ecx

push   eax

lea    ecx, [ebp+var_28];同上

mov    edx, 2

push   ecx

push   offset aDD      ; "%d%d"

mov    [ebp+var_14], 1 ;置為1

mov    [ebp+var_10], edx ;置為2

mov    [ebp+var_24], 1

mov     [ebp+var_20], edx

;到此為止兩個陣列的所有成員均被初始化

call   sub_4011D0;呼叫scanf函式

mov    edx, [ebp+var_28] ;取出i

mov    eax, [ebp+edx*4+var_14] ;得到一維陣列相應元素

push   eax

push   offset aNarrayD ; "nArray = %d\r\n"

call   sub_401096 ;呼叫printf函式

mov    ecx, [ebp+var_2C] ;取出j

mov    edx, [ebp+var_28] ;取出i

lea    eax, [ecx+edx*2]   ;這裡並不是取第i個一維陣列的首地址,而是計算出當這個元素相對於陣列首地址的偏移個數,即把他轉化之後相當於就是一個一維陣列而不是二維陣列

mov    ecx, [ebp+eax*4+var_24] ;取出陣列中的元素

push   ecx

push   offset aNtwoarrayD ; "nTwoArray = %d\r\n"

call   sub_401096

下面的程式碼應該就是用到了var_4變數,但是我不知道它的實際用途

mov    ecx, [ebp+ var_4]

xor    ecx, ebp

add    esp, 1Ch

xor    eax, eax

call   sub_4011ED

mov    esp, ebp

pop    ebp

retn

正如上面的程式碼所表示的,記憶體給區域性變數分配空間是從大到小分配的,不僅如此,在O2情況下,當一維陣列和二維陣列初始化的內容相同時便會一起進行初始化,並且編譯器不將所有的初始化語句都放在一起是為了減少指令之間的相關性,使得指令能夠像流水線那樣執行,提高程式的效率。

其他多維陣列的分析和二維陣列相同,都是逐漸降維最後轉化成低維陣列進行處理,這裡不再贅述。

6.      指標陣列

指標陣列就是存放同一型別指標的陣列,陣列的每個元素都存放著地址,一般用於處理若干個字串的操作(如二維字元陣列)。它和陣列的區別就在於在取出陣列中的元素之後還要進行一次間接定址。下面就用一個例子來加深理解。

   126:      char * pBuff[3] = {

   127:          "Hello ",

0042840E  mov        dword ptr [pBuff],offset string "Hello " (472CA0h) 

;將字串的首地址存入陣列中,下同

   128:          "World ",

00428415  mov        dword ptr [ebp-0Ch],offset string "World " (472C98h) 

   129:          "!\r\n"

   130: };

0042841C  mov         dword ptr [ebp-8],offset string"!\r\n" (472C94h) 

   131:      for (int i = 0; i < 3; i++) {

;for迴圈程式碼略

   132:          printf(pBuff[i]);

0042843B  mov        eax,dword ptr [i]  ;取i

0042843E  mov        ecx,dword ptr pBuff[eax*4]  ;找到相應的陣列中的地址元素並放到ecx中

00428442  push       ecx 

00428443  call       @ILT+3900(_printf) (426F41h) 

00428448  add        esp,4 

   133: }

指標陣列和二維字元陣列有著很明顯的區別,若是在初始化階段,二維字元陣列是將每個字元資料賦值,而指標陣列僅僅是將字串的首地址存入陣列中。

7.      陣列指標

這是指向陣列的一個指標變數,裡面儲存的是陣列的首地址。看一個簡單的例子:

    charcArray[3][10] = {

        "Hello ",

        "World ",

        "!\r\n"

    };

;二維字元陣列初始化略

   146:    char (*pArray)[10] = cArray;

00428473  lea        eax,[ebp-28h]  ;取出cArray陣列的首地址

00428476  mov        dword ptr [ebp-34h],eax  ;將地址存入pArray中

   147:    for (int i = 0; i < 3; i++)

;for迴圈程式碼略

   148:    {

   149:        printf(*pArray);

00428491  mov        eax,dword ptr [ebp-34h] 

00428494  push       eax 

00428495  call       @ILT+3900(_printf) (426F41h) 

0042849A  add         esp,4 

   150:        pArray++;

0042849D  mov        eax,dword ptr [ebp-34h] 

004284A0  add         eax,0Ah 

;這裡每次pArray+1都是使地址+10,這是因為pArray的指標型別是長度為10的字元陣列

004284A3  mov         dword ptr [ebp-34h],eax  ;將計算過後的地址重新放入pArray指標變數中

   151:    }

指標地址的運算公式:指標變數的地址 += ( sizeof(指標型別) * 數值 ),這裡的指標型別為char[10],所以每次指標變數+1時就相當於變數中的地址+10。

下面再來看一下二級指標以及他和指向二維字元陣列的指標的區別。二級指標由於其指標的型別是指標型別,所以每次+1都只會在地址上+4,而指向二維字元陣列的指標的型別是陣列,所以需要根據陣列的大小來決定加多少。下面來看一個輸出main函式引數的例子:

voidmain(int argc, char *argv[ ], char *envp[ ] )

   153: {

   154:    for (int i = 1; i < argc; i++)

;for迴圈程式碼略

   155:    {

   156:        printf(argv[i]);

00428428  mov        eax,dword ptr [i]  ;取i

0042842B  mov        ecx,dword ptr [argv]  ;取出引數argv首地址

0042842E  mov        edx,dword ptr [ecx+eax*4]  ;獲得第i個argv引數的首地址

00428431  push       edx 

00428432  call       @ILT+3900(_printf) (426F41h) 

00428437  add        esp,4 

   157:    }

158:}

從上面可以看到argv是二級指標,取出裡面的元素時必須要進行兩次取址

8.      函式指標

call指令會跳轉到函式的首地址處然後執行函式內部的程式碼,這樣它也可以使用指標來呼叫。還是看一個簡單的例子:

   162:     void(__cdecl *pShow)(void) = Show;

;函式名就是函式的首地址,是一個常量

0042840E  mov        dword ptr [pShow],offset Show (427027h) 

   163:

   164:     pShow();

00428415  mov        esi,esp 

00428417  call       dword ptr [pShow]  ;通過指標間接呼叫函式

;下面兩句都是棧平衡檢查,Debug版本所特有

0042841A  cmp         esi,esp 

0042841C  call        @ILT+3020(__RTC_CheckEsp)(426BD1h) 

   165:     Show();

00428421  call       Show (427027h)  ;直接函式呼叫

函式指標是比較特殊的指標,它儲存的是程式碼段而非資料段的地址,所以不存在地址偏移的情況,編譯器會在編譯階段對函式指標進行檢查以防止其進行加減等沒有意義的運算。

上面的函式的返回值和引數均為void,下面再來看一個有返回值和引數的函式指標:

   167:    int(__stdcall *pShow)(int) = Show; ;在定義指標時就定義返回值和引數

0042840E  mov        dword ptr [pShow],offset Show (4262F3h) 

   168:    intnRet = pShow(5);

00428415  mov        esi,esp 

00428417  push       5 

00428419  call       dword ptr [pShow]  ;間接呼叫函式

;下面是棧平衡檢查

0042841C  cmp         esi,esp 

0042841E  call       @ILT+3020(__RTC_CheckEsp) (426BD1h) 

00428423  mov        dword ptr [nRet],eax 

   169:    printf("ret= %d \r\n", nRet);

00428426  mov        eax,dword ptr [nRet] 

00428429  push       eax 

0042842A  push        offset string "ret = %d \r\n"(472C90h) 

0042842F  call        @ILT+3900(_printf) (426F41h) 

00428434  add        esp,8 

一個函式指標只能儲存同一種類型的函式的地址,包括函式的引數和返回值,否則無法傳遞引數和返回值,也無法進行棧平衡檢查。