分享

通过内存布局完成局部变量访问解析

 ar135 2016-02-23
http://topic.csdn.net/u/20110324/19/b2c6a156-7b7e-41d1-933d-34f34b240034.html首先看一段代码:
  1. void a()  
  2. {  
  3.     int arr[10];  
  4.     int i;  
  5.     for(i=0;i<10;i++)  
  6.     {  
  7.         arr[i]=i;           //  初始化数组arr[]  
  8.     }  
  9. }  
  10.   
  11. void b()  
  12. {  
  13.     int arr2[10];   
  14.     int i;  
  15.     for(i=0;i<10;i++)      
  16.     {  
  17.         printf("%d", arr2[i]); //未初始化arr2[],直接输出它的值  
  18.     }     
  19.     // 结果是输出0123456789  
  20. }  
  21.   
  22.   
  23. int _tmain(int argc, _TCHAR* argv[])  
  24. {     
  25.     a();  
  26.     b();  
  27. }  

学过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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多