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