基于arm的C++反汇编 数组和指针的寻址

  • 数组在函数内
  • 数组作为参数
  • 数组作为返回值
  • 下标寻址和指针寻址
    • 下标值为整型常量的寻址
    • 下标值为整型变量的寻址
    • 下标值为整型表达式的寻址
    • 数组越界
  • 多维数组
  • 存放指针类型数据的数组
  • 指向数组的指针变量
  • 函数指针

??虽然数组和指针都是针对地址操作,但它们有许多不同之处。数组是相同数据类型的数 据集合,以线性方式连续存储在内存中;而指针只是一个保存地址值的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

??上述代码中的函数指针调用只是多了参数的传递、返回值的接收,和前面代码中的函数指针并无实质区别。它们有着共同特征——都是间接调用函数,这是识别函数指针的关键点。

时间: 2024-10-08 13:17:35

基于arm的C++反汇编 数组和指针的寻址的相关文章

[转组第9天] | 数组和指针的寻址

2018-05-09 <C++反汇编和逆向技术>第八章 数组和指针的寻址 读书笔记 虽然数组和指针都是针对地址操作,但它们有许多不同之处.数组是相同数据类型的数据集合,以线性方式连续存储在内存中:而指针只是一个保存地址值的4字节变量.在使用中,数组名是一个地址常量值,保存数组首元素地址,不可修改,只能以此为基地址访问内存数据:而指针却是一个变量,只要修改指针中所保存的地址数据,就可以随意访问,不受约束. 1. 数组在函数内 对于数组的识别,应判断数据在内存中,是否连续并且类型是否一致,均符合即

基于arm的C++反汇编 函数的工作原理

栈帧的形成和关闭 各种调用方式的考擦 使用 fp或sp寻址 函数的参数 与返回值 arm指令中立即数存放位置 gdbserver 调试环境 栈帧的形成和关闭 ??栈在内存中是一块特殊的存储空同, 它的存储原则是"先进后出", 即最先被存储的数据最后被释放, 汇编过程通常使用 push 指令与 POP指令对栈空间执行数据压入和数据弹出操作. ??栈结构在内存中占用一段连续的存储空间, 通过sp与 fp这两个栈指针寄存器(在x86上是esp,ebp)来保存当前栈的起始地址与结束地址(又称为

c++ 反汇编 数组和指针

字符串初始化字符数组 58: char as[] = "hello word"; 00AC7308 A1 70 2E B6 00 mov eax,dword ptr [string "hello word" (0B62E70h)] 00AC730D 89 45 EC mov dword ptr [as],eax //复制4字节 00AC7310 8B 0D 74 2E B6 00 mov ecx,dword ptr ds:[0B62E74h] 00AC7316 89

基于ARM处理器的反汇编器软件简单设计及实现

写在前面 2012年写的,仅供参考 反汇编的目的 缺乏某些必要的说明资料的情况下, 想获得某些软件系统的源代码.设计思想及理念, 以便复制, 改造.移植和发展: 从源码上对软件的可靠性和安全性进行验证,对那些直接与CPU 相关的目标代码进行安全性分析: 涉及的主要内容 分析ARM处理器指令的特点,以及编译以后可执行的二进制文件代码的特征: 将二进制机器代码经过指令和数据分开模块的加工处理: 分解标识出指令代码和数据代码: 然后将指令代码反汇编并加工成易于阅读的汇编指令形式的文件: 下面给出个示例

《C专家编程》第四章——令人震惊的事实:数组和指针并不相同

数组和指针是C语言里相当重要的两部分内容,也是新手程序员最容易搞混的两个地方,本章我们锁定指针与数组,探讨它们的异同点. 首先来看指针与数组在声明上的区别: int a[10]; int *p; 很明显的,第一个是数组a,第二个是指针p.下一个问题是a的类型是什么?p的类型是什么?a[0]的类型是int,而a是个数组名,它是否表示整个数组呢?事实并非如此,a是一个指针常量,是一个指向int的指针常量,而p是一个指向int的指针,是一个变量.这是它们的第一个区别:一个是常量,一个是变量.那么常量和

队列的三种实现(静态数组、动态数组及指针)

本文有关栈的介绍部分参考自网站数据结构. 1. 队列  1.1 队列的定义 队列(Queue)是只允许在一端进行插入,而在另一端进行删除的运算受限的线性表. (1)允许删除的一端称为队头(Front). (2)允许插入的一端称为队尾(Rear). (3)当队列中没有元素时称为空队列. (4)队列亦称作先进先出(First In First Out)的线性表,简称为FIFO表.    队列的修改是依先进先出的原则进行的.新来的成员总是加入队尾(即不允许"加塞"),每次离开的成员总是队列头

数组和指针关系的探讨

在学习过程中,数组和指针的操作几乎完全一样,且不说传数组可以用传指针完全替代,而且指针也可以使用[]操作符来访问指针偏移后的地址,所以在实际应用中数组和指针用不出任何差别.(如下代码所示) #include<iostream> using namespace std; int main() { int a[]={1,2}; cout<<*a<<endl; int* b=a; cout<<*b<<endl; return 0; } 所以人们在实际使

数组的指针特性

从一道小题下手(该考点曾被腾讯等大公司作为基础笔试题考过), #include <stdio.h> int size(char a[10]) { return sizeof(a); } int main(void) { char a[] = {'C','h','i','n','a','\0'}; char *p = "China"; char *q = a; printf("sizeof(a)=%d\n", sizeof(a)); printf(&quo

基于ARM的指纹识别门禁系统设计方案

现代社会高速发展,很多场合需要身份确认,传统的身份识别技术已经不能满足社会要求.人的身体特征具有不可复制性,因此人们开始研究生物识别技术,而指纹具有唯一性.终生不变性.难于伪造等特点,安全性高,因而得到了广泛应用.在一些机要部门,如银行.宾馆.机房等一般都安装有门禁系统,门禁系统是为保障人们生活.工作及财产安全, 对重要通道的出入口进行管理与控制的系统,基于指纹识别技术的门禁系统是一项高科技安全设施,提高了系统的安全性.ARM作为一种嵌入式系统处理器,具有高性能.低功耗.低成本等特点,因而在工业