C++反汇编学习笔记7——数组和指针以及他们的寻址

两年前写的,欢迎大家吐槽! 

转载请注明出处。

1.      数组在函数内

先通过一个简单的例子来看一下数组和普通变量在初始化时的不同之处:

这是数组初始化:

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

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

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

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

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

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

下面是变量的初始化:

    43:  charcChar = 'A';

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

    44:  floatfFloat = 1.0f;

0042B77F fld1 

0042B781  fstp       dword ptr [ebp-34h] 

    45:  short sShort = 1;

0042B784  mov        eax,1 

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

    46:  intnInt = 2;

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

    47:   double dDouble = 2.0f;

0042B794  fld        qword ptr [__real@4000000000000000 (47DC90h)] 

0042B79A fstp        qword ptr[ebp-5Ch] 

数组中每个元素的数据类型是相同的,并且地址是连续的,通过这一点可以很容易的区别出在函数中的数组,而全局数组则在后面会讲到。Release版本的没有多大变化,和局部变量一样优化,但是数组不会因为被赋值常量而进行常量传递,代码如下:

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

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

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

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

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

现在再来看一下字符串,其实字符串就是字符数组,只是最后一个元素为‘\0’作为字符串结束标志而已。而字符串的初始化就是复制字节的过程,书上说VC++6.0编译器是通过寄存器来赋值的,因为每个寄存器拥有4字节,所以每次最多可以复制4字节的内容。但是我电脑上只有VS2010,它的编译器选择了直接将字符串常量写进文件然后将指针直接指向字符串常量装载入内存的地址。下面看这个例子使理解更加深刻一些:

    55:   char *szHello = "Hello world";

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

通过监视可以看到变量所在地址:

szHello的地址中存储的就是"Hello world"所在文件中的地址,再根据这个地址可以找到字符串:

2.      数组作为参数

首先来看个例子(Debug版本):

这是main函数里面数组定义、初始化以及作为参数调用函数的代码

    57:  charszHello[20] = {0};

0042B758  mov        byte ptr [ebp-1Ch],0  ;这个字节是数组的首地址

0042B75C xor         eax,eax 

;下面则是数组的剩余19个元素的初始化,利用eax寄存器进行赋值

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

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

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

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

;当不足4字节时编译器会做相应处理,这里的3字节被拆成2字节和1字节

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

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

    58:   Show(szHello);

0042B771  lea        eax,[ebp-1Ch] 

0042B774  push       eax  ;取出数组首地址并入栈作为参数传到函数里面

0042B775  call       Show (429FA5h) 

0042B77A add         esp,4 

下面是show函数的定义:

     9: void Show(char szBuff[])

    10: {;先前代码略

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

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

0042B673  mov        eax,dword ptr [esp+8]  ;获取参数

0042B676  push       eax  ;将其作为字符串复制函数的参数入栈

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

0042B67C add         esp,8 

    12:   printf(szBuff);

;printf函数代码略

    13: };后续代码略

从以上的例子可以很清楚的看到,数组作为函数的参数时是将数组的首地址入栈作为参数,然后根据首地址找到数组的每一个元素。

字符串处理函数在Debug版本下非常容易识别,但是在Release版本中字符串处理函数内联到程序中,没有call指令对函数进行调用,因此识别这些函数有一定的困难,但是可以根据反汇编的代码确定其内联代码的功能,并非必须还原出原函数。下面就用一个例子来具体说明:

先是C源代码:

main()

{

    charszHello[20] = {0};

    Show(szHello);

}

void Show(charszBuff[])

{

    strcpy(szBuff, "HelloWorld");

    printf(szBuff);

}

首先对main函数中的代码用IDA P ro进行反汇编得到如下代码:

.text:00401030 aHello          = byte ptr -18h ;这是一个数组,可以看到大小为0x14

.text:00401030 var_4           = dword ptr -4

.text:00401030

.text:00401030                 push    ebp

.text:00401031                 mov     ebp, esp

.text:00401033                 sub     esp, 18h

.text:00401036                 mov     eax, dword_40B014

.text:0040103B                 xor     eax, ebp

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

.text:00401040                 xor     eax, eax

;和Debug版本一样,下面是对数组的初始化

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

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

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

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

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

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

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

;取出数组首地址作为函数参数入栈

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

.text:0040105B                 push    eax

;调用函数

.text:0040105C                 call    sub_401000

;后续代码略

下面再来看看show函数的内部代码:

.text:00401000 arg_0           = dword ptr  8

.text:00401000

.text:00401000                 push    ebp

