堆栈平衡:估计这是最详细的讲解堆栈平衡的了

[cpp]  view plain  copy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4. #include <windows.h>  
  5.   
  6. int ShowEsp(int* arg1,int* arg2);  
  7.   
  8. /* 
  9. 引言 各种面试宝典上都会说 又说栈在进程空间的高地址部分,向下扩展;  
  10. 堆在进程空间的低地址部分,堆向上扩展 
  11. 来验证一下是否正如所说这些变量在内存中如何分布? 
  12. */  
  13. int main()  
  14. {  
  15.   
  16.     //0)  
  17.     int i=0xABCDABCD;  
  18.     //int* j = (int*)malloc(sizeof(int)); //malloc为什么被我注掉了?  
  19.     //memcpy(j,&i,sizeof(int));  
  20.     int* k = (int*)VirtualAlloc(NULL,sizeof(int),MEM_COMMIT,PAGE_READWRITE);  
  21.     int* ptr = NULL;  
  22.     ptr = (int*)&i;  
  23.     //ptr = j;   
  24.     /* 
  25.     malloc 是crt运行库实现 系统为crt库在进程靠近2G的高地址处保留大块堆内存 用链表管理  
  26.     调用malloc时,单独从此处抽取分配给调用者 
  27.     */  
  28.     ptr = k;  
  29.   
  30.     //1) 忽略malloc这另类 看看实际系统定义诺干栈变量后,查看他们在内存中的分布  
  31.     int arg1=0x40302010;  
  32.     int arg2=0x20;  
  33.     int* ptr1=NULL;  
  34.     int* ptr2=NULL;  
  35.     char str[] = {"1234567"};  
  36.     printf("arg1:%x\narg2:%x\n",&arg1,&arg2);  
  37.     printf("ptr1:%x\nptr2:%x\n",&ptr1,&ptr2);  
  38.     printf("str:%x\n",str);  
  39.     //结论:变量的地址是连续并且向下扩展  
  40.     //编译器通过sub esp,immd来开辟栈空间  
  41.     //再来看下变量地址存放的内容 arg1  
  42.     char* arg1Ptr = (char*)&arg1;  
  43.     printf("arg1Ptr[0]:%02x\narg1Ptr[1]:%02x\narg1Ptr[2]:%02x\narg1Ptr[3]:%02x\n",*arg1Ptr,*(arg1Ptr+1),*(arg1Ptr+2),*(arg1Ptr+3));  
  44.     //程序没问题吧,感觉在用arg1Ptr取arg1各个字节的内容,看下arg1在内存中的分布  
  45.     //.........................................................................  
  46.     //intel cpu小端机 (题外话 网络编程时 ip/port转换即为大小端字节转换) 数据在内存中按高高低低分布 高字节 在高位 低字节在低位  
  47.   
  48.     //2) 知道了变量在内存中的分布 再看下如何存取变量,alt-8  
  49.     //  
  50.     arg1 = 0x80706050;  
  51.     arg2 = arg1;  
  52.     arg2 = 0x20;  
  53.   
  54.     //程序编译后 用[ebp-N]相对偏移取变量 why?   
  55.     //intel CPU用esp指向当前函数栈顶,变量又保存在栈中,理所当然的可以用esp取变量。  
  56.     //但是变量在不断扩充esp在不断减小 ------  
  57.     //假设 现在要取arg1变量值 可能会编译成 mov eax,[esp+0x40],意思是arg1离栈顶esp相差0x40个字节  
  58.     //如果程序又新定义一个栈变量,栈顶向下移动,即esp=esp-4 此时 esp离arg1的距离为0x44   
  59.     //如果再次取arg1的值 会编译成 mov eax,[esp+0x44] 这样编译起来太麻烦了    
  60.     //a) intel CPU用ebp做当前函数帧[不是强制约定 习惯],也就是栈底,某些面试宝典会写  
  61.     //函数栈底不改变,说的就是ebp再当前函数中不改变。栈变量编译后内存位置固定下来,现在ebp又是固定不变的准绳  
  62.     //这样无论程序怎样扩展栈变量,ebp到各个变量之间的距离都不会改变  
  63.       
  64.     //3) 面试宝典还说 c++参数入栈顺序是 从右往左压栈 全部入完后 压入函数返回地址  
  65.     //既然 大家对堆栈达成共识才看到这了 再来看下调用函数,继续反汇编 观察堆栈变化  
  66.     ShowEsp(&arg1,&arg2);  
  67.     //a)入栈操作 (esp寄存器的变化) 用的是push 每次push后esp减4 压入返回地址后 jmp到ShowEsp  
  68.     //程序jmp到ShowEsp 这里也跟到esp  
  69.       
  70.     //5) 堆栈平衡的收尾  
  71.     /* 
  72.     来看下函数返回后当前栈顶还剩啥 
  73.     指令执行顺序-|esp变化------|保存ebp后变量相对于新栈帧ebp的距离------- 
  74.     push &arg2   |1次esp=esp-4 | ebp+0x0C 
  75.     push &arg1   |2次esp=esp-4 | ebp+0x08 
  76.     */  
  77.     /*函数参数已经显得不重要了可以忽略不计,再说main函数中依然可以通过[ebp-N]的形式访问这些变量 
  78.     但是目前堆栈还没有恢复到函数调用前的样子 还倒欠main函数8个字节,于是编译器采用了一种简单粗暴 
  79.     却有行之有效的办法 
  80.     add esp,0x08 
  81.     于是堆栈平衡 
  82.     还有一个问题,函数范围值在哪?eax中 
  83.     eax 4字节 不管返回什么都没问题 
  84.     */  
  85.     exit(0);  
  86. }  
  87.   
  88. int ShowEsp(int* arg1,int* arg2)  
  89. {  
  90.     //进入到ShowEsp后 先是取形参,然后赋值给局部变量  
  91.     //局部变量访问 已经不是这里的重点 此处重点看下如何取形参  
  92.     int op1,op2,res;  
  93.     //atl-8  
  94.     op1 = *arg1;  
  95.     op2 = *arg2;  
  96.     /* 
  97.     访问arg1 arg2 被翻译成mov eax,[ebp+8],[ebp+0x0c] 
  98.     之前访问变量是用[ebp-N]的形式 现在怎么变成使用[ebp+N]的形式? 
  99.     解释这个 还是得看反汇编的结果 
  100.     反汇编的前几句如下 
  101.     push ebp 
  102.     mov ebp,esp 
  103.     sub esp,4Ch 
  104.     上面2-a)处写到过 intel CPU用ebp做当前函数帧,刚才指令流是在main函数中,因此ebp是基于main函数的 
  105.     现在进入到ShowEsp函数中,要形成新的函数帧,因此先用push ebp把前一个函数的函数帧保存起来,然后 
  106.     mov ebp,esp把当前的栈顶esp赋值给ebp形成新的函数帧 最后的sub esp,4Ch 为本函数创建栈空间 
  107.      
  108.     如果把之前main函数中的函数调用和参数入栈 以及此处生成新的函数帧一系列动作联合起来 并查看esp在此期间的 
  109.     变化: 
  110.     指令执行顺序-|esp变化------|保存ebp后变量相对于新栈帧ebp的距离------- 
  111.     push &arg2   |1次esp=esp-4 | ebp+0x0C 
  112.     push &arg1   |2次esp=esp-4 | ebp+0x08 
  113.     call ShowEsp |3次esp=esp-4 | ebp+0x04 
  114.     push ebp     |4次esp=esp-4 | ebp+0x00 
  115.      
  116.     这应该能解释函数取形参用mov eax,[ebp+0x08]等形式 
  117.     */  
  118.     res = op1+op2;  
  119.   
  120.     //4)堆栈平衡的下半段  
  121.     /* 
  122.     还是拿某宝典说事,说c++是_stdcall c是_cdcel call 
  123.     区别是函数执行结束 一个由被调用的函数恢复堆栈 另一个是由调用者恢复堆栈 
  124.     这代码是_cdcel call 由调用者恢复堆栈 
  125.     来看下这函数怎么恢复堆栈 继续返回编 
  126.     */  
  127.     return res;  
  128.     /* 
  129.     程序结尾处看到 
  130.     mov esp ebp 
  131.     pop ebp 
  132.     ret 8 
  133.     还记得函数入口处的? 
  134.     push ebp 
  135.     mov ebp esp 
  136.     都说是堆栈操作了,所有的操作要呼应对吧,执行了 
  137.     mov esp ebp 
  138.     pop ebp 
  139.     这两句之后,函数堆栈恢复到发生调用ShowEsp的情景,虽然ShowEsp分配的栈变量还存在,但 
  140.     已经处在esp指向的范围之外,换句话说,此时再新建栈变量,以前栈上的变量就会覆盖。当然 
  141.     很少有人这么做。这也是有时候函数返回了,还能得到函数内部变量的原因。注意,所谓的宝典 
  142.     会说,函数返回会自动清栈变量,反正c的代码,我没看到这种语句 
  143.  
  144.     最后的ret语句会使程序返回到函数调用的地方,这个地方由谁指定? 
  145.     还记得3-a)处调用ShowEsp时,把下一条指令压入堆栈?就是那个时候指定函数返回地址,翻阅intel手册 
  146.     说发生ret时,返回到当前栈顶指向的值 
  147.  
  148.     来看下当前栈顶还剩啥 
  149.     指令执行顺序-|esp变化------|保存ebp后变量相对于新栈帧ebp的距离------- 
  150.     push &arg2   |1次esp=esp-4 | ebp+0x0C 
  151.     push &arg1   |2次esp=esp-4 | ebp+0x08 
  152.     call ShowEsp |3次esp=esp-4 | ebp+0x04 
  153.     栈顶还剩call ShowEsp时压入的返回地址,ret执行后 相当于pop eax, jmp eax 弹出一个栈值。 
  154.     于是我们乘坐这个传送门返回到main函数中 
  155.      
  156.     [题外话]如果修改这个返回地址会得到意想不到的结果,这是后面要讲的缓冲区溢出 
  157.     */  
  158. }  
  159.   
  160. //文章结尾感谢同事yj同志 帮我做义务审校,并提出修改建议  
  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值