先通过一个简单的例子来看一下数组和普通变量在初始化时的不同之处:
这是数组初始化:
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"所在文件中的地址,再根据这个地址可以找到字符串:
首先来看个例子(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
总结:字符串处理函数经常内联到程序中,所以在看到相关代码时只需将其功能恢复即可。
局部变量可以作为返回值,这是因为局部变量虽然也在函数的栈中,但是他会在函数结束之前将值存入某个寄存器中(一般是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
可以看到无论静态局部数组有多少个元素,都只进行一次初始化。
数组可以通过下标和指针两种方式进行寻址。下标方式较为常见,效率也更高,指针方式不方便,效率也较低,因为它在访问数组时需要先取出指针中的地址,然后再对其进行偏移得到数组元素,而数组名则是一个常量地址,直接可以进行偏移计算。还是来看例子:
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
从上面的代码可以看到,指针和下标寻址有着很明显的区别,指针需要进行两次寻址,而下标只需要一次,所以下标寻址的效率比较高,但是更为灵活,可以改变指针变量中的地址来访问其他内存,而数组在不越界的情况下是无法访问到数组以外的数据的。
这里再来讲讲下标值的不同类型会带来什么区别:
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函数分析略
需要将数组元素作为参数时直接计算出其下标值
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] ;取出相应的数组元素
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] ;这里和变量一样
当整形表达式的计算结果可以计算出来时,编译器会选择常量折叠,将表达式用常量代替。上面的是先进行表达式计算,然后再找到相应的数组元素。
以上就是数组的三种寻址方式,这三种方式都可以运用在指针寻址中。
C/C++中并不会对数组的越界访问进行检查,所以在使用下标时要时刻注意不要越界访问内存中的其他数据,从而造成程序崩溃甚至更严重的后果。越界下标的使用和正常下标一样,这里不再赘述。
多维数组其实在内存中也是线性存储的,只是在程序中编译器将其做了相应的处理而已。在学习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情况下,当一维数组和二维数组初始化的内容相同时便会一起进行初始化,并且编译器不将所有的初始化语句都放在一起是为了减少指令之间的相关性,使得指令能够像流水线那样执行,提高程序的效率。
其他多维数组的分析和二维数组相同,都是逐渐降维最后转化成低维数组进行处理,这里不再赘述。
指针数组就是存放同一类型指针的数组,数组的每个元素都存放着地址,一般用于处理若干个字符串的操作(如二维字符数组)。它和数组的区别就在于在取出数组中的元素之后还要进行一次间接寻址。下面就用一个例子来加深理解。
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
一个函数指针只能存储同一种类型的函数的地址,包括函数的参数和返回值,否则无法传递参数和返回值,也无法进行栈平衡检查。