数组
数组是C语言中的一个常见的数据结构,那么数组在汇编语言中是如何表示的呢?
一个简单的一维数组
#include <stdio.h>
int main(void)
{
int array[10] = { 1,2,3,4,5,6,7,8,9,10 };
return 0;
}
反汇编如下
5: int array[10] = { 1,2,3,4,5,6,7,8,9,10 };
00A343A2 C7 45 D0 01 00 00 00 mov dword ptr [array],1
00A343A9 C7 45 D4 02 00 00 00 mov dword ptr [ebp-2Ch],2
00A343B0 C7 45 D8 03 00 00 00 mov dword ptr [ebp-28h],3
00A343B7 C7 45 DC 04 00 00 00 mov dword ptr [ebp-24h],4
00A343BE C7 45 E0 05 00 00 00 mov dword ptr [ebp-20h],5
00A343C5 C7 45 E4 06 00 00 00 mov dword ptr [ebp-1Ch],6
00A343CC C7 45 E8 07 00 00 00 mov dword ptr [ebp-18h],7
00A343D3 C7 45 EC 08 00 00 00 mov dword ptr [ebp-14h],8
00A343DA C7 45 F0 09 00 00 00 mov dword ptr [ebp-10h],9
00A343E1 C7 45 F4 0A 00 00 00 mov dword ptr [ebp-0Ch],0Ah
6:
7: return 0;
00A343E8 33 C0 xor eax,eax
为了更清晰的表示,我将符号名去掉
5: int array[10] = { 1,2,3,4,5,6,7,8,9,10 };
00A343A2 C7 45 D0 01 00 00 00 mov dword ptr [ebp-30h],1
00A343A9 C7 45 D4 02 00 00 00 mov dword ptr [ebp-2Ch],2
00A343B0 C7 45 D8 03 00 00 00 mov dword ptr [ebp-28h],3
00A343B7 C7 45 DC 04 00 00 00 mov dword ptr [ebp-24h],4
00A343BE C7 45 E0 05 00 00 00 mov dword ptr [ebp-20h],5
00A343C5 C7 45 E4 06 00 00 00 mov dword ptr [ebp-1Ch],6
00A343CC C7 45 E8 07 00 00 00 mov dword ptr [ebp-18h],7
00A343D3 C7 45 EC 08 00 00 00 mov dword ptr [ebp-14h],8
00A343DA C7 45 F0 09 00 00 00 mov dword ptr [ebp-10h],9
00A343E1 C7 45 F4 0A 00 00 00 mov dword ptr [ebp-0Ch],0Ah
6:
7: return 0;
00A343E8 33 C0 xor eax,eax
可以看的出来数组中的元素地址最低的是1,最高的是10。数组实际上就是将函数中将栈帧中的一部分作为了数组,而且这部分空间的地址是连续的
在C语言中经常会出现数组越界的情况,那么来看看在汇编中这种情况是怎么表示的
#include <stdio.h>
int main(void)
{
int array[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i <= 10; i++)
printf("%d ", array[i]);
putchar('\n');
return 0;
}
这段代码执行后会出现这种情况
最后一个负数实际上就是在main函数反汇编讲解中出现那个用来填充栈帧的CCCCCCCC,用计算器将它用十六进制表示就能发现。
我们随便把栈帧中一部分空间找出来,看看里面的值是不是CCCCCCCC
#include <stdio.h>
int main(void)
{
int x;
__asm /*__asm是C语言的关键字,可以在__asm中写汇编代码*/
{
mov eax, dword ptr [ebp-4]
mov x, eax
}
printf("%d\n", x);
return 0;
}
运行结果如下:
值是一模一样的
结论:
从上述结果可知,数组越界实际上是将栈帧中一部分未赋值的空间进行了读取,从而会出现数组越界的情况
C语言中有时还会出现二维数组、三维数组等,我们来看看它们在汇编中是如何表示的
二维数组
#include <stdio.h>
int main(void)
{
int array[3][3] =
{
{0,1,2},
{3,4,5},
{6,7,8}
};
return 0;
}
反汇编后如下:
5: int array[3][3] =
009843A2 C7 45 D4 00 00 00 00 mov dword ptr [ebp-2Ch],0
009843A9 C7 45 D8 01 00 00 00 mov dword ptr [ebp-28h],1
009843B0 C7 45 DC 02 00 00 00 mov dword ptr [ebp-24h],2
009843B7 C7 45 E0 03 00 00 00 mov dword ptr [ebp-20h],3
009843BE C7 45 E4 04 00 00 00 mov dword ptr [ebp-1Ch],4
009843C5 C7 45 E8 05 00 00 00 mov dword ptr [ebp-18h],5
009843CC C7 45 EC 06 00 00 00 mov dword ptr [ebp-14h],6
009843D3 C7 45 F0 07 00 00 00 mov dword ptr [ebp-10h],7
009843DA C7 45 F4 08 00 00 00 mov dword ptr [ebp-0Ch],8
6: {
7: {0,1,2},
8: {3,4,5},
9: {6,7,8}
10: };
11:
12: return 0;
009843E1 33 C0 xor eax,eax
这时会发现二维数组的表示和一维数组的表示几乎是一样的
三维数组会如何呢?
5: int array[2][2][2] =
00335202 C7 45 D8 00 00 00 00 mov dword ptr [ebp-28h],0
00335209 C7 45 DC 01 00 00 00 mov dword ptr [ebp-24h],1
00335210 C7 45 E0 02 00 00 00 mov dword ptr [ebp-20h],2
00335217 C7 45 E4 03 00 00 00 mov dword ptr [ebp-1Ch],3
0033521E C7 45 E8 04 00 00 00 mov dword ptr [ebp-18h],4
00335225 C7 45 EC 05 00 00 00 mov dword ptr [ebp-14h],5
0033522C C7 45 F0 06 00 00 00 mov dword ptr [ebp-10h],6
00335233 C7 45 F4 07 00 00 00 mov dword ptr [ebp-0Ch],7
6: {
7: {
8: {0,1},
9: {2,3},
10: },
11: {
12: {4,5},
13: {6,7},
14: },
15:
16: };
17:
18: return 0;
0033523A 33 C0 xor eax,eax
同上
结论:
无论是几维数组,在栈帧中总是一块连续的空间,且每相邻两个元素之间的距离视数组的数据类型决定
字符串
在看字符串之前先看看字符数组在汇编中是如何表示的
一个简单的字符数组
#include <stdio.h>
int main(void)
{
char str[12] = { 'H','e','l','l','o',' ','W','o','r','l','d','!' };
return 0;
}
反汇编如下
5: char str[12] = { 'H','e','l','l','o',' ','W','o','r','l','d','!' };
001A5202 C6 45 EC 48 mov byte ptr [ebp-14h],48h
001A5206 C6 45 ED 65 mov byte ptr [ebp-13h],65h
001A520A C6 45 EE 6C mov byte ptr [ebp-12h],6Ch
001A520E C6 45 EF 6C mov byte ptr [ebp-11h],6Ch
001A5212 C6 45 F0 6F mov byte ptr [ebp-10h],6Fh
001A5216 C6 45 F1 20 mov byte ptr [ebp-0Fh],20h
001A521A C6 45 F2 57 mov byte ptr [ebp-0Eh],57h
001A521E C6 45 F3 6F mov byte ptr [ebp-0Dh],6Fh
001A5222 C6 45 F4 72 mov byte ptr [ebp-0Ch],72h
001A5226 C6 45 F5 6C mov byte ptr [ebp-0Bh],6Ch
001A522A C6 45 F6 64 mov byte ptr [ebp-0Ah],64h
001A522E C6 45 F7 21 mov byte ptr [ebp-9],21h
6:
7: return 0;
001A5232 33 C0 xor eax,eax
在汇编中,每个字符都用ASCII码表示,而且每个字符大小都为一个字节,即byte
了解了这些就可以看看C语言中的字符串在汇编中是如何表示的了
字符串
#include <stdio.h>
int main(void)
{
char str[13] = "Hello,World!";
return 0;
}
这里多出来的一个数组空间是为了给空白符'\0'准备的。这段代码反汇编如下
5: char str[13] = "Hello,World!";
00625202 A1 CC 7B 62 00 mov eax,dword ptr ds:[00627BCCh]
00625207 89 45 E8 mov dword ptr [ebp-18h],eax
0062520A 8B 0D D0 7B 62 00 mov ecx,dword ptr ds:[00627BD0h]
00625210 89 4D EC mov dword ptr [ebp-14h],ecx
00625213 8B 15 D4 7B 62 00 mov edx,dword ptr ds:[00627BD4h]
00625219 89 55 F0 mov dword ptr [ebp-10h],edx
0062521C A0 D8 7B 62 00 mov al,byte ptr ds:[00627BD8h]
00625221 88 45 F4 mov byte ptr [ebp-0Ch],al
6:
7: return 0;
00625224 33 C0 xor eax,eax
因为字符串也是一种特殊的字符数组,它与字符数组最大的区别就在于末尾会有一个空白符'\0'。在之前讲过的栈中我们知道,汇编中的栈一个空间单位大小为4个字节,因此每次入栈都是4个字符
先将H、e、l、l入栈,然后是o、,、W、o入栈,最后是r、l、d、!入栈,全部入栈完毕后再入栈空白符'\0'
指针
C语言中最头疼的就是指针,可以说掌握了指针就相当于掌握了C语言,现在让我们看看指针在汇编中的表现形式
一个简单的指针
#include <stdio.h>
int main(void)
{
int* p = NULL;
return 0;
}
反汇编如下
5: int* p = NULL;
00F151F8 C7 45 F8 00 00 00 00 mov dword ptr [ebp-8],0
6:
7: return 0;
00F151FF 33 C0 xor eax,eax
可以看到在汇编下指针与其他普通变量并无大的区别
现在来看看下面这个代码,看看指针怎么操作的
指针的简单操作
#include <stdio.h>
int main(void)
{
int* p = NULL;
int x = 5;
int y;
p = &x;
y = *p;
return 0;
}
反汇编如下
5: int* p = NULL;
00F851F8 C7 45 F8 00 00 00 00 mov dword ptr [ebp-8],0
6: int x = 5;
00F851FF C7 45 EC 05 00 00 00 mov dword ptr [ebp-14h],5
7: int y;
8:
9: p = &x;
00F85206 8D 45 EC lea eax,[ebp-14h]
00F85209 89 45 F8 mov dword ptr [ebp-8],eax
10: y = *p;
00F8520C 8B 45 F8 mov eax,dword ptr [ebp-8]
00F8520F 8B 08 mov ecx,dword ptr [eax]
00F85211 89 4D E0 mov dword ptr [ebp-20h],ecx
11:
12: return 0;
00F85214 33 C0 xor eax,eax
我们一步一步看,先从p = &x开始看起
9: p = &x;
00F85206 8D 45 EC lea eax,[ebp-14h]
00F85209 89 45 F8 mov dword ptr [ebp-8],eax
在之前讲过,lea操作符是将变量的地址赋值给寄存器,这里就是将x的地址复制给了指针p。
10: y = *p;
00F8520C 8B 45 F8 mov eax,dword ptr [ebp-8]
00F8520F 8B 08 mov ecx,dword ptr [eax]
00F85211 89 4D E0 mov dword ptr [ebp-20h],ecx
这里p将地址赋给了eax,然后eax通过解引用地址后,将x赋值给了y。指针操作很明显是间接寻址,即通过一个地址媒介来操作一个对象;与之相反的是直接寻址,即直接操作对象。
在C语言中,我们访问数组的方式有两种,一种是下标法,一种是指针法。下面看看用指针法访问数组的汇编表示
指针法访问数组
#include <stdio.h>
int main(void)
{
int array[10] = { 1,2,3,4,5,6,7,8,9,10 };
int x;
x = *array;
x = *(array + 1);
x = *(array + 2);
return 0;
}
反汇编如下
5: int array[10] = { 1,2,3,4,5,6,7,8,9,10 };
00E95202 C7 45 D0 01 00 00 00 mov dword ptr [ebp-30h],1
00E95209 C7 45 D4 02 00 00 00 mov dword ptr [ebp-2Ch],2
00E95210 C7 45 D8 03 00 00 00 mov dword ptr [ebp-28h],3
00E95217 C7 45 DC 04 00 00 00 mov dword ptr [ebp-24h],4
00E9521E C7 45 E0 05 00 00 00 mov dword ptr [ebp-20h],5
00E95225 C7 45 E4 06 00 00 00 mov dword ptr [ebp-1Ch],6
00E9522C C7 45 E8 07 00 00 00 mov dword ptr [ebp-18h],7
00E95233 C7 45 EC 08 00 00 00 mov dword ptr [ebp-14h],8
00E9523A C7 45 F0 09 00 00 00 mov dword ptr [ebp-10h],9
00E95241 C7 45 F4 0A 00 00 00 mov dword ptr [ebp-0Ch],0Ah
6: int x;
7:
8: x = *array;
00E95248 B8 04 00 00 00 mov eax,4
00E9524D 6B C8 00 imul ecx,eax,0
00E95250 8B 54 0D D0 mov edx,dword ptr [ebp+ecx-30h]
00E95254 89 55 C4 mov dword ptr [ebp-3Ch],edx
9: x = *(array + 1);
00E95257 8B 45 D4 mov eax,dword ptr [ebp-2Ch]
00E9525A 89 45 C4 mov dword ptr [ebp-3Ch],eax
10: x = *(array + 2);
00E9525D 8B 45 D8 mov eax,dword ptr [ebp-28h]
00E95260 89 45 C4 mov dword ptr [ebp-3Ch],eax
11:
12: return 0;
00E95263 33 C0 xor eax,eax
可以看到指针法的汇编表示与下标法的差不多,那么二维的呢?
指针法访问二维数组
#include <stdio.h>
int main(void)
{
int array[3][3] =
{
{0,1,2},
{3,4,5},
{6,7,8}
};
return 0;
}
反汇编如下
5: int array[3][3] =
00E75202 C7 45 D4 00 00 00 00 mov dword ptr [ebp-2Ch],0
00E75209 C7 45 D8 01 00 00 00 mov dword ptr [ebp-28h],1
00E75210 C7 45 DC 02 00 00 00 mov dword ptr [ebp-24h],2
00E75217 C7 45 E0 03 00 00 00 mov dword ptr [ebp-20h],3
00E7521E C7 45 E4 04 00 00 00 mov dword ptr [ebp-1Ch],4
00E75225 C7 45 E8 05 00 00 00 mov dword ptr [ebp-18h],5
00E7522C C7 45 EC 06 00 00 00 mov dword ptr [ebp-14h],6
00E75233 C7 45 F0 07 00 00 00 mov dword ptr [ebp-10h],7
00E7523A C7 45 F4 08 00 00 00 mov dword ptr [ebp-0Ch],8
6: {
7: {0,1,2},
8: {3,4,5},
9: {6,7,8}
10: };
11:
12: int x;
13:
14: x = *(*(array));
00E75241 B8 0C 00 00 00 mov eax,0Ch
00E75246 6B C8 00 imul ecx,eax,0
00E75249 8D 54 0D D4 lea edx,[ebp+ecx-2Ch]
00E7524D B8 04 00 00 00 mov eax,4
00E75252 6B C8 00 imul ecx,eax,0
00E75255 8B 14 0A mov edx,dword ptr [edx+ecx]
00E75258 89 55 C8 mov dword ptr [ebp-38h],edx
15: x = *(*(array)+2);
00E7525B B8 0C 00 00 00 mov eax,0Ch
00E75260 6B C8 00 imul ecx,eax,0
00E75263 8B 54 0D DC mov edx,dword ptr [ebp+ecx-24h]
00E75267 89 55 C8 mov dword ptr [ebp-38h],edx
16: x = *(*(array+2)+2);
00E7526A 8B 45 F4 mov eax,dword ptr [ebp-0Ch]
00E7526D 89 45 C8 mov dword ptr [ebp-38h],eax
17:
18: return 0;
00E75270 33 C0 xor eax,eax
同上
总结
- 数组无论是几维,在栈中都是一块连续的空间,且每相邻两个数组单元之间的距离视数组的数据类型决定(int对应dword、long int对应qword、char对应byte等)
- 数组访问越界实际上是因为访问到了一块没有赋值或初始化的空间,在C语言中无论什么函数,执行操作之前都会将栈帧中的区域进行初始化(如CCCCCCCC)
- 字符串是一种特殊的字符数组,又因为字符都用ASCII编码,每一个字符占一个字节,所以对字符串进行初始化时一次填入4个字符,并在最后加入空白符'\0'
- 指针在汇编中与其他一般的变量没有太大的区别,只不过多了一个lea操作
- 指针操作是间接寻址,即通过一个地址媒介来操作一个对象;与之相反的是直接寻址,即直接操作对象。