.text:00401001                 mov     ebp, esp

;这是非常关键的一步,可以看到数据段408140偏移的4字节数据复制到ecx寄存器

.text:00401003                 mov     ecx, ds:dword_408140

;接着再将数组首地址存入eax,虽然它本来就存着首地址,但是我也不知道为什么要这样

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

;可以看到这一步实现了字符串的前四个字节的赋值,打开内存发现这正是“Hell”

.text:0040100C                 mov     [eax], ecx

;与上同理,此处不再赘述

.text:0040100E                 mov     edx, ds:dword_408144

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

.text:00401017                 mov     ecx, ds:dword_408148

;到此为止,所有字符串复制完毕,下面的代码便是printf函数调用和环境恢复了

.text:0040101D                 push    eax

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

.text:00401021                 call    sub_401074

.text:00401026                 add     esp, 4

.text:00401029                 pop     ebp

.text:0040102A                 retn

由于这段代码和书上给的反汇编代码完全不同,因此几乎都是我自己分析得到的,所以我花了好几个小时才完全弄明白,从这里可以看到,按照书上的东西似乎觉得自己都懂了,但是完全凭借自己来解决问题的时候却发现非常困难。这里仅仅是一段小小的字符串复制的代码就要花如此长的时间,足以显现反汇编是多么的不容易,至少对于我这种新手来说。所以这大概就是现在完全自动化的反编译器很少或是质量不高的原因,因为编译是一个不可逆的过程,想要正确地由二进制代码转换成汇编代码再转换成高级代码是很困难的。通过这段代码的独立分析,我还要不断实践以增加自己的经验。技术的进步正是靠着不断的积累!

看完了上面的代码,再来看个简单的(Release版本strlen函数):

还是一样,贴出C源代码和反汇编代码,如下:

函数调用:

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

函数定义:

//strlen函数分析

int GetLen(charszBuff[])

{

    returnstrlen(szBuff);

}

main函数:

push   offset aHello   ;"Hello";字符串常量存储在文件中,装载到内存指定位置

call   sub_401000 ;调用GetLen函数

push   eax

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

call   sub_40103B;调用printf函数

add    esp, 0Ch

xor    eax, eax

retn

下面是GetLen函数定义的反汇编代码:

.text:00401000 arg_0           = dword ptr  8;这里就是字符串的首地址

.text:00401000

.text:00401000                 push    ebp

.text:00401001                 mov     ebp, esp

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

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

.text:00401009                 lea     esp, [esp+0];不知这条语句是何用意

.text:00401010

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

.text:00401010                 mov    cl, [eax];取出eax存储的地址中的字节

.text:00401012                 inc     eax;地址加1

.text:00401013                 test    cl, cl;相当于逻辑与,但不改变cl中的值

.text:00401015                 jnz     short loc_401010

;test与jnz共用,当cl为0时会使得标志寄存器FR中的标志位ZF=0,导致不跳转,循环结束,表示字符串中所有元素均已遍历,此时下一条语句只需将现在的地址eax减去原来的地址edx即为字符串的长度,相减后存储在eax作为返回值

.text:00401017                 sub     eax, edx

.text:00401019                 pop     ebp

.text:0040101A                 retn

总结:字符串处理函数经常内联到程序中,所以在看到相关代码时只需将其功能恢复即可。

3.      局部数组作为返回值

局部变量可以作为返回值,这是因为局部变量虽然也在函数的栈中,但是他会在函数结束之前将值存入某个寄存器中(一般是eax),但是局部数组作为返回值往往只会将他的首地址存入某个寄存器,这样一来当寄存器存储的地址内的区域里面的内容发生改变时便会造成原数据的改变。下面来举个例子方便理解(Debug版本):

main函数中的调用:

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

0042B81E  call       RetArray (42A324h) 

0042B823  push       eax 

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

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

0042B82E  add        esp,8 

RetArray函数定义:

    22:  /*局部数组作为返回值*/

    23: char* RetArray()

    24:  {;先前代码略

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

;字符串复制

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

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

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

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

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

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

    26:  returnszBuff;

0042B722  lea        eax,[ebp-14h]  ;将数组的首地址存入寄存器作为返回值

    27: };后续代码略

从上面的代码看,貌似并不会出现什么问题,在调用printf函数之前将eax的值作为参数入栈,然后将其所在的内存空间中的内容以字符串的形式输出。eax是安全的,但是以eax的值作为地址的内存空间并不安全。下面来看一下内存中的值的变化会更容易理解:

