[原创]从反汇编的角度学C/C++之数组,指针及字符串
一.指针
指针最为C/C++最有特色的数据类型,其中保存着某个数据的地址,而对指针的解引用可以让我们获取对应类型的数据。那么不同类型的指针在内存中有什么样的不同,他们又是如何获取地址中的数据的。请看下面这个实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
char cChar = 'a' ; 00D910E8 mov byte ptr [ebp-5],61h //给cChar赋值 char *pChar = &cChar; 00D910EC lea eax,[ebp-5] //取出cChar的占四字节的地址 00D910EF mov dword ptr [ebp-14h],eax //将四字节的地址赋值给pChar int iInt = 1900; 00D910F2 mov dword ptr [ebp-20h],76Ch //给iInt赋值 int *pInt = &iInt; 00D910F9 lea eax,[ebp-20h] //取出iInt的占四字节的地址 00D910FC mov dword ptr [ebp-2Ch],eax //将四字节的地址赋值给pInt printf ( "size of pChar=%d *pChar=%c\n" , sizeof (pChar), *pChar); 00D910FF mov eax,dword ptr [ebp-14h] //将pChar的数据取出,此时pChar保存的是cChar的地址 00D91102 movsx ecx,byte ptr [eax] //从pChar的地址中把数据取出,此时movsx后面跟着的是byte的操作 00D91105 push ecx //压入数据 00D91106 push 4 //压入sizeof(pChar)的值,这个值是4 00D91108 push 0E031B0h //压入字符串 00D9110D call 00D91180 //调用函数 00D91112 add esp,0Ch printf ( "size of pInt=%d *pInt=%d\n" , sizeof (pInt), *pInt); 00D91115 mov eax,dword ptr [ebp-2Ch] //将pInt的数据取出,此时pInt保存的是iInt的地址 00D91118 mov ecx,dword ptr [eax] //从pInt的地址中把数据取出,此时mov后面跟着的是dword的操作 00D9111A push ecx //压入数据 00D9111B push 4 //压入sizeof(pInt)的值,这个值是4 00D9111D push 0E031CCh //压入字符串 00D91122 call 00D91180 //调用函数 00D91127 add esp,0Ch |
由上我们可以得出结论:在内存中,无论是什么类型的指针,他都占有4个字节,保存着数据的地址,而解引用的过程就是拿出这个保存的地址,在去这个地址中拿出所需要的值。他们在内存中的保存形式是一样的,只是在解引用使用的时候不同类型的指针会有不同的使用方法。最终的程序运行结果如下:
指针的指针也是同样的原理,唯一的不同就是对地址多一次的取址或者解引用。
二.一维数组与指针
在C/C++中,我们可以使用数组来保存一组连续的相同类型的数据。那么不同类型的数组在内存中的表现形式究竟如何?他们与指针的关系又是怎么样的?看下面这个实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
int arrInt[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9}; 00F110E8 mov dword ptr [ebp-2Ch],1 int arrInt[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9}; 00F110EF mov dword ptr [ebp-28h],2 00F110F6 mov dword ptr [ebp-24h],3 00F110FD mov dword ptr [ebp-20h],4 00F11104 mov dword ptr [ebp-1Ch],5 00F1110B mov dword ptr [ebp-18h],6 00F11112 mov dword ptr [ebp-14h],7 00F11119 mov dword ptr [ebp-10h],8 00F11120 mov dword ptr [ebp-0Ch],9 00F11127 xor eax,eax 00F11129 mov dword ptr [ebp-8],eax //ebp-0x2C是arrInt的首地址,这段汇编的作用就是初始化我们arrInt数组中的每个元素,这里每个元素之间的间隔都是4字节 int *pInt = arrInt; 00F1112C lea eax,[ebp-2Ch] 00F1112F mov dword ptr [ebp-38h],eax //将arrInt的首地址赋值给pInt char arrChar[10] = { 'a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' }; 00F11132 mov byte ptr [ebp-4Ch],61h 00F11136 mov byte ptr [ebp-4Bh],62h 00F1113A mov byte ptr [ebp-4Ah],63h 00F1113E mov byte ptr [ebp-49h],64h 00F11142 mov byte ptr [ebp-48h],65h 00F11146 mov byte ptr [ebp-47h],66h 00F1114A mov byte ptr [ebp-46h],67h 00F1114E mov byte ptr [ebp-45h],68h 00F11152 xor eax,eax 00F11154 mov word ptr [ebp-44h],ax //ebp-0x4C是arrChar的首地址,这段汇编的作用就是初始化我们arrChar数组中的每个元素,这里每个元素之间的间隔都是1字节 char *pChar = arrChar; 00F11158 lea eax,[ebp-4Ch] 00F1115B mov dword ptr [ebp-58h],eax //将arrChar的的首地址赋值给pChar |
可以看出,声明为数组的变量名就是数组的首地址。对数组的使用时,根据数组所保存的数据类型的宽度不同,程序所赋值的地址也不同。如上面整型数组,数组之间的元素相差4个字节,而字符型数组,数组元素之间就是相差1字节。那么对于数组的寻址过程是怎么样的,他与指针又有什么不同?看下面的实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
printf ( "arrInt[3]=%d,arrInt+3=%d,arrInt=%d\n" , arrInt[3], arrInt + 3, arrInt); 00EB115E lea eax,[ebp-2Ch] //计算arrInt的值,这里就是数组arrInt的首地址 00EB1161 push eax 00EB1162 lea ecx,[ebp-20h] //计算arrInt + 3的值,这里看出arrInt和arrInt+3差了0xC的字节也就是12个字节 00EB1165 push ecx 00EB1166 mov edx,4 //为edx赋值为4 00EB116B imul eax,edx,3 //将edx乘3以后的结果0xC赋值到eax 00EB116E mov ecx,dword ptr [ebp+eax-2Ch] //ebp-0x2C是arrInt的首地址,这里加上eax的值0xC以后得到的就是arrInt[3]的地址,从这个地址中拿出的值就是arrInt[3] 00EB1172 push ecx 00EB1173 push 0F231B0h //压入字符串地址 00EB1178 call 00EB1220 //调用printf 00EB117D add esp,10h printf ( "arrChar[3]=%c,arrChar+3=%d,arrChar=%d\n" , arrChar[3], arrChar + 3, arrChar); 00EB1180 lea eax,[ebp-4Ch] //计算arrChar的值,这里就是数组arrChar的首地址 00EB1183 push eax 00EB1184 lea ecx,[ebp-49h] //计算arrChar + 3的值,这里看出arrChar和arrChar+3差了0x3的字节也就是3个字节 00EB1187 push ecx 00EB1188 mov edx,1 //为edx赋值为1 00EB118D imul eax,edx,3 //将edx乘以3,并把结果放入eax 00EB1190 movsx ecx,byte ptr [ebp+eax-4Ch] //ebp-0x4C是arrChar的首地址,这里加上eax的值0x3以后得到的就是arrChar[3]的地址,从这个地址中拿出的值就是arrChar[3] 00EB1195 push ecx 00EB1196 push 0F231D4h //压入字符串地址 00EB119B call 00EB1220 //调用printf 00EB11A0 add esp,10h printf ( "*(pInt + 3)=%d, pInt + 3=%d, pInt=%d\n" , *(pInt + 3), pInt + 3, pInt); 005611A3 mov eax,dword ptr [ebp-38h] //取出ebp-0x38中的值,这个值就是pInt的值,也就是数组arrInt的首地址,赋值给eax后压栈 005611A6 push eax 005611A7 mov ecx,dword ptr [ebp-38h] //取出ebp-0x38中的值,赋值给ecx, 005611AA add ecx,0Ch //将ecx加0xC个字节也就是12个字节后得到pInt+3的值然后压栈 005611AD push ecx 005611AE mov edx,dword ptr [ebp-38h] //pInt的值,也就是arrInt的首地址赋值给edx 005611B1 mov eax,dword ptr [edx+0Ch] //将edx的值加上0xC得到arrInt+3,把这个地址中的值赋值给eax后压栈 005611B4 push eax 005611B5 push 5D31FCh //压入字符串地址 005611BA call 00561240 //调用printf 005611BF add esp,10h printf ( "*(pChar + 3)=%c, pChar + 3=%d, pChar=%d\n" , *(pChar + 3), pChar + 3, pChar); 005611C2 mov eax,dword ptr [ebp-58h] //取出ebp-0x58中的值,这个值就是pChar,也就是数组arrChar的首地址,赋值给eax然后压栈 005611C5 push eax 005611C6 mov ecx,dword ptr [ebp-58h] //取出ebp-0x58的值赋值给ecx 005611C9 add ecx,3 //将ecx+0x3个字节后得到pChar+3的值然后压栈 005611CC push ecx 005611CD mov edx,dword ptr [ebp-58h] //取出ebp-0x58的值赋值给edx 005611D0 movsx eax,byte ptr [edx+3] //将edx的值加3得到arrChar+3,把这个地址中的值赋值给eax后压栈 005611D4 push eax 005611D5 push 5D3224h //压入字符串地址 005611DA call 00561240 //调用printf 005611DF add esp,10h |
从上面的分析可以得出,无论是数组还是指针,他们在对变量进行加减操作,也就是对arrInt或pInt进行加减操作的时候,不只是数学上简单的加减上随后的数字,而是会根据变量所指类型的宽度来乘以相应的大小。不过数组和指针也存在着不同,数组首先把数组宽度赋值到寄存器中,然后乘以相应的下标,得出距离后和数组首地址进行相加,而指针是直接加上需要的偏移大小。程序运行结果如下:
三.二维数组与指向数组的指针
在C/C++中可以声明多维数组,对应的指针也就是指向数组的指针。那么多维数组和一维数组有什么区别呢?请看下面的实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
int arrInt[2][3] = { {1, 2, 3}, {4, 5, 6} }; 001A10E8 mov dword ptr [ebp-1Ch],1 001A10EF mov dword ptr [ebp-18h],2 001A10F6 mov dword ptr [ebp-14h],3 001A10FD mov dword ptr [ebp-10h],4 001A1104 mov dword ptr [ebp-0Ch],5 001A110B mov dword ptr [ebp-8],6 //从地址ebp-0x1C开始给每个4字节的元素赋值,这里可以看出二维数组的初始化和一维数组的初始化过程并没有区别 int (*pInt)[3] = arrInt; 001A1112 lea eax,[ebp-1Ch] 001A1115 mov dword ptr [ebp-28h],eax //取出数组的首地址赋值给pInt printf ( "arrInt[1][1]=%d,arrInt[1]=%d,arrInt=%d\n" , arrInt[1][1], arrInt[1], arrInt); 001A1118 lea eax,[ebp-1Ch] //取出数组首地址并压栈 001A111B push eax 001A111C mov ecx,0Ch //将ecx赋值为0xC也就是数组的第二维的字节大小乘以3 001A1121 shl ecx,0 //左移操作,这里是为了对移动大小进行乘法,公式是2^(n-1)次方,这里n就是我们指定的arrInt[1]中的1,通过这个方法来算出偏移 001A1124 lea edx,[ebp+ecx-1Ch] //根据偏移得出数据的地址并压栈 001A1128 push edx 001A1129 mov eax,0Ch 001A112E shl eax,0 001A1131 lea ecx,[ebp+eax-1Ch] //跟上面的分析知道这里是根据偏移得到arrInt[1]的位置 001A1135 mov edx,4 //赋值为4,这是因为每个整型是4字节 001A113A shl edx,0 //左移操作,功能与上相同 001A113D mov eax,dword ptr [ecx+edx] //将偏移与arrInt[1]的地址相加得出arrInt[1][1]的位置,取出数据后压栈 001A1140 push eax 001A1141 push 2131B0h //压入字符串的地址 001A1146 call 001A11B0 //调用函数 001A114B add esp,10h printf ( "*(*(pInt + 1) + 1)=%d,pInt+1=%d,pInt=%d" , *(*(pInt + 1) + 1), pInt + 1, pInt); 00FC114E mov eax,dword ptr [ebp-28h] //取出pInt的值,里面保存的是arrInt的首地址 00FC1151 push eax 00FC1152 mov ecx,dword ptr [ebp-28h] //取出pInt的值 00FC1155 add ecx,0Ch //加0xC也就是3*4由于此时指针是指向3维数组的指针所以地址加一都要跨过三个整型大小的字节才能得到对应的值 00FC1158 push ecx 00FC1159 mov edx,dword ptr [ebp-28h] //取出pInt的值 00FC115C mov eax,dword ptr [edx+10h] //首先pInt是指向3维数组的指针所以他加1需要跨过12个字节 00FC115F push eax //对pInt+1解引用之后得到了指向整型的指针这个时候在+1需要跨过4个字节,所以这里需要+0x10 00FC1160 push 10331D8h // 压入字符串地址 00FC1165 call 00FC11B0 //调用printf 00FC116A add esp,10h |
由上我们可以看出,其实二维数组和一维数组没有什么太大的差别,只是他寻找数据的时候要多跨一个维度,而且这个维度的大小和第二维的大小有关。指向数组的指针在进行寻址的时候也是如此,他需要考虑所指数组的大小。
四.字符串
在C/C++中,用""包围的字符被称为字符串,每个字符串的最后都是以0作为字符串的结束。那么字符串在内存中是如何保存的和我们的字符有什么不同。看下面这个实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
char arrTest1[] = { 'H' , 'e' , 'l' , 'l' , 'o' , ' ' , '1' , '9' , '0' , '0' }; 00081038 mov byte ptr [ebp-10h],48h 0008103C mov byte ptr [ebp-0Fh],65h 00081040 mov byte ptr [ebp-0Eh],6Ch 00081044 mov byte ptr [ebp-0Dh],6Ch 00081048 mov byte ptr [ebp-0Ch],6Fh 0008104C mov byte ptr [ebp-0Bh],20h 00081050 mov byte ptr [ebp-0Ah],31h 00081054 mov byte ptr [ebp-9],39h 00081058 mov byte ptr [ebp-8],30h 0008105C mov byte ptr [ebp-7],30h //将单引号所表示的字符的数分别赋值给相应的arrTest1数组的位置 char arrTest2[] = { "Hello 1900" }; 00081060 mov eax,dword ptr ds:[000F31B0h] 00081065 mov dword ptr [ebp-24h],eax 00081068 mov ecx,dword ptr ds:[000F31B4h] 0008106E mov dword ptr [ebp-20h],ecx 00081071 mov dx,word ptr ds:[000F31B8h] 00081078 mov word ptr [ebp-1Ch],dx 0008107C mov al,byte ptr ds:[000F31BAh] 00081081 mov byte ptr [ebp-1Ah],al //从000F31B0地址处取出4个字节的数据赋值给arrTest2数组相应的位置 char *arrTest3 = "Hello 1900" ; 00081084 mov dword ptr [ebp-30h],0F31B0h //将000F31B0地址赋值给arrTest3 |
那么这里的的0x000F31B0存储了什么内容,我们可以在内存窗口查看
可以看到这个地址保存的就是我们需要用到的字符数据,而这个地址所在的位置也被称为全局数据区。
由此我们可以得出结论,相对于单引号的一个一个赋值,双引号包含的字符串放在了全局数据区,当我们对数组进行赋值的时候,程序会从这个地址中把数据赋值到相应的位置,而当我们是对指针进行赋值的时候,程序会把这个地址直接赋值给相应的指针变量。
最后我们可以在变量窗口查看相应的赋值结果