C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。
首先,栈是从高地址向低地址延伸的。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
先来看一个代码
#include <stdio.h> void fun() { int tmp = 10; int*p = (int*)(*(&tmp + 1)); *(p - 1) = 20; } int main() { int a = 0; fun(); printf("a = %d\n", a); return 0; }
这个代码输出的a的值时多少呢? 答案是20。
调用fun()函数后居然把a的值改掉了,从上面的代码可以看到并没有给fun()函数传a的地址,那么fun()函数为什么会把a的值改掉呢? 回答这个问题之前我们需要了解C语言函数的调用过程。
为了更好的观察到上述函数的调用过程,我们在VC 6.0环境下查看一下它的汇编代码。
在这之前首先要知道:
寄存器ebp称为“基址指针”,在未受改变之前始终指向栈底,用途是:在堆栈种寻址。
寄存器esp称为“栈指针”,会随着数据的入栈出栈移动,也就是说始终指向栈 顶。
8: int main()
9: {
0040D700 push ebp ;ebp入栈,即将调用main()的函数的栈底地址 放入堆栈, 以便在执行完main()之后,继续执行调用main()的函数
0040D701 mov ebp,esp ;将esp的值给ebp,此时ebp指向新的栈底
0040D703 sub esp,44h ; esp减44h, 为main函数开辟空间
0040D706 push ebx ; ebx入栈,esp减1, 指向新的栈顶
0040D707 push esi ; esi入栈, esp减1 ,指向新的栈顶
0040D708 push edi ;edi入栈, esp减1,指向新的栈顶
0040D709 lea edi,[ebp-44h] ;将ebp-44h即ebx esi edi入栈前esp的地址放入edi
0040D70C mov ecx,11h ; 为ecx初始化
0040D711 mov eax,0CCCCCCCCh ; 为eax初始化
0040D716 rep stos dword ptr [edi] ;将ebp的地址 存入edi
10: int a =0;
0040D718 mov dword ptr [ebp-4],0 ;为变量a开辟空间,地址为ebp-4, 将0放入该空间
11: fun();
0040D71F call @ILT+5(_fun) (0040100a) ; 首先将main函数中将下一条指令的地址入栈esp 减1,指向新栈顶 , 然后跳到0040100a,即fun() 入口地址
******************************************************************************************************
0040100A jmp fun (00401010) ;跳转到00401010,开始执行fun()
2: void fun()
3: {
00401010 push ebp ;ebp入栈,即将main()的栈底地址放入堆栈
00401011 mov ebp,esp ;将esp的值赋给ebp,此时ebp指向fun()的栈 底,该空间放有main()栈底地址
00401013 sub esp,48h ; esp-48h,为fun()开辟空间
00401016 push ebx ; ebx入栈,esp减1, 指向新的栈顶
00401017 push esi ; esi入栈,esp减1, 指向新的栈顶
00401018 push edi ;edi入栈, esp减1,指向新的栈顶
00401019 lea edi,[ebp-48h] ;将ebp-48h即ebx esi edi入栈前esp的值放入edi
0040101C mov ecx,12h ; 为ecx初始化
00401021 mov eax,0CCCCCCCCh ; 为eax初始化
00401026 rep stos dword ptr [edi] ;将ebp的值 存入edi
4: int tmp =10;
00401028 mov dword ptr [ebp-4],0Ah ;为变量tmp开辟空间,地址为ebp-4,将10放入该 空间
5: int*p = (int*)(*(&tmp+1));
0040102F mov eax,dword ptr [ebp] ;将ebp的值(main()的栈底地址)放到eax中
00401032 mov dword ptr [ebp-8],eax ;将eax的内容放到ebp-8,即为p开辟的空间,此 时p的内容为 main()栈底地址
6: *(p-1) =20;
00401035 mov ecx,dword ptr [ebp-8] ;将p的内容放到ecx中,此时ecx存放的是main() 栈底的地址
00401038 mov dword ptr [ecx-4],14h ;将14h放到ecx减4即main()中的栈底地址减4即a 的地址,这时a的值被改为14h,也就是20
7: }
0040103F pop edi ; edi出栈,esp加1,指向新栈顶
00401040 pop esi ;esi出栈,esp加1,指向新栈顶
00401041 pop ebx ;ebx出栈,esp加1,指向新栈顶
00401042 mov esp,ebp ;将ebp的地址给esp,即销毁为fun()开辟的空间
00401044 pop ebp ;ebp出栈,esp 减1,指向新栈顶,
00401045 ret ;返回,继续执行main()
*******************************************************************************************************
12: printf("a = %d\n",a);
0040D724 mov eax,dword ptr [ebp-4] ;将a的值放入eax中
0040D727 push eax ;eax入栈,esp减1,指向新栈顶
0040D728 push offset string "a = %d\n" (00422fa4)
0040D72D call printf (00401070)
0040D732 add esp,8 ;esp加8
13: return 0;
0040D735 xor eax,eax ;将eax中的内容清零
14: }
0040D737 pop edi ;edi出栈,esp加1,指向新栈顶
0040D738 pop esi ;esi出栈,esp加1,指向新栈顶
0040D739 pop ebx ;ebx出栈,esp加1,指向新栈顶
0040D73A add esp,44h ;esp加44h,销毁为main()开辟的空间
0040D73D cmp ebp,esp ;比较ebp和esp的地址,用于堆栈平衡检测
0040D73F call __chkesp (004010f0) ;返回检测结果
0040D744 mov esp,ebp ;将ebp的值给esp,即调用main()的函数的栈底地址
0040D746 pop ebp ;ebp出栈,esp减1,指向新栈顶,此时esp为调 用main()的函数的栈顶,并且指向调用main()的函 数的下一条指令的地址 ,call-->next
0040D747 ret ;返回,继续执行调用main()的函数
函数调用过程如下图所示:
图中有部分esp移动的过程没有画出,但最终不管如何入栈出栈esp会在销毁空间后指向创建空间前esp最后指向的位置
从上面的汇编代码,我们可以看到fun()函数是通过找到变量a所在的栈的栈底地址,进而找到a的地
址,将a的内容改掉,这就解释了为什么没有给fun()传a的地址,却可以改变a的值。
总结:
函数在调用另一个函数之前会保存两个信息(1)函数调用完返回之后下一条指令的地址
(2)该函数的栈底的地址
最后分享一个使用ebp修改程序执行顺序的代码,并且最后esp回到原处,貌似什么都没发生的样子。
但是却在屏幕上多输出了一个funtest
#include <stdio.h> void*p =NULL; void*q =NULL; void funtest() { int tmp =0; int tmp2 =1; printf("funtest\n"); *(int*)(&tmp+2) =p; } void swap(int *pa, int *pb) { int tmp=0; p= *(&tmp+2); q= &tmp+2; *(&tmp+2) = &funtest; tmp = *pa; *pa = *pb; *pb = tmp; } int main() { int a =10; int b =20; swap(&a,&b); printf("main\n"); _asm{ sub esp,4 } return 0; }
本文出自 “蜗牛” 博客,请务必保留此出处http://15129279495.blog.51cto.com/10845420/1735749
原文地址:http://15129279495.blog.51cto.com/10845420/1735749