还没退出函数之前可以看到,查看其对应的内存:,可以看到里面的值就是Hello World。那么现在来看一下输出是多少,,其中的内存变成了。是的,这就是输出结果,和原来的字符串一点关系都没有,那这是什么原因呢?这里可以看到字符串是利用printf函数输出的,凡是函数在执行到时必定会有自己的栈,而程序的栈空间是每个函数都可以使用的,RetArray函数所使用的栈空间在后面又分配给了printf函数使用,此时很有可能导致字符串所在内存的不稳定,容易发生错误。因此以局部数组或是指针作为返回值是极不明智的,因为其所在区域在函数退出后不会有任何保护,非常容易造成最后的结果是错误的。

为了尽量避免上述所说的错误,可以使用全局数组、静态局部数组或上层函数中定义的局部数组来存储被调函数中需要返回的数组。下面就来简单的了解一下全局数组和静态局部数组。

全局数组的用法其实和全局变量相同。举一个简单的例子:

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

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

{

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

}

用IDA Pro反汇编出的结果如下:

.text:0042DE0E                 mov     [ebp+var_8], 0 ;变量i赋初值

.text:0042DE15                 jmp     short loc_42DE20

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

.text:0042DE17

;loc_42DE17这块代码段是在第二次及以后的循环中做i++操作

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

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

.text:0042DE1A                 add     eax, 1

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

.text:0042DE20

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

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

.text:0042DE24                 jge     short loc_42DE40  ;大于则跳出循环

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

.text:0042DE29                 mov     ecx, g_nArry[eax*4] ;利用下标寻址找到相应的数据

.text:0042DE30                 push    ecx

.text:0042DE31                 push    offset __real@4000000000000000 ;"%d"

.text:0042DE36                 call    j_printf

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

.text:0042DE3E                 jmp     short loc_42DE17

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

.text:0042DE40

.text:0042DE40loc_42DE40:                     ; CODEXREF: main+34j;这里就出了循环

再来看看全局数组:

.data:0048D000 g_nArry         dd 1, 2, 3, 4, 5        ;这里我把它压缩到了一起形成一个数组

了解完了全局数组,再来简单的看一下静态局部数组,还是看一个例子:

    83:  intnOne;

    84:  intnTwo;

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

;scanf函数代码略

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

;查看变量g_snArry的地址为0x0048D464,所以下面的[$S1 (48D460h)]代表的就是标志字节

;将标志字节存入eax中

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

0042B5C8 and         eax,1 

;这里判断静态局部数组是否曾经被初始化,若是被初始化则跳过下面的代码

0042B5CB  jne        main+70h (42B600h) 

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

0042B5D2  or         eax,1  ;若没有被初始化那将其标志位置为1并在后面的代码中将其初始化

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

0042B5DA  mov        eax,dword ptr [nOne] 

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

0042B5E2  mov        eax,dword ptr [nTwo] 

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

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

0042B5F4 xor         eax,eax 

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

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

可以看到无论静态局部数组有多少个元素,都只进行一次初始化。

4.      下标寻址和指针寻址

数组可以通过下标和指针两种方式进行寻址。下标方式较为常见,效率也更高,指针方式不方便,效率也较低,因为它在访问数组时需要先取出指针中的地址,然后再对其进行偏移得到数组元素,而数组名则是一个常量地址,直接可以进行偏移计算。还是来看例子:

    95:  char *pChar = NULL;

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

    96:  charszBuff[10] = {0};

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

0042B5C3 xor         eax,eax 

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

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

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

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

0042B5CE  lea        eax,[ebp-20h] 

0042B5D1  push       eax 

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

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

0042B5DC  add        esp,8 

    98:  pChar =szBuff;

0042B5DF  lea        eax,[ebp-20h] 

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

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

0042B5E5  mov        eax,dword ptr [ebp-0Ch]  ;取出指针变量中的地址

0042B5E8  movsx      ecx,byte ptr [eax]  ;在这个地址处取出数据

0042B5EB  push       ecx 

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

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

0042B5F6 add         esp,8 

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

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

0042B5FD  push       eax 

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

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

0042B608  add        esp,8 

从上面的代码可以看到,指针和下标寻址有着很明显的区别,指针需要进行两次寻址,而下标只需要一次,所以下标寻址的效率比较高,但是更为灵活,可以改变指针变量中的地址来访问其他内存,而数组在不越界的情况下是无法访问到数组以外的数据的。

这里再来讲讲下标值的不同类型会带来什么区别:

4.1下标值为整形常量

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

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

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

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

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

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

    65:   

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

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

