雖然數組和指針都是針對地址操作,但它們有許多不同之處。數組是相同數據類型的數 據集合,以線性方式連續存儲在內存中;而指針只是一個保存地址值的4字節變量。在使用中,數組名是一個地址常量值,保存數組首元素地址不可修改,只能以此為基地址訪問內存數據;而指針卻是一個變量,只要修改指針中所保存的地址數據,就可以隨意訪問,不受約束。本章將深入介紹數組的構成以及兩種尋址方式。
數組在函數內
??當在函數內定義數組時,如果無其他聲明,該數組即為局部變量,擁有局部變量的所有特性。數組中的數據在內存中的存儲是線性連續的,其數據排列順序由低地址到高地址,數組名稱表示該數組的首地址,如:
int nArray[5] = {l, 2, 3,4, 5};
??此數組為5個int類型數據的集合,其占用的內存空間大小為sizeof(數據類型)*數組 中元素個數
,即4*5=20字節。如果數組nArray第一項所在地址為0X0012FF00,那么第二項所在地址為OX0012FF04,其尋址方式與指針相同。這樣看上去很像是在函 數內連續定義了 5個int類型的變量,但也不完全相同。通過下述代碼分析,我們將能夠找出它們之間的不同之處。
#include
using namespace std;
int main()
{
//局部數組的初始化
int nArry[5] = {1, 2, 3, 4, 5};
int nOne = 1;
int nTwo = 2;
int nThree = 3;
int nFour = 4;
int nFive = 5;
}
編譯arm-linux-c++ -static xx.c
,反匯編 arm-linux-objdump -D -m arm xx.out > xx.txt
000091fc
:
91fc: e92d0810 push {r4, fp}
9200: e28db004 add fp, sp, #4
9204: e24dd028 sub sp, sp, #40 ; 0x28
9208: e59f3050 ldr r3, [pc, #80] ; 9260
920c: e24bc02c sub ip, fp, #44 ; 0x2c
9210: e1a04003 mov r4, r3
9214: e8b4000f ldm r4!, {r0, r1, r2, r3}
9218: e8ac000f stmia ip!, {r0, r1, r2, r3}
921c: e5943000 ldr r3, [r4]
9220: e58c3000 str r3, [ip]
9224: e3a03001 mov r3, #1
9228: e50b3008 str r3, [fp, #-8]
922c: e3a03002 mov r3, #2
9230: e50b300c str r3, [fp, #-12]
9234: e3a03003 mov r3, #3
9238: e50b3010 str r3, [fp, #-16]
923c: e3a03004 mov r3, #4
9240: e50b3014 str r3, [fp, #-20] ; 0xffffffec
9244: e3a03005 mov r3, #5
9248: e50b3018 str r3, [fp, #-24] ; 0xffffffe8
924c: e3a03000 mov r3, #0
9250: e1a00003 mov r0, r3
9254: e24bd004 sub sp, fp, #4
9258: e8bd0810 pop {r4, fp}
925c: e12fff1e bx lr
9260: 000c8aac andeq r8, ip, ip, lsr #21
??當執行到 0x9214 的時候,r4的值為 0xc8aac (恰好對應函數末尾的一個立即數),這是一個指針,用此指針指向的一塊連續內存來初始化數組。

??0x9214 處為一條ldm指令,這條指令就是從內存加載到寄存器里面,這里是加載到r0,r1,r2,r3
寄存器,并且把r4數值加上0x4*4 。
??當執行到 0x9218的時候ip寄存器為數組首地址,stm表示把寄存器內容加載到內存里面,這里恰好就是給數組前4個元素賦值。ldm和stm成對使用,具體可見 ARM LDR/STR, LDM/STM 指令 接下來的2條指令921c,9220 給數組的第五個元素賦值,至此數組元素初始化完畢。
至于局部變量賦值,是從 0x9224 開始的幾條指令。
??在上述代碼中,連續定義的為同一類型的變最,這一點和數組相同。但是,這幾個局部變量的類型不同時,將更容易區分出它們與數組間的不同之處。將 5 個局部變量修改為如下所示。
char cChar = 'A';
float fFloat = 1.0f;
short sShort = 1;
int nInt = 2;
double dDouble = 2.0f;
觀察其反匯編代碼:
9224: e3a03041 mov r3, #65 ; 0x41
9228: e54b3005 strb r3, [fp, #-5]
922c: e3a035fe mov r3, #1065353216 ; 0x3f800000
9230: e50b300c str r3, [fp, #-12]
9234: e3a03001 mov r3, #1
9238: e14b30be strh r3, [fp, #-14]
923c: e3a03002 mov r3, #2
9240: e50b3014 str r3, [fp, #-20] ; 0xffffffec
9244: e3a02000 mov r2, #0
9248: e3a03101 mov r3, #1073741824 ; 0x40000000
924c: e14b21fc strd r2, [fp, #-28] ; 0xffffffe4
??A的ascii編碼值是0x41,1.0f對應的IEEE標準32位表示就是0x3f800000,這個換算可以使用前面提到的進制工具。 double類型占用8字節這個賦值用到了strd指令,屬于arm擴展的64bit指令。同樣2.0的IEEE標準64bit內存形式0x4000000000000000。
??從以上代碼中可以看出,毎一次為局部變量賦值時的類型都不相同,根據此特征即可判 斷這些局部變量不是數組中的元素,因為數組中的各項元素為間一類型數據,以此便可區分 局部變量與數組。對于數組的識別,應判斷數據在內存中是否連續并且類型是否一致,均符合即可將此段 數據視為數組。對于全局數組的識別也比較簡單,具體看后文講解。
??學習了數組,就不得不提一下字符串在C++中,字符串本身就是數組,根據約定,該數組的最后一個數據統一使用0作為字符串結束符。
??在g++ 編譯器下為字符類型的數組賦值(初始化)其實是復制字符串的過程。這里并不是單字節復制,而是每次復制4字節的數據。兩個內存間的數據傳遞需要借用寄存器,而每個寄存器一次性可以保存4字節的 數據,如果以單字節的方式復制就會浪費掉3字節的空間,而且多次數據傳遞也會降低執行 效率,所以編譯器采用4字節的復制方式,如下代碼所示。
int main()
{
char azHello[] = "Hello World";
}
000091fc
:
91fc: e52db004 push {fp} ; (str fp, [sp, #-4]!)
9200: e28db000 add fp, sp, #0
9204: e24dd014 sub sp, sp, #20
9208: e59f201c ldr r2, [pc, #28] ; 922c
920c: e24b3010 sub r3, fp, #16
9210: e8920007 ldm r2, {r0, r1, r2}
9214: e8830007 stm r3, {r0, r1, r2}
9218: e3a03000 mov r3, #0
921c: e1a00003 mov r0, r3
9220: e24bd000 sub sp, fp, #0
9224: e49db004 pop {fp} ; (ldr fp, [sp], #4)
9228: e12fff1e bx lr
922c: 000c8a7c andeq r8, ip, ip, ror sl
??根據上面經驗,不用動態調試,分析也知道當執行完指令 0x9208 之后r2的值為 0xc8a7c。并且在這個地址處存放著 Hello World
這個字符串。在0x920c處執行完之后 r3指向azHello(?臻g)數組首地址。然后后面2條指令是利用r0,r1,r2寄存器作為中轉把Hello World
字串拷貝到azHello的?臻g中。
??在上面代碼中,字符串長度為12字節,即4的倍數。當字符串的長度不為4的倍數時,又如何以4字節的方式復制數據呢?通過實例來分析下:
int main()
{
char azHello[] = "Hello Worl"; //將原字符串中的字符 d,去掉
}
對應的反匯編:
000091fc
:
91fc: e52db004 push {fp} ; (str fp, [sp, #-4]!)
9200: e28db000 add fp, sp, #0
9204: e24dd014 sub sp, sp, #20
9208: e59f202c ldr r2, [pc, #44] ; 923c
920c: e24b3010 sub r3, fp, #16
9210: e8920007 ldm r2, {r0, r1, r2}
9214: e8a30003 stmia r3!, {r0, r1}
9218: e1c320b0 strh r2, [r3]
921c: e2833002 add r3, r3, #2
9220: e1a02822 lsr r2, r2, #16
9224: e5c32000 strb r2, [r3]
9228: e3a03000 mov r3, #0
922c: e1a00003 mov r0, r3
9230: e24bd000 sub sp, fp, #0
9234: e49db004 pop {fp} ; (ldr fp, [sp], #4)
9238: e12fff1e bx lr
923c: 000c8a8c andeq r8, ip, ip, lsl #21
??上面處理方式是在最后一次不等于4字節的數據復制過程中按照1或者2字節的方式復制即可。字符串的前面數據的復制過程沒有變化,最后3字節的字符被拆分為兩部分,先復制2字節的數據strh,然后再復制剩余的1字節的數據strb。
數組作為參數
??在上面分析了局部數組的定義以及初始化過程。數組中的數據元素連續存儲, 并且數組是同類型數據的集合。當作為參數傳遞時,數組所占的內存大小通常大干4字節, 那么它是如何將數據傳遞到目標函數中并使用的呢,先看下面實例代碼:
#include
#include
using namespace std;
// 參數為字符數組
void Show(char szBuff[])
{
strcpy(szBuff, "Hello World");
cout << szBuff << endl;
}
int main()
{
char szHello[20] = {0};
Show(szHello);
}
編譯 arm-linux-c++ -static 3.c
反匯編代碼:
000091fc <_Z4ShowPc>:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd008 sub sp, sp, #8
9208: e50b0008 str r0, [fp, #-8] ;函數參數到棧中
920c: e51b2008 ldr r2, [fp, #-8] ;函數參數到r2
9210: e59f303c ldr r3, [pc, #60] ; 9254 <_Z4ShowPc+0x58>
;執行完之后r3 為字符串首地址
9214: e1a01002 mov r1, r2 ;函數參數到r1
9218: e1a02003 mov r2, r3 ;字串首地址到r2
921c: e3a0300c mov r3, #12 ;字串長度r3
9220: e1a00001 mov r0, r1 ;函數參數szBuff到r0
9224: e1a01002 mov r1, r2 ;字串首地址到r1
9228: e1a02003 mov r2, r3 ;字串長度r2
922c: eb01d74b bl 7ef60 ; 這里把strcpy換成memcpy了
9230: e59f0020 ldr r0, [pc, #32] ; 9258 <_Z4ShowPc+0x5c>
9234: e51b1008 ldr r1, [fp, #-8] ;棧中獲取參數到r1
9238: eb0008be bl b538 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
923c: e1a03000 mov r3, r0
9240: e1a00003 mov r0, r3
9244: e59f1010 ldr r1, [pc, #16] ; 925c <_Z4ShowPc+0x60>
9248: eb000482 bl a458 <_ZNSolsEPFRSoS_E>
924c: e24bd004 sub sp, fp, #4
9250: e8bd8800 pop {fp, pc}
9254: 000c8b1c andeq r8, ip, ip, lsl fp
9258: 000f7384 andeq r7, pc, r4, lsl #7
925c: 0000af38 andeq sl, r0, r8, lsr pc
00009260
:
9260: e92d4800 push {fp, lr}
9264: e28db004 add fp, sp, #4
9268: e24dd018 sub sp, sp, #24
926c: e24b3018 sub r3, fp, #24 ;r3指向數組首地址
9270: e3a02000 mov r2, #0
9274: e5832000 str r2, [r3]
9278: e2833004 add r3, r3, #4
927c: e3a02000 mov r2, #0
9280: e5832000 str r2, [r3]
9284: e2833004 add r3, r3, #4
9288: e3a02000 mov r2, #0
928c: e5832000 str r2, [r3]
9290: e2833004 add r3, r3, #4
9294: e3a02000 mov r2, #0
9298: e5832000 str r2, [r3]
929c: e2833004 add r3, r3, #4
92a0: e3a02000 mov r2, #0
92a4: e5832000 str r2, [r3] ;到這里為止,初始化數組元素都為0
92a8: e2833004 add r3, r3, #4
92ac: e24b3018 sub r3, fp, #24
92b0: e1a00003 mov r0, r3 ;傳遞數組首地址到r0
92b4: ebffffd0 bl 91fc <_Z4ShowPc>
92b8: e3a03000 mov r3, #0
92bc: e1a00003 mov r0, r3
92c0: e24bd004 sub sp, fp, #4
92c4: e8bd8800 pop {fp, pc}
??在上述代碼中,當數組作為參數時,數組的下標值被省略。這是因為,當數組作為函數形參時,函數參數中保存的是數組的首地址,是一個指針變量。
??雖然參數是指針變,但需要特別注意的是,實參數組名為常量值,而指針或形參數組為變量。使用sizeof (數組名)可以獲取數組的總大小,而對指針或者形參中保存的數組名 使用sizeof 只能得到當前平臺的指針長度,這里是32位的環境,所以指針的長度為4字節。
因此,在編寫代碼的過程中應避免如下錯誤:
void Show(char szBuff[])
{
int nLen = 0 ; //保存字符串長度變量
//錯誤的使用方法,此時szBuff為指針類型,并非數紐,只能得到4字節長度
nLen = sizeof(szBuff);
//正確的使用方法,使用獲取字符串長度函數strlen
nLen = strlen(szBuff);
}
再看下strcpy的反匯編分析:
0007d5a4 :
7d5a4: e0612000 rsb r2, r1, r0 ;r2 = r0 - r1
7d5a8: e2411001 sub r1, r1, #1 ;r1 = r1 - 1
7d5ac: e5f13001 ldrb r3, [r1, #1]! ;從r1地址處加載到r3,并且r1自加1
7d5b0: e7c13002 strb r3, [r1, r2] ;存儲到內存r1+r2(也就是r0)處
7d5b4: e3530000 cmp r3, #0 ;是不是結尾字符
7d5b8: 1afffffb bne 7d5ac
7d5bc: e12fff1e bx lr
可以看出這里簡單的ldrb,strb指令來每個字節的拷貝數值。
數組作為返回值
??上面講解了數組作為參數的用途,本節將講解數組在函數中的另一個用處:作為函數返回值的處理過程。
??數組作為函數的返回值與作為函數的參數大同小異,都是將數組的首地址以指針的方式進行傳遞,但是它們也有不同。當數組作為參數時,其定義所在的作用域必然在函數調用以外,在調用之前已經存在。所以,在函數中對數組進行操作是沒有問題的,而數組作為函數返回值則存在著一定的風險。
??當數組為局部變量數據時,便產生了穩定性問題,當退出函數時,需要平衡棧,而數組是作為局部變量存在,其內存空間在當前函數的棧內。如果此時函數退出,棧中定義的數據 將變得不穩定。由于函數退出后sp會回歸到調用前的位置上,而函數內的局部數組在 sp 之下,隨時都有可能由在其他函數的調用過程中產生的棧操作指令將其數據破壞。數據的破壞將導致函數返回結果具備不確定性,影響程序的結果,比如如下代碼:
int main()
{
char *array;
char a,b,c,d;
array = RetArray();
//a = 'a';
//b = 'b';
//c = 'c';
//d = 'd';
cout << array << endl;
}
自己嘗試,可以看到在編譯期間已經給出了警告:
3.c: In function 'char* RetArray()':
3.c:9:7: warning: address of local variable 'szBuff' returned [-Wreturn-local-addr]
char szBuff[] = {"Hello World"};
而且 main 函數里面對字符變量賦值,可以影響到array指向的內存。
??如果既想使用數組作為返回值,又要避免上面的錯誤,可以使用全局數組、靜態數 組或是上層調用函數中定義的局部數組,這里就不再一一舉例。
下標尋址和指針尋址
??訪問數組的方法有兩種:通過下標訪問(尋址)和通過指針訪問(尋址)。因為使用方便,通過下標訪問的方式比較常用,其格式為數組名[標號]
指針尋址的方式不但沒有 下標尋址的方式便利,而且效率也比下標尋址低。由于指針是存放地址數據的變量類型,因 此在數據訪問的過程中需要先取出指針變量中的數據,然后再針對此數據進行地址偏移計 算,從而尋址到目標數據。數組名本身就是常量地址,可直接針對數組名所代替的地址值進行偏移計算。通過下面代碼分析出差距:
int main()
{
char * pChar = NULL;
char szBuff[] = "Hello World";
pChar = szBuff;
cout << *pChar << endl;
cout << szBuff[0] << endl;
}
反匯編指令如下所示:
000091fc
:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd010 sub sp, sp, #16
9208: e3a03000 mov r3, #0
920c: e50b3008 str r3, [fp, #-8] ; *pChar = NULL
9210: e59f2064 ldr r2, [pc, #100] ; 927c
;r2指向字符串 "Hello World"
9214: e24b3014 sub r3, fp, #20 ; r3為數組首地址
9218: e8920007 ldm r2, {r0, r1, r2} ;r2指向的字符串拷貝到r0,r1,r2
921c: e8830007 stm r3, {r0, r1, r2} ;再次拷貝到r3指向也就是數組里面
9220: e24b3014 sub r3, fp, #20 ;r3 再次指向數組首地址
9224: e50b3008 str r3, [fp, #-8] ;pChar = szBuff
9228: e51b3008 ldr r3, [fp, #-8]
922c: e5d33000 ldrb r3, [r3] ;從數組取一個字節
9230: e59f0048 ldr r0, [pc, #72] ; 9280
9234: e1a01003 mov r1, r3
9238: eb000893 bl b48c <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c>
923c: e1a03000 mov r3, r0
9240: e1a00003 mov r0, r3
9244: e59f1038 ldr r1, [pc, #56] ; 9284
9248: eb000472 bl a418 <_ZNSolsEPFRSoS_E>
924c: e55b3014 ldrb r3, [fp, #-20] ; 0xffffffec
;直接從 [fp, #-20] 處取的一個字節放到r3里面
9250: e59f0028 ldr r0, [pc, #40] ; 9280
9254: e1a01003 mov r1, r3
9258: eb00088b bl b48c <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c>
925c: e1a03000 mov r3, r0
9260: e1a00003 mov r0, r3
9264: e59f1018 ldr r1, [pc, #24] ; 9284
9268: eb00046a bl a418 <_ZNSolsEPFRSoS_E>
926c: e3a03000 mov r3, #0
9270: e1a00003 mov r0, r3
9274: e24bd004 sub sp, fp, #4
9278: e8bd8800 pop {fp, pc}
927c: 000c8adc ldrdeq r8, [ip], -ip
9280: 000f7334 andeq r7, pc, r4, lsr r3 ;
9284: 0000aef8 strdeq sl, [r0], -r8
??上述代碼中分別使用了指針尋址和下標尋址兩種方式對字符數組szBuff
進行了訪 問。從這兩種訪問方式的代碼實現上來看,指針尋址方式要經過2次尋址才能得到目標數 據,而下標尋址方式只需要1次尋址就可以得到目標數據。因此,指針尋址比下標尋址多一次尋址操作,效率自然要低。
??雖然使用指針尋址方式需要經過2次間接訪問,效串要比下標尋址方式低,但其靈活性更強,可修改指針中保存的地址數據,訪問其他內存中的數據,而數組下標在沒有越界使用 的情況下只能訪問數組內的數據。
??在以下標方式尋址時,如何才能準確定位到數組中數據所在的地址呢?由于數組內的數據是連續排列的,而且數據類型又一致,所以只需要數組首地址、數組元素的類型和下標值,就可以求出數組某下標元素的地址。假設首地址為aryAddr,數組元素的類型為type, 元素個數為M,下標為n,要求數組中某下標元素的地址,其尋址公式如下:
type Ary[M];
&Ary[n] == (type *)((int)aryAddr + sizeof(type)*n);
容易理解的寫法如下(注意這里是整型加法,不是地址加法):
ary[n]的地址 = ary 的首地址 + sizeof(type)*n
??由于數組的首地址是數組中第一個元素的地址,因此下標值從0開始。首地址加偏移最 0自然就得到了第一個數組元素的首地址。
??下標尋址方式中的下標值可以使用三種類型來表示:整型常量、整型變量、計算結果為 整型的表達式。接下來我們以數組int nAry[5] = {1, 2, 3, 4, 5};
為例來具體講解一下這三種以不同方式作為下標值的尋址。
下標值為整型常量的尋址
??在下標值為常量的情況下,由于類型大小為已知數,編譯器可以直接計算出數據所在的 地址。其尋址過程和局部變置相同,分析過程如下:
int nArry[5] = {1, 2, 3, 4, 5};
000091fc
:
91fc: e92d0810 push {r4, fp}
9200: e28db004 add fp, sp, #4
9204: e24dd018 sub sp, sp, #24
9208: e59f3028 ldr r3, [pc, #40] ; 9238
;r3指向數組 {1, 2, 3, 4, 5}
920c: e24bc018 sub ip, fp, #24 ;ip 指向數組首地址
9210: e1a04003 mov r4, r3 ;r4 指向元素
9214: e8b4000f ldm r4!, {r0, r1, r2, r3} ;利用寄存器作為拷貝
9218: e8ac000f stmia ip!, {r0, r1, r2, r3} ;拷貝到數組里面
921c: e5943000 ldr r3, [r4] ;拷貝最后一個 int
9220: e58c3000 str r3, [ip]
... ...
9238: 000c8a8c andeq r8, ip, ip, lsl #21
下標值為整型變量的尋址
??當下標值為變量時,編譯器無法計算出對應的地址,只能先進行地址偏移計算,然后得出目標數據所在的地址。
int main()
{
int index = 3;
int nArry[5] = {1, 2, 3, 4, 5};
cout << nArry[index] << endl;
}
對應的反匯編如下:
000091fc
:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd018 sub sp, sp, #24
9208: e3a03003 mov r3, #3
920c: e50b3008 str r3, [fp, #-8] ;fp-8 對應著index
9210: e59f305c ldr r3, [pc, #92] ;9274
9214: e24bc01c sub ip, fp, #28 ;fp-28 對應著nArry首地址
9218: e1a0e003 mov lr, r3
921c: e8be000f ldm lr!, {r0, r1, r2, r3} ;這兩條不用說就是
9220: e8ac000f stmia ip!, {r0, r1, r2, r3} ;數組初始化
9224: e59e3000 ldr r3, [lr]
9228: e58c3000 str r3, [ip]
922c: e51b2008 ldr r2, [fp, #-8] ;r2 = 3 下標值
9230: e3e03017 mvn r3, #23 ;這個表示 -24,內存中存放補碼
9234: e1a02102 lsl r2, r2, #2 ;r2 = r2*4 = 12
9238: e24b1004 sub r1, fp, #4 ;r1 = fp - 4
923c: e0812002 add r2, r1, r2 ;r2 = fp - 4 + 12
9240: e0823003 add r3, r2, r3 ;r3 = r2 + (-24) = fp -16
9244: e5933000 ldr r3, [r3] ;為什么這樣計算,感覺怪怪的
;數組首元素對應 fp-28 ,數組下標3就對應著fp - 28 + siezeof(int)*3 = fp - 16
;這里為什么不先求出fp - 28 然后在加上12,而是要弄上面一些奇怪的計算??編譯器行為讓人不解
9248: e59f0028 ldr r0, [pc, #40] ; 9278
924c: e1a01003 mov r1, r3
9250: eb000972 bl b820 <_ZNSolsEi>
9254: e1a03000 mov r3, r0
9258: e1a00003 mov r0, r3
925c: e59f1018 ldr r1, [pc, #24] ; 927c
9260: eb00046a bl a410 <_ZNSolsEPFRSoS_E>
9264: e3a03000 mov r3, #0
9268: e1a00003 mov r0, r3
926c: e24bd004 sub sp, fp, #4
9270: e8bd8800 pop {fp, pc}
9274: 000c8acc andeq r8, ip, ip, asr #21
9278: 000f732c andeq r7, pc, ip, lsr #6
927c: 0000aef0 strdeq sl, [r0], -r0
下標值為整型表達式的尋址
??當下標值為表達式時,會先計算出表達式的結果,然后將其結果作為下標值,這里把上面示例代碼稍作修改:
int main()
{
int index = 2;
int nArry[5] = {1, 2, 3, 4, 5};
cout << nArry[index*2] << endl;
}
觀察其反匯編代碼:
000091fc
:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd018 sub sp, sp, #24
9208: e3a03002 mov r3, #2
920c: e50b3008 str r3, [fp, #-8]
9210: e59f3060 ldr r3, [pc, #96] ; 9278
9214: e24bc01c sub ip, fp, #28
9218: e1a0e003 mov lr, r3
921c: e8be000f ldm lr!, {r0, r1, r2, r3}
9220: e8ac000f stmia ip!, {r0, r1, r2, r3}
9224: e59e3000 ldr r3, [lr]
9228: e58c3000 str r3, [ip]
922c: e51b3008 ldr r3, [fp, #-8] ;這里r3 = index
9230: e1a02083 lsl r2, r3, #1 ;r2=index * 2
9234: e3e03017 mvn r3, #23
9238: e1a02102 lsl r2, r2, #2 ;r2 = r2 * 4,因為int占4字節
923c: e24b1004 sub r1, fp, #4
9240: e0812002 add r2, r1, r2
9244: e0823003 add r3, r2, r3
9248: e5933000 ldr r3, [r3]
924c: e59f0028 ldr r0, [pc, #40] ; 927c
9250: e1a01003 mov r1, r3
9254: eb000972 bl b824 <_ZNSolsEi>
數組越界
??普通的編譯器一般都不會對數組的下標進行訪問檢査,使用數組時很容易導致越界訪問的錯誤。當下標值小干0或大于數組下標最大值時,就會訪問到數組鄰近定義的數據,造成越界訪問,進而導致程序崩潰,或者產生更為嚴重的其他隱患,如下代碼所示。
int main()
{
int index = 0x256;
int nArry[5] = {1, 2, 3, 4, 5};
cout << nArry[5] << endl;
}
對應的反匯編:
000091fc
:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd018 sub sp, sp, #24
9208: e59f304c ldr r3, [pc, #76] ; 925c
920c: e50b3008 str r3, [fp, #-8]
9210: e59f3048 ldr r3, [pc, #72] ; 9260
9214: e24bc01c sub ip, fp, #28
9218: e1a0e003 mov lr, r3
921c: e8be000f ldm lr!, {r0, r1, r2, r3}
9220: e8ac000f stmia ip!, {r0, r1, r2, r3}
9224: e59e3000 ldr r3, [lr]
9228: e58c3000 str r3, [ip]
922c: e51b3008 ldr r3, [fp, #-8]
9230: e59f002c ldr r0, [pc, #44] ; 9264
9234: e1a01003 mov r1, r3
9238: eb000973 bl b80c <_ZNSolsEi>
923c: e1a03000 mov r3, r0
9240: e1a00003 mov r0, r3
9244: e59f101c ldr r1, [pc, #28] ; 9268
9248: eb00046b bl a3fc <_ZNSolsEPFRSoS_E>
924c: e3a03000 mov r3, #0
9250: e1a00003 mov r0, r3
9254: e24bd004 sub sp, fp, #4
9258: e8bd8800 pop {fp, pc}
925c: 00000256 andeq r0, r0, r6, asr r2
9260: 000c8abc ; instruction: 0x000c8abc
9264: 000f731c andeq r7, pc, ip, lsl r3 ;
9268: 0000aedc ldrdeq sl, [r0], -ip
運行結果如下:

上面代碼使用了越界數值作為下標值。將數組下標尋址 nArry[5] 帶入尋址公式中為
nArry[5] = nArry[0] + 5 * 4 = fp - 28 + 20 = fp - 8
??恰好對應著 index 的地址,另外經過實驗驗證,不管變量放在數組定義之前,還是之后都無法改變變量在棧中的位置。
??下標尋址方式也可以被指針尋址方式所代替,但指針尋址方式需要兩次間接訪問才能訪問到數組內的元素,第一次是訪問指針變量,第二次才能訪問到數組元素,故指針尋址的執行效率不會高于下標尋址,但是在使用的過程中更加方便。
??數組下標和指針的尋址如此相似,如何在反匯編代碼中區分它們呢?只要抓住一點即 可,那就是指針尋址需要兩次以上間接訪問才可以得到數據。因此,在出現了兩次間接訪問的反匯編代碼中,如果第一次間接訪問得到的值作為地址,則必然存在指針。
??數組下標尋址的識別相對復雜,下標為常量時,由于數組的元素長度固定,sizeof(type)*n 也為常量,產生了常量折疊,編譯前可直接算出偏移量,因此只需使用數組首地址作為基址加偏移即可尋址相關數據,不會出現二次尋址現象。當下標為變量或者變量表達式時,會明顯體現出數組的尋址公式,且發生兩次內存訪問,但是和指針尋址明顯不同。第一次訪問的 是下標,這個值一般不會作為地址使用,且代入公式計算后才得到地址。
多維數組
??前面講述了一維數組的各種展示形態,而超過一維的多維數組在內存中如何存儲呢? 內存中數據是線性排列的。多維數組看上去像是在內存使用了多塊空間來存儲數據,事實是 這樣的嗎?編譯器采用了非常簡單有效的手法,將多維數組通過轉化重新變為一維數組。在這里多維數組的講解以二維數組為例,如二維整型數組:int nArray[2][2]
,經過轉換后可用一維數組表示為:int nArray[4]
,它們在內存中的存儲方式也相同:

這里直接引用 C++反匯編與逆向分析技術揭秘
圖片。
??兩者在內存中的排列相同,可見在內存中根本就沒有多維數組。二維數組甚至多維數組的出現只是為了方便開發者計算偏移地址、尋址數組數據。
??二維數組的大小計算非常簡單,一維數組使用類型大小乘以下標值,得到一維數組占用 內存大小。二維數組中的二維下標值為一維數組個數,因此只要將二維下標值乘以一維數組占用內存大小,即可得知二維數組的大小。
??求得二維數組的大小后,它的地址偏移如何計算呢?根據之前的學習,我們知道一維數組的尋址根據數組首地址+類型大小*下標值
。計算二維數組的地址偏移要先分析二維數組的組成部分,如整型二維數組int nArray[2][3]
可拆分為三部分:
數組首地址: nArray
一維元素類型:int[3],此下標值記作 j
????類型:int
????元素個數: [3]
一維元素個數:[2],此下標值記作 i
??此二維數組的組成可理解為兩個一維整型數組的集合,而這兩個一維數組又各自擁有三個整型數據。在地址偏移的計算過程中,先計算出所在的一維整型的偏移量。并以此地址作為基地址,加上此元素在所在的一維數組中的地址偏移,尋址到二維數組中某個數據地址。比如說對于nArray[i][j]
其所在的數組為nArray[5][8]尋址公式為:
二維數組首地址 + sizeof (type)*二維下標值(8) * i + sizeof (type) * j
??對于一個二維數組比如說nArray[5][8],可以將他看做一個5行8列的矩陣,尋址的時候總是從左到右一行一行的尋址。
看雪的 C++反匯編與逆向分析技術揭秘
關于這里敘述比較拗口,而且按照它的說法 導致行列交換無法正確尋址。 這里只是簡單的按照最自然的想法來,經過驗證在x86,arm下都遵循這個方式。
??將理論與實踐結合,分析如下代碼,進一步加強對多維數組的學習、理解。
int main()
{
// 二維數組與一維數組尋址比較
int i = 0;
int j = 0;
int nArray[4] = {1, 2, 3, 4};
int nTwoArray[2][2] = {{1, 2},{3, 4}};
cin >> i >> j;
cout << nArray[i] << endl;
cout << nTwoArray[i][j] << endl;
}
對應的反匯編分析:
000091fc
:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd028 sub sp, sp, #40 ; 0x28
9208: e3a03000 mov r3, #0
920c: e50b3008 str r3, [fp, #-8] ;初始化 i = 0
9210: e3a03000 mov r3, #0
9214: e50b300c str r3, [fp, #-12] ;初始化 j = 0
9218: e59f30c8 ldr r3, [pc, #200] ; 92e8
921c: e24bc01c sub ip, fp, #28 ;fp - 28 對應 nArray
9220: e893000f ldm r3, {r0, r1, r2, r3} ;nArray 初始化
9224: e88c000f stm ip, {r0, r1, r2, r3}
9228: e59f30bc ldr r3, [pc, #188] ; 92ec
922c: e24bc02c sub ip, fp, #44 ; 0x2c-- fp - 44對應 nTwoArray
9230: e893000f ldm r3, {r0, r1, r2, r3} ; nTwoArray初始化
9234: e88c000f stm ip, {r0, r1, r2, r3}
9238: e24b3008 sub r3, fp, #8
923c: e59f00ac ldr r0, [pc, #172] ; 92f0
9240: e1a01003 mov r1, r3
9244: eb006aad bl 23d00 <_ZNSirsERi>
9248: e1a02000 mov r2, r0
924c: e24b300c sub r3, fp, #12
9250: e1a00002 mov r0, r2
9254: e1a01003 mov r1, r3
9258: eb006aa8 bl 23d00 <_ZNSirsERi>
925c: e51b2008 ldr r2, [fp, #-8] ;從這里才能看出fp-8對應局部變量 i
9260: e3e03017 mvn r3, #23 ;r3 = -24
9264: e1a02102 lsl r2, r2, #2 ;r2 *= 4
9268: e24b1004 sub r1, fp, #4 ;r1 = fp -4
926c: e0812002 add r2, r1, r2 ;r2 = fp - 4 + i*4
9270: e0823003 add r3, r2, r3 ;r3 = fp - 4 + i*4 + (-24)
;r3 = fp - 28 + i * 4 ,對應一維數組尋址
9274: e5933000 ldr r3, [r3]
9278: e59f0074 ldr r0, [pc, #116] ; 92f4
927c: e1a01003 mov r1, r3
9280: eb000985 bl b89c <_ZNSolsEi>
9284: e1a03000 mov r3, r0
9288: e1a00003 mov r0, r3
928c: e59f1064 ldr r1, [pc, #100] ; 92f8
9290: eb00047d bl a48c <_ZNSolsEPFRSoS_E>
9294: e51b2008 ldr r2, [fp, #-8] ;r2 = i
9298: e51b300c ldr r3, [fp, #-12] ;r3 = j
929c: e1a02082 lsl r2, r2, #1 ;r2 *= 2 ,r2=2*i
92a0: e0822003 add r2, r2, r3 ;r2 = r2 + r3 = 2*i + j
92a4: e3e03027 mvn r3, #39 ; 0x27 ;r3 = -40
92a8: e1a02102 lsl r2, r2, #2 ;r2 = r2 * 4 = 4*(2*i + j)
92ac: e24b1004 sub r1, fp, #4 ;r1 = fp -4
92b0: e0812002 add r2, r1, r2 ;r2 = fp - 4 + (2*i+j)*4
92b4: e0823003 add r3, r2, r3 ;r3 = fp - 4 + (2*i+j)*4 - 40
;而對于 nTwoArray 來說首地址為 fp - 44
;所以 r3 = nTwoArray + (2*i + j)*4 ,也就是nTwoArray[i][j]
;與上面分析的那個求址公式計算結果一致
92b8: e5933000 ldr r3, [r3]
92bc: e59f0030 ldr r0, [pc, #48] ; 92f4
92c0: e1a01003 mov r1, r3
92c4: eb000974 bl b89c <_ZNSolsEi>
92c8: e1a03000 mov r3, r0
92cc: e1a00003 mov r0, r3
92d0: e59f1020 ldr r1, [pc, #32] ; 92f8
92d4: eb00046c bl a48c <_ZNSolsEPFRSoS_E>
92d8: e3a03000 mov r3, #0
92dc: e1a00003 mov r0, r3
92e0: e24bd004 sub sp, fp, #4
92e4: e8bd8800 pop {fp, pc}
92e8: 000c8b4c andeq r8, ip, ip, asr #22
92ec: 000c8b5c andeq r8, ip, ip, asr fp
92f0: 000f7448 andeq r7, pc, r8, asr #8
92f4: 000f73bc ; instruction: 0x000f73bc
92f8: 0000af6c andeq sl, r0, ip, ror #30
??上述代碼演示了一維數組與二維數組的尋址方式,二維數組的尋址過程比一維數組多一步操作,先取得二維數組中某個一維數組的首地址,再利用此地址作為基址尋址到一 維數組中某個數據地址處。
??在上述代碼的二維數組尋址過程中,兩下標值都是未知變量,若其中某一下標值為常量,則不會出現二次尋址計算。二維數組尋址轉換成匯編后的代碼和一維數組相似。由于下標值為常量,且類型大小可預先計算出,因此變成兩常量計算,利用常量折疊可直接計算 出偏移地址:
int main()
{
// 二維數組與一維數組尋址比較
int i = 0;
int nTwoArray[2][2] = {{1, 2},{3, 4}};
cin >> i;
cout << nTwoArray[1][i] << endl;
}
對應的反匯編講解:
000091fc
:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd018 sub sp, sp, #24
9208: e3a03000 mov r3, #0
920c: e50b3008 str r3, [fp, #-8] ;i = 0
9210: e59f3064 ldr r3, [pc, #100] ; 927c
9214: e24bc018 sub ip, fp, #24 ;ip = nTwoArray
9218: e893000f ldm r3, {r0, r1, r2, r3} ;數組初始化
921c: e88c000f stm ip, {r0, r1, r2, r3}
9220: e24b3008 sub r3, fp, #8 ;r3 = &i
9224: e59f0054 ldr r0, [pc, #84] ; 9280
9228: e1a01003 mov r1, r3
922c: eb006a97 bl 23c90 <_ZNSirsERi> ;輸入到 i
9230: e51b3008 ldr r3, [fp, #-8] ;r3 = i
9234: e2832002 add r2, r3, #2 ;r2 = i + 2,直接計算2
9238: e3e03013 mvn r3, #19 ;r3 = -20
923c: e1a02102 lsl r2, r2, #2 ;r2 = r2 * 4 = (i+2)*4
9240: e24b1004 sub r1, fp, #4 ;r1 = fp - 4
9244: e0812002 add r2, r1, r2 ;r2 = fp - 4 + (i+2)*4
9248: e0823003 add r3, r2, r3 ;r3 = fp - 24 + (4*i+8)
924c: e5933000 ldr r3, [r3]
9250: e59f002c ldr r0, [pc, #44] ; 9284
9254: e1a01003 mov r1, r3
9258: eb000973 bl b82c <_ZNSolsEi>
925c: e1a03000 mov r3, r0
9260: e1a00003 mov r0, r3
9264: e59f101c ldr r1, [pc, #28] ; 9288
9268: eb00046b bl a41c <_ZNSolsEPFRSoS_E>
926c: e3a03000 mov r3, #0
9270: e1a00003 mov r0, r3
9274: e24bd004 sub sp, fp, #4
9278: e8bd8800 pop {fp, pc}
927c: 000c8adc ldrdeq r8, [ip], -ip
9280: 000f73c8 andeq r7, pc, r8, asr #7
9284: 000f733c andeq r7, pc, ip, lsr r3 ;
9288: 0000aefc strdeq sl, [r0], -ip
注意到上面的指令 0x9234,直接加上立即數2,因為 nTwoArray[1][i]
已經限定了在nTwoArray[2][2]
矩陣的第二行,而每一行恰好有2個元素。所以直接加上一個2。
存放指針類型數據的數組
??顧名思義,存放指針類型數據的數組就是數組中各數據元素都是由相同類型的指針組 成,我們稱之為指針數組,其語法為
組成部分1 組成部分2 組成部分3
類型名* 數組名稱 元素個數
??指針數組主要用于管理同種類型的指針,一般用于處理若千個字符串(如二維字符數組)的操作。使用指針數組處理多字符串數據更加方便、簡潔、高效。
??掌握了如何識別數組后,識別指針數組就會相對簡單。既然都是數組,必然遵循數組所擁有的相關特性。但是指針數組中的數據為地址類型,需要再次進行間接訪問獲取數據。下面通過代碼來分析指針數組與普通類型數組的區別。
int main()
{
// 指針數組
char * pBuff[3] = {
"Hello ",
"World ",
"!\r\n"
};
for (int i = 0; i < 3; i++) {
cout << pBuff[i] << endl;
}
}
對應的反匯編講解:
000091fc
:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd010 sub sp, sp, #16
9208: e59f2074 ldr r2, [pc, #116] ; 9284
920c: e24b3014 sub r3, fp, #20 ;r3 為 指針數組首地址
9210: e8920007 ldm r2, {r0, r1, r2} ;初始化指針數組,每個成員都是
9214: e8830007 stm r3, {r0, r1, r2} ;字符串的首地址
9218: e3a03000 mov r3, #0 ;r3 = 0
921c: e50b3008 str r3, [fp, #-8] ;i = 0
9220: ea000010 b 9268
9224: e51b2008 ldr r2, [fp, #-8] ;r2 = i
9228: e3e0300f mvn r3, #15 ;r3 = -16
922c: e1a02102 lsl r2, r2, #2 ;r2 = i * 4
9230: e24b1004 sub r1, fp, #4 ;r1 = fp -4
9234: e0812002 add r2, r1, r2 ;r2 = fp -4 + 4 * i
9238: e0823003 add r3, r2, r3 ;r3 = fp - 20 + 4 * i
;此時r3也就是指針數組下標為i的成員的地址
923c: e5933000 ldr r3, [r3] ;r3為指針數組一個成員值,指向字符串首址
9240: e59f0040 ldr r0, [pc, #64] ; 9288
9244: e1a01003 mov r1, r3
9248: eb0008ac bl b500 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
924c: e1a03000 mov r3, r0
9250: e1a00003 mov r0, r3
9254: e59f1030 ldr r1, [pc, #48] ; 928c
9258: eb000470 bl a420 <_ZNSolsEPFRSoS_E>
925c: e51b3008 ldr r3, [fp, #-8]
9260: e2833001 add r3, r3, #1 ;i ++
9264: e50b3008 str r3, [fp, #-8]
9268: e51b3008 ldr r3, [fp, #-8]
926c: e3530002 cmp r3, #2 ;i < 3 ,也就是 i <= 2
9270: daffffeb ble 9224
9274: e3a03000 mov r3, #0
9278: e1a00003 mov r0, r3
927c: e24bd004 sub sp, fp, #4
9280: e8bd8800 pop {fp, pc}
9284: 000c8af0 strdeq r8, [ip], -r0
9288: 000f734c andeq r7, pc, ip, asr #6
928c: 0000af00 andeq sl, r0, r0, lsl #30
上述代碼中定義了字符串數組,該數組由3個指針變量組成,故長度為12字節。 該數組所指向的字符串長度和數組本身沒有關系,而二維字符數組則與之不同。 指針數組用二維數組表示如下: