首页
论坛
课程
招聘
[原创]从反汇编的角度学C/C++之数组,指针及字符串
2021-9-28 16:08 2359

[原创]从反汇编的角度学C/C++之数组,指针及字符串

2021-9-28 16:08
2359

一.指针

    指针最为C/C++最有特色的数据类型,其中保存着某个数据的地址,而对指针的解引用可以让我们获取对应类型的数据。那么不同类型的指针在内存中有什么样的不同,他们又是如何获取地址中的数据的。请看下面这个实例:

	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++中,我们可以使用数组来保存一组连续的相同类型的数据。那么不同类型的数组在内存中的表现形式究竟如何?他们与指针的关系又是怎么样的?看下面这个实例:

	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字节。那么对于数组的寻址过程是怎么样的,他与指针又有什么不同?看下面的实例:

	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++中可以声明多维数组,对应的指针也就是指向数组的指针。那么多维数组和一维数组有什么区别呢?请看下面的实例:

	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作为字符串的结束。那么字符串在内存中是如何保存的和我们的字符有什么不同。看下面这个实例:

	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存储了什么内容,我们可以在内存窗口查看

    可以看到这个地址保存的就是我们需要用到的字符数据,而这个地址所在的位置也被称为全局数据区。

    由此我们可以得出结论,相对于单引号的一个一个赋值,双引号包含的字符串放在了全局数据区,当我们对数组进行赋值的时候,程序会从这个地址中把数据赋值到相应的位置,而当我们是对指针进行赋值的时候,程序会把这个地址直接赋值给相应的指针变量。

    最后我们可以在变量窗口查看相应的赋值结果

五.引用

    由于指针的错误使用,往往会造成比较严重的后果,C++推出了引用。

11:       int x = 0;
00401068   mov         dword ptr [ebp-4],0        //为x赋值为0
12:       int &y = x;
0040106F   lea         eax,[ebp-4]               //将x地址取出
00401072   mov         dword ptr [ebp-8],eax     //将地址给y
13:
14:       printf("%d\n", x);
00401075   mov         ecx,dword ptr [ebp-4]    
00401078   push        ecx
00401079   push        offset string "%d\n" (00427008)    //输出x
0040107E   call        printf (00401190)
00401083   add         esp,8
15:       y = 1;
00401086   mov         edx,dword ptr [ebp-8]        //取出y,此时是x的地址
00401089   mov         dword ptr [edx],1            //对这个地址赋值为1,其实就是为x赋值
16:       printf("%d\n", x);
0040108F   mov         eax,dword ptr [ebp-4]
00401092   push        eax
00401093   push        offset string "%d\n" (00427008)
00401098   call        printf (00401190)
0040109D   add         esp,8

    可以看出引用和指针在内存中存储形式是一样的,引用只是编译器对指针的封装罢了。


2022 KCTF春季赛【最佳人气奖】火热评选中!快来投票吧~

最后于 2021-10-20 11:18 被1900编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回