- 数组在函数内
- 数组作为参数
- 数组作为返回值
- 下标寻址和指针寻址
- 下标值为整型常量的寻址
- 下标值为整型变量的寻址
- 下标值为整型表达式的寻址
- 数组越界
- 多维数组
- 存放指针类型数据的数组
- 指向数组的指针变量
- 函数指针
??虽然数组和指针都是针对地址操作,但它们有许多不同之处。数组是相同数据类型的数 据集合,以线性方式连续存储在内存中;而指针只是一个保存地址值的4字节变量。在使用中,数组名是一个地址常量值,保存数组首元素地址不可修改,只能以此为基地址访问内 存数据;而指针却是一个变量,只要修改指针中所保存的地址数据,就可以随意访问,不受约束。本章将深入介绍数组的构成以及两种寻址方式。
数组在函数内
??当在函数内定义数组时,如果无其他声明,该数组即为局部变量,拥有局部变量的所有特性。数组中的数据在内存中的存储是线性连续的,其数据排列顺序由低地址到高地址,数组名称表示该数组的首地址,如:
int nArray[5] = {l, 2, 3,4, 5};
??此数组为5个int类型数据的集合,其占用的内存空间大小为sizeof(数据类型)*数组 中元素个数
,即4*5=20字节。如果数组nArray第一项所在地址为0X0012FF00,那么第二项所在地址为OX0012FF04,其寻址方式与指针相同。这样看上去很像是在函 数内连续定义了 5个int类型的变量,但也不完全相同。通过下述代码分析,我们将能够找出它们之间的不同之处。
#include <iostream>
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 <main>:
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 <main+0x64>
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 <main>:
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 <main+0x30>
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(栈空间)数组首地址。然后后面2条指令是利用r0,r1,r2寄存器作为中转把Hello World
字串拷贝到azHello的栈空间中。
??在上面代码中,字符串长度为12字节,即4的倍数。当字符串的长度不为4的倍数时,又如何以4字节的方式复制数据呢?通过实例来分析下:
int main()
{
char azHello[] = "Hello Worl"; //将原字符串中的字符 d,去掉
}
对应的反汇编:
000091fc <main>:
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 <main+0x40>
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 <iostream>
#include <string.h>
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 <memcpy> ; 这里把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 <main>:
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 <strcpy>:
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 <strcpy+0x8>
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 <main>:
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 <main+0x80>
;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 <main+0x84>
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 <main+0x88>
9248: eb000472 bl a418 <_ZNSolsEPFRSoS_E>
924c: e55b3014 ldrb r3, [fp, #-20] ; 0xffffffec
;直接从 [fp, #-20] 处取的一个字节放到r3里面
9250: e59f0028 ldr r0, [pc, #40] ; 9280 <main+0x84>
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 <main+0x88>
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 ; <UNPREDICTABLE>
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 <main>:
91fc: e92d0810 push {r4, fp}
9200: e28db004 add fp, sp, #4
9204: e24dd018 sub sp, sp, #24
9208: e59f3028 ldr r3, [pc, #40] ; 9238 <main+0x3c>
;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 <main>:
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 <main+0x78>
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 <main+0x7c>
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 <main+0x80>
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 <main>:
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 <main+0x7c>
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 <main+0x80>
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 <main>:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd018 sub sp, sp, #24
9208: e59f304c ldr r3, [pc, #76] ; 925c <main+0x60>
920c: e50b3008 str r3, [fp, #-8]
9210: e59f3048 ldr r3, [pc, #72] ; 9260 <main+0x64>
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 <main+0x68>
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 <main+0x6c>
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 ; <UNDEFINED> instruction: 0x000c8abc
9264: 000f731c andeq r7, pc, ip, lsl r3 ; <UNPREDICTABLE>
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 <main>:
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 <main+0xec>
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 <main+0xf0>
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 <main+0xf4>
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 <main+0xf8>
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 <main+0xfc>
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 <main+0xf8>
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 <main+0xfc>
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 ; <UNDEFINED> 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 <main>:
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 <main+0x80>
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 <main+0x84>
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 <main+0x88>
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 <main+0x8c>
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 ; <UNPREDICTABLE>
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 <main>:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd010 sub sp, sp, #16
9208: e59f2074 ldr r2, [pc, #116] ; 9284 <main+0x88>
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 <main+0x6c>
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 <main+0x8c>
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 <main+0x90>
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 <main+0x28>
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字节。 该数组所指向的字符串长度和数组本身没有关系,而二维字符数组则与之不同。 指针数组用二维数组表示如下:
int main()
{
// 二维字符数组
char cArray[3][10] = {
"Hello ",
"World ",
"!\r\n"
};
}
??同样存储着3个字符串,但指针数组中存储的是各字符串的首地址,而二维字符数组中存储着每个字符串中的字符数据。这是它们之间本质的不同。要对它们进行区分也非常简 单,分析它们的初始化过程即可。二维字符数组的初始化如下:
000091fc <main>:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd028 sub sp, sp, #40 ; 0x28
9208: e59f3084 ldr r3, [pc, #132] ; 9294 <main+0x98>
;r3指向字符串首地址
920c: e24b1028 sub r1, fp, #40 ; 0x28
;r1指向二维数组首地址
9210: e1a02003 mov r2, r3
9214: e3a0301e mov r3, #30 ; 长度
9218: e1a00001 mov r0, r1
921c: e1a01002 mov r1, r2
9220: e1a02003 mov r2, r3
9224: eb01d741 bl 7ef30 <memcpy>
9228: e3a03000 mov r3, #0
... ...
928c: e24bd004 sub sp, fp, #4
9290: e8bd8800 pop {fp, pc}
9294: 000c8aec andeq r8, ip, ip, ror #21
9298: 000f735c andeq r7, pc, ip, asr r3 ; <UNPREDICTABLE>
929c: 0000af10 andeq sl, r0, r0, lsl p
??只看上面示例C代码想当然的认为可能要调用三次memcpy,但实际上只调用了一次memcpy,而且拷贝正好30个字节。这样做除非一个前提:编译器已经根据数组定义cArray[3][10]
把内存中的三个字符串规划好布局,每个字符串占用10字节,经过调试验证了猜想。
??在二维字符数组初始化过程中,赋值的不是字符串地址,而是其中的字符数据,据此可以明显地区分它与字符指针数组。如果代码中没有初始化操作,那么就需要分析它们如何寻址数据。获取二维字符数组中的数据过程如下:
9234: e24b1028 sub r1, fp, #40 ; 0x28 二维数组首地址
9238: e51b2008 ldr r2, [fp, #-8] ;r2 = i
923c: e1a03002 mov r3, r2
9240: e1a03103 lsl r3, r3, #2 ;r3 = 4*i
9244: e0833002 add r3, r3, r2 ;r3 = 5*i
9248: e1a03083 lsl r3, r3, #1 ;r3 = 10*i
924c: e0813003 add r3, r1, r3 ;r3 = 数组首地址 + 10*i
9250: e59f0040 ldr r0, [pc, #64] ; 9298 <main+0x9c>
9254: e1a01003 mov r1, r3
9258: eb0008ac bl b510 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
925c: e1a03000 mov r3, r0
9260: e1a00003 mov r0, r3
9264: e59f1030 ldr r1, [pc, #48] ; 929c <main+0xa0>
9268: eb000470 bl a430 <_ZNSolsEPFRSoS_E>
926c: e51b3008 ldr r3, [fp, #-8]
9270: e2833001 add r3, r3, #1 ; i ++
9274: e50b3008 str r3, [fp, #-8]
9278: e51b3008 ldr r3, [fp, #-8]
927c: e3530002 cmp r3, #2
9280: daffffeb ble 9234 <main+0x38> ; i < 3 也就是 i <= 2
??虽然二维字符数组和指针数组的寻址过程非常相似,但依然有一些不同。字符指针数组寻址后,得到的是数组成员内容,而二维字符数组寻址后得到的却是数组中某个一维数组的首地址。
指向数组的指针变量
??什么是指向数组的指针呢?在学习一维数组时,已经有所接触。当指针变量保存的数据为数组的首地址,且将此地址解释为数组时,此指针变量被称为数组指针。指向数组元素的指针很简单,只要是指针变量,都可以用于寻址该类型的一维数组中各元素,得到数组中的 数据。而指向一维数组的数组指针会有些变化,指向一维数组的数组指针的定义格式如下:
组成部分1 组成部分2 组成部分3
类型名 (*指针变量名称) [一维数组大小];
??例如,对于二维字符数组 “char cArray[3][10] = {{"Hello "},{"World "},{"!\r\n"}};”
,定义指向这个数组的指针为“char(*pArray)[10] = cArray;”
,那么数组指针如何访问数组成员呢?
int main()
{
int i;
// 二维字符数组
char cArray[3][10] = {"Hello ","World ","!\r\n"};
// 数组指针
char (*pArray)[10] = cArray;
for(i = 0;i < 3;i ++){
cout << *pArray << endl;
pArray ++;
}
}
对应的反汇编代码讲解:
000091fc <main>:
91fc: e92d4800 push {fp, lr}
9200: e28db004 add fp, sp, #4
9204: e24dd028 sub sp, sp, #40 ; 0x28
9208: e59f307c ldr r3, [pc, #124] ; 928c <main+0x90>
920c: e24b102c sub r1, fp, #44 ; 0x2c r1指向二维数组首地址
9210: e1a02003 mov r2, r3 ; r2 指向字符串 首地址
9214: e3a0301e mov r3, #30
9218: e1a00001 mov r0, r1
921c: e1a01002 mov r1, r2
9220: e1a02003 mov r2, r3
9224: eb01d741 bl 7ef30 <memcpy> ; 二维数组用字符串初始化
9228: e24b302c sub r3, fp, #44 ; 0x2c
922c: e50b300c str r3, [fp, #-12] ;char (*pArray)[10] = cArray;
;上面一条指令初始化数组指针 pArray ,可以看出 被 初始化为二维数组首地址
;数组指针,就是一个指针,指向一个数组每个数组有10个成员
9230: e3a03000 mov r3, #0
9234: e50b3008 str r3, [fp, #-8] ;i = 0
9238: ea00000c b 9270 <main+0x74>
923c: e59f004c ldr r0, [pc, #76] ; 9290 <main+0x94>
9240: e51b100c ldr r1, [fp, #-12]
9244: eb0008af bl b508 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
9248: e1a03000 mov r3, r0
924c: e1a00003 mov r0, r3
9250: e59f103c ldr r1, [pc, #60] ; 9294 <main+0x98>
9254: eb000473 bl a428 <_ZNSolsEPFRSoS_E>
9258: e51b300c ldr r3, [fp, #-12] ; r3 = pArray
925c: e283300a add r3, r3, #10 ; r3 += 10
;注意因为 pArray 指向的类型占有 10 个字节(一个有10字节的数组),所以在 pArray ++
;的时候要加上 10
9260: e50b300c str r3, [fp, #-12]
9264: e51b3008 ldr r3, [fp, #-8]
9268: e2833001 add r3, r3, #1 ;i ++
926c: e50b3008 str r3, [fp, #-8]
9270: e51b3008 ldr r3, [fp, #-8]
9274: e3530002 cmp r3, #2 ; i <= 2
9278: daffffef ble 923c <main+0x40>
927c: e3a03000 mov r3, #0
9280: e1a00003 mov r0, r3
9284: e24bd004 sub sp, fp, #4
9288: e8bd8800 pop {fp, pc}
928c: 000c8aec andeq r8, ip, ip, ror #21
9290: 000f735c andeq r7, pc, ip, asr r3 ; <UNPREDICTABLE>
9294: 0000af08 andeq sl, r0, r8, lsl #30
??上述代码中的数组指针pArray保存了二维字符数组cArray首地址,当对pArray 执行加等于1操作后,指针pArray中保存的地址值增加了 10字节长。这个数值是如何计算 出来的呢?根据指针加法公式:
指针变量 += 数值 ~~~~~~~~~ 指针变量地址数据 += ( sizeof(指针类型) *数值 )
??上述代码中的数组指针pArray类型为char[10]
,求得其大小为10字节。对pArray 加1操作,实质是对pArray中保存的地址加10。加1后偏移到地址为二维字符数组cArray 中的第二个一维数组首地址,即&(cArray[1])
。
??对指向二维数组的数组指针执行取内容操作后,得到的还是一个地址值,再次执行取内容操作才能寻址到二维字符数组中的单个字符数据。看上去与二级指针相似,实际上并不一 样。二级指针的类型为指针类型,其偏移长度在32位下固定为4字节,而数组指针的类型为数组,其偏移长度随数组而定,两者的偏移计算不同,不可混为一谈。
??二级指针可用于保存一维指针数组。如对于一维指针数组char* p[3]
,可用Char**pp
来保存其数组首地址。通过对二级指针pp使用3次寻址即可得到数据。main函数的定义(main( int argc, char *argv[], char *envp[]))
中有3个参数:
argc :命令行参数个数,整型.
argv;命令行信息,保存字符串数组首地址的指针变量,是一个指向数组的指针。
envp :环境变量信息,和argv类型相同?
??参数argv与envp就是两个指针数组,当数组作为参数时,实际上以指针方式进行数 据传递。这里两个参数可转换为char**
二级指针类型,修改为:main(int argc,char **argv, char **envp)
。
??在使用数组指针的过程中,经常在定义数组指针中出现类型匹配错误。有没有什么方法可以根据多维数组的类型,快速匹配出对应的数组指针类型呢?可以通过指定数组下标达到这一目标,如三维数组int nArray[2][3][4]
;其数组指针的定义如下:
int (*pnArray)[3][4] = nArray;
??三维数组指针变量名称为*pnArray
,替换掉原三维数组中的数组名称及三维下标 nArray[2]
。数组转换数组指针的规则总结如下:
数组 数组指针
类型 数组名称[最高维数] [X][Y]…… 类型 (*数组指针名称)[X][Y]……
在定义数组指针时,为什么只有最高维数可以省去?先来看看普通的指针变量寻址过程:
??假设:整型指针变量*p
中保存的地址为0x0012FF00,对其执行加等于1操作
p += 1;
p = 0x0012FF00 + sizeof(int);
p = 0x0012FF04
??指针在地址偏移过程中需要计算出偏移量,因此需要所指向的数据类型来配合计算偏移 长度。在多维数组中,可以将最高维看做是一维数组,其后数据为这个一维数组中各元素的数据类型。例如:int nArray[3][4][5]
同 int[4][5] nArray[3]
—样,可将 int[4][5]
看做是一个整体的数据类型,记作int[4][5] * p = nArray
;。由于C++语法中没有此种语法格式,故无法使用,正确的语法格式为:int(*p)[4][5]=nArray
;,括号的使用是为了与指针数组进行区分。
??虽然指针与数组间的关系千变万化,错综复杂.但只要掌握了它们的寻址过程,就可通过偏移量获得其类型以及它们之间的关系。
函数指针
??既然函数是个是地址,当然就可以使用指针变量进行存储。用于保存函数首地址的指针变量被称为函数指针。
函数指针的定义很简单,和函数的定义非常相似,由四部分组成:
返回值类型 ([调用约定,可选] * 函数指针变量名称) (参数信息)
??函数指针的类型由返回值、参数信息、调用约定组成,它们决定了函数指针在函数调用过程中参数的传递、返回值信息,以及如何平衡栈顶。如何区分函数调用与函数指针的调用呢?见下述代码(据说在linux下没有 __cdecl
语法关键字 ):
#include <iostream>
#include <string.h>
using namespace std;
void Show()
{
cout << "Show " << endl;
}
int main()
{
void (*pShow)(void) = Show;
pShow();
Show();
}
对应的反汇编如下:
000091fc <_Z4Showv>:
91fc: e92d4800 push {fp, lr}
... ...
9228: 000c8aac andeq r8, ip, ip, lsr #21
922c: 0000aed4 ldrdeq sl, [r0], -r4
00009230 <main>:
9230: e92d4800 push {fp, lr}
9234: e28db004 add fp, sp, #4
9238: e24dd008 sub sp, sp, #8
923c: e59f301c ldr r3, [pc, #28] ; 9260 <main+0x30>
; 加载函数地址到r3
9240: e50b3008 str r3, [fp, #-8] ;局部变量,对应着函数指针
9244: e51b3008 ldr r3, [fp, #-8]
9248: e12fff33 blx r3 ;函数指针调用
924c: ebffffea bl 91fc <_Z4Showv> ;Show函数地址
9250: e3a03000 mov r3, #0
9254: e1a00003 mov r0, r3
9258: e24bd004 sub sp, fp, #4
925c: e8bd8800 pop {fp, pc}
9260: 000091fc strdeq r9, [r0], -ip
??上述代码演示了函数指针的赋值和调用过程。函数指针调用与函数调用的最大区別在于函数是 直接调用,而函数指针的调用需要取出指针变量中保存的地址数据,间接调用函数。
??函数指针是比较特殊的指针类型,由干其保存的地址数据为代码段内的地址信息,而非数据区,因此不存在地址偏移的情况。
??在上述代码中,函数指针类型的参数和返回值都为void类型,只可存储相同类型的函数地址,否则无法传递函数的参数,返回值,无法正确平衡栈顶。通过修改上述代码清单,分析带参数与返回信息的函数指针类型,如下所示:
int Show(int show)
{
cout << show << endl;
return show;
}
int main()
{
int (*pShow)(int) = Show;
int nRet = pShow(5);
cout << nRet << endl;
}
对应反汇编讲解:
000091fc <_Z4Showi>:
91fc: e92d4800 push {fp, lr} ;压栈 ,保存返回值
9200: e28db004 add fp, sp, #4
... ...
9234: e8bd8800 pop {fp, pc} ;弹栈,返回
00009240 <main>:
9240: e92d4800 push {fp, lr}
9244: e28db004 add fp, sp, #4
9248: e24dd008 sub sp, sp, #8
924c: e59f3040 ldr r3, [pc, #64] ; 9294 <main+0x54>
;获取函数地址到r3
9250: e50b3008 str r3, [fp, #-8] ;保存地址到局部变量栈帧中
9254: e51b3008 ldr r3, [fp, #-8]
9258: e3a00005 mov r0, #5 ;函数参数
925c: e12fff33 blx r3 ;通过指针调用
9260: e1a03000 mov r3, r0 ;函数返回值保存在 r0
9264: e50b300c str r3, [fp, #-12]
9268: e59f0028 ldr r0, [pc, #40] ; 9298 <main+0x58>
926c: e51b100c ldr r1, [fp, #-12]
9270: eb000972 bl b840 <_ZNSolsEi>
9274: e1a03000 mov r3, r0
9278: e1a00003 mov r0, r3
927c: e59f1018 ldr r1, [pc, #24] ; 929c <main+0x5c>
9280: eb00046a bl a430 <_ZNSolsEPFRSoS_E>
9284: e3a03000 mov r3, #0
9288: e1a00003 mov r0, r3
928c: e24bd004 sub sp, fp, #4
9290: e8bd8800 pop {fp, pc}
9294: 000091fc strdeq r9, [r0], -ip
9298: 000f734c andeq r7, pc, ip, asr #6
929c: 0000af10 andeq sl, r0, r0, lsl pc
??上述代码中的函数指针调用只是多了参数的传递、返回值的接收,和前面代码中的函数指针并无实质区别。它们有着共同特征——都是间接调用函数,这是识别函数指针的关键点。