;printf函数分析略

需要将数组元素作为参数时直接计算出其下标值

4.2   下标值为为整形变量

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

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

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

4.3   下标值为整形表达式

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

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

004284D4  shl        eax,1  ;这里先计算表达式的值

004284D6  mov        ecx,dword ptr [ebp+eax*4-1Ch] ;这里和变量一样

当整形表达式的计算结果可以计算出来时,编译器会选择常量折叠,将表达式用常量代替。上面的是先进行表达式计算,然后再找到相应的数组元素。

以上就是数组的三种寻址方式,这三种方式都可以运用在指针寻址中。

4.4数组下标越界

C/C++中并不会对数组的越界访问进行检查,所以在使用下标时要时刻注意不要越界访问内存中的其他数据,从而造成程序崩溃甚至更严重的后果。越界下标的使用和正常下标一样,这里不再赘述。

5.      多维数组

多维数组其实在内存中也是线性存储的,只是在程序中编译器将其做了相应的处理而已。在学习C等高级语言时都已经学习了多维数组的原理,这里就来个简单的例子巩固一下:

   103:  int i =0;

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

   104:  int j =0;

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

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

;一维数组的初始化

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

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

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

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

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

;二维数组的初始化

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

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

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

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

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

;scanf函数调用略

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

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

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

0042B61A push        ecx 

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

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

0042B625  add        esp,8 

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

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

0042B62B  lea        ecx,[ebp+eax*8-48h]  ;8为每一个一维数组的大小,这里是取得第i个一维数组的首地址

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

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

0042B635  push       eax 

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

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

0042B640  add        esp,8 

这里一维数组和二维数组有着明显的区别,一维数组直接根据偏移量取出数组中的元素,二维数组则需要先计算第i个一维数组的首地址然后根据j偏移量来得到元素。

当有一个下标为常量时:

   112:  int i =0;

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

   113:  intnTwoArray[2][2] = {{1, 2},{3, 4}};            //二维数组

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

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

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

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

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

;scanf函数代码略

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

;这里ebp-1Ch是由ebp-24h+1*8计算而来的,利用了常量折叠

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

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

0042B5F3 push        ecx 

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

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

0042B5FE  add        esp,8 

上面分析了Debug版本的一维数组和二维数组之间的区别,现在来看看经编译器优化后的数组初始化以及Release版本下的数组寻址过程:

var_2C=dword ptr -2Ch  ;j

var_28= dword ptr -28h  ;i

var_24= dword ptr -24h  ;二维数组首地址

var_20= dword ptr -20h

var_1C=dword ptr -1Ch

var_18= dword ptr -18h

var_14= dword ptr -14h  ;一维数组首地址

var_10= dword ptr -10h

var_C= dword ptr -0Ch

var_8= dword ptr -8

var_4= dword ptr -4  ;不知道这个是干嘛用的,从后面的代码分析应该是检验或是校正之类的

 

push   ebp

mov    ebp, esp

sub    esp, 2Ch

mov    eax, dword_40D014

xor    eax, ebp

mov    [ebp+var_4], eax

;以上代码不知道是干嘛的

xor    eax, eax  ;将eax置0

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

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

mov    eax, 4

mov    ecx, 3

;两个数组的最后一个元素均被置为4,其他元素初始化同理

mov    [ebp+var_8], eax

mov    [ebp+var_18], eax

lea    eax, [ebp+var_2C] ;取j作为scanf的参数

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

mov    [ebp+var_1C], ecx

push   eax

lea    ecx, [ebp+var_28];同上

mov    edx, 2

push   ecx

push   offset aDD      ; "%d%d"

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

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

mov    [ebp+var_24], 1

mov     [ebp+var_20], edx

;到此为止两个数组的所有成员均被初始化

call   sub_4011D0;调用scanf函数

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

mov    eax, [ebp+edx*4+var_14] ;得到一维数组相应元素

push   eax

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

call   sub_401096 ;调用printf函数

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

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

lea    eax, [ecx+edx*2]   ;这里并不是取第i个一维数组的首地址,而是计算出当这个元素相对于数组首地址的偏移个数,即把他转化之后相当于就是一个一维数组而不是二维数组

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

push   ecx

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

call   sub_401096

下面的代码应该就是用到了var_4变量,但是我不知道它的实际用途

mov    ecx, [ebp+ var_4]

xor    ecx, ebp

add    esp, 1Ch

xor    eax, eax

call   sub_4011ED

mov    esp, ebp

pop    ebp

retn

