首先看一段代码:
void a()
{
int arr[10];
int i;
for(i=0;i<10;i++)
{
arr[i]=i; // 初始化数组arr[]
}
}
void b()
{
int arr2[10];
int i;
for(i=0;i<10;i++)
{
printf("%d", arr2[i]); //未初始化arr2[],直接输出它的值
}
// 结果是输出0123456789
}
int _tmain(int argc, _TCHAR* argv[])
{
a();
b();
}
学过C语言的童鞋都知道,这段代码是有问题的,函数a()中对arr[]的初始化,随着函数a()的执行完毕,栈上的局部变量被释放,变量arr[]便“不存在”了,函数b()未初始化数组arr2[]的值,便使用它,其值是随机的。在VS2005和VS2010运行上述代码,输出结果是
竟然是数组arr[]的值,晴天霹雳啊,难道“a()函数内的局部变量,竟在b()函数内得到了访问”,当然这种说法是错误的,我们决定深入的研究下这个问题,话说“汇编一出,真相大白”,就一起来分析一下。
下面为函数a()的汇编代码,学过汇编的人,应该知道CPU自带栈机制的实现,即SS:IP,push,pop操作对应相应SS:IP的改变。
在下面代码里,ebp,esp是函数a()内的栈的栈底地址和栈顶地址。
void a()
{
00401030 push ebp //将ebp压栈
00401031 mov ebp,esp //将栈顶的值赋给栈底指针,即初始化该栈,(汇编含义为mov 寄存器esp的值至ebp)
00401033 sub esp,2Ch //将栈顶指针减少2Ch,即分配出一片内存为函数a()内的变量使用
//2Ch对应的10进制是44,这44个字节,每个int 4字节,arr[10]占40个字节,i占4个字节
int arr[10];
int i;
for(i=0;i<10;i++)
00401036 mov dword ptr [i],0
0040103D jmp a+18h (401048h)
0040103F mov eax,dword ptr [i]
00401042 add eax,1
00401045 mov dword ptr [i],eax
00401048 cmp dword ptr [i],0Ah
0040104C jge a+2Ah (40105Ah)
{
arr[i]=i; // 初始化数组arr[]
0040104E mov ecx,dword ptr [i]
00401051 mov edx,dword ptr [i]
00401054 mov dword ptr arr[ecx*4],edx
00401058 jmp a+0Fh (40103Fh)
}
}
0040105A mov esp,ebp //销毁函数a()的栈
0040105C pop ebp //恢复之前入栈的ebp的值
0040105D ret
其实,函数a()执行后的,内存布局为
0x0012FF68 | 10 | //变量i的值 |
0x0012FF64 | 9 | |
0x0012FF60 | 8 | |
0x0012FF5C | 7 | |
0x0012FF58 | 6 | |
0x0012FF56 | 5 | |
0x0012FF52 | 4 | |
0x0012FF48 | 3 | |
0x0012FF44 | 2 | |
0x0012FF40 | 1 | |
0x0012FF3C | 0 | //数组arr[] |
其中ebp为0x0012FF68,esp为0x0012FF3C(这些内存地址分配根据每台PC的情况不同而不同,以上地址为我的PC的内存地址),函数执行到最后
0040105A mov esp,ebp //销毁函数a()的栈
将指向栈底的ebp的值付给esp(栈顶的值),这个栈为空了,里面没变量了,从函数的角度说,所有的局部变量都已经“不存在”了,我们不应该用函数的局部变量做返回值,但是我们看到了
0040105C pop ebp //恢复之前入栈的ebp的值
0040105D ret
恢复了ebp的值,然后ret(汇编指令与call相对应,控制CS:IP的值),但我们应该注意到, 这段内存空间即0x0012FF68至0x0012FF3C并未被改写或随机化(实际上不存在随机化内存这种说法),然后我们看下函数b()的代码:
void b()
{
00401060 push ebp
00401061 mov ebp,esp
00401063 sub esp,2Ch
int arr2[10];
int i;
for(i=0;i<10;i++)
00401066 mov dword ptr [i],0
0040106D jmp b+18h (401078h)
0040106F mov eax,dword ptr [i]
00401072 add eax,1
00401075 mov dword ptr [i],eax
00401078 cmp dword ptr [i],0Ah
0040107C jge b+35h (401095h)
{
printf("%d", arr2[i]); //未初始化arr2[],直接输出它的值
0040107E mov ecx,dword ptr [i]
00401081 mov edx,dword ptr arr2[ecx*4]
00401085 push edx
00401086 push offset ___xt_z+120h (41DB5Ch)
0040108B call printf (4010D1h)
00401090 add esp,8
00401093 jmp b+0Fh (40106Fh)
}
// 结果是输出0123456789
}
其中ebp还是为0x0012FF68,esp还是为0x0012FF3C,也就是函数b()的局部变量栈的内存布局,同函数a()的(已经释放了,又被函数b()拿来使用),完全一样,所以从结果上看好像是函数b()输出了函数a()的值。
任何改变函数a()或者函数b()的内存布局的做法,都会影响到以上代码的结果,比如只在函数a()内多定义数组或变量,或者只在函数b()内多定义数组或变量(int x;int arr1[10]),,都会改变函数b()的输出,也可以思考下,为什么main()定义的变量为什么没改变输出结果,只是改变了ebp和esp的值。
总之,上面的代码利用内存布局,完成了不可能完成的访问,但具体编程不应该使用这种方法,1.太颠覆常规概念;2.跟编译器有很大关系,以上代码在GCC上的某些版本输出便是随机数,所以这还是一种“错误做法”。
参考资料http://topic.csdn.net/u/20110324/19/b2c6a156-7b7e-41d1-933d-34f34b240034.html