正如上面的代码所表示的,内存给局部变量分配空间是从大到小分配的,不仅如此,在O2情况下,当一维数组和二维数组初始化的内容相同时便会一起进行初始化,并且编译器不将所有的初始化语句都放在一起是为了减少指令之间的相关性,使得指令能够像流水线那样执行,提高程序的效率。

其他多维数组的分析和二维数组相同,都是逐渐降维最后转化成低维数组进行处理,这里不再赘述。

6.      指针数组

指针数组就是存放同一类型指针的数组,数组的每个元素都存放着地址,一般用于处理若干个字符串的操作(如二维字符数组)。它和数组的区别就在于在取出数组中的元素之后还要进行一次间接寻址。下面就用一个例子来加深理解。

   126:      char * pBuff[3] = {

   127:          "Hello ",

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

;将字符串的首地址存入数组中,下同

   128:          "World ",

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

   129:          "!\r\n"

   130: };

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

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

;for循环代码略

   132:          printf(pBuff[i]);

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

0042843E  mov        ecx,dword ptr pBuff[eax*4]  ;找到相应的数组中的地址元素并放到ecx中

00428442  push       ecx 

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

00428448  add        esp,4 

   133: }

指针数组和二维字符数组有着很明显的区别,若是在初始化阶段,二维字符数组是将每个字符数据赋值,而指针数组仅仅是将字符串的首地址存入数组中。

7.      数组指针

这是指向数组的一个指针变量,里面存储的是数组的首地址。看一个简单的例子:

    charcArray[3][10] = {

        "Hello ",

        "World ",

        "!\r\n"

    };

;二维字符数组初始化略

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

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

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

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

;for循环代码略

   148:    {

   149:        printf(*pArray);

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

00428494  push       eax 

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

0042849A  add         esp,4 

   150:        pArray++;

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

004284A0  add         eax,0Ah 

;这里每次pArray+1都是使地址+10,这是因为pArray的指针类型是长度为10的字符数组

004284A3  mov         dword ptr [ebp-34h],eax  ;将计算过后的地址重新放入pArray指针变量中

   151:    }

指针地址的运算公式:指针变量的地址 += ( sizeof(指针类型) * 数值 ),这里的指针类型为char[10],所以每次指针变量+1时就相当于变量中的地址+10。

下面再来看一下二级指针以及他和指向二维字符数组的指针的区别。二级指针由于其指针的类型是指针类型,所以每次+1都只会在地址上+4,而指向二维字符数组的指针的类型是数组,所以需要根据数组的大小来决定加多少。下面来看一个输出main函数参数的例子:

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

   153: {

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

;for循环代码略

   155:    {

   156:        printf(argv[i]);

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

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

0042842E  mov        edx,dword ptr [ecx+eax*4]  ;获得第i个argv参数的首地址

00428431  push       edx 

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

00428437  add        esp,4 

   157:    }

158:}

从上面可以看到argv是二级指针,取出里面的元素时必须要进行两次取址

8.      函数指针

call指令会跳转到函数的首地址处然后执行函数内部的代码,这样它也可以使用指针来调用。还是看一个简单的例子:

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

;函数名就是函数的首地址,是一个常量

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

   163:

   164:     pShow();

00428415  mov        esi,esp 

00428417  call       dword ptr [pShow]  ;通过指针间接调用函数

;下面两句都是栈平衡检查,Debug版本所特有

0042841A  cmp         esi,esp 

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

   165:     Show();

00428421  call       Show (427027h)  ;直接函数调用

函数指针是比较特殊的指针,它保存的是代码段而非数据段的地址,所以不存在地址偏移的情况,编译器会在编译阶段对函数指针进行检查以防止其进行加减等没有意义的运算。

上面的函数的返回值和参数均为void,下面再来看一个有返回值和参数的函数指针:

   167:    int(__stdcall *pShow)(int) = Show; ;在定义指针时就定义返回值和参数

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

   168:    intnRet = pShow(5);

00428415  mov        esi,esp 

00428417  push       5 

00428419  call       dword ptr [pShow]  ;间接调用函数

;下面是栈平衡检查

0042841C  cmp         esi,esp 

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

00428423  mov        dword ptr [nRet],eax 

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

00428426  mov        eax,dword ptr [nRet] 

00428429  push       eax 

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

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

00428434  add        esp,8 

一个函数指针只能存储同一种类型的函数的地址,包括函数的参数和返回值,否则无法传递参数和返回值,也无法进行栈平衡检查。

 

你可能感兴趣的:(C++逆向)