函数栈帧的创建和销毁(全网最全,最详细)

对于栈帧这两个字我们好像非常陌生,但是我们对一个程序函数的调用过程非常熟悉,传参、赋值等操作我们都能明白,那传参和赋值等操作,编译器是怎么进行的呢?编译器操作的过程的被栈帧记录下来,我们可以研究函数栈帧的创建和销毁来看看函数是怎么被调用的。

本次调试我全程使用VS2013,如果大家安装的不是13版本,其它版本也大差不差,只是VS随着版本更新,数据的封装就越复杂,用2013更方便看清楚函数调用的过程。


1、准备工作

1.1 寄存器

 在查看函数栈帧前,我们得先了解一些寄存器。像:eax、ebx、ecx、edx、ebp、esp。

其中ebp和esp是非常重要的两个寄存器,ebp始终指向所调用函数的栈帧底部,esp始终指向所调用函数的栈帧顶部,它们的内容都是地址,这两个地址是用来维护函数栈帧的,并且这两个寄存器特殊之处是它们的值是哪个地址就指向哪个地址(但其它的寄存器的内容仅仅是内容)。

1.2 栈的基础知识

栈区是由高地址到低地址,栈底在高地址处,栈顶在低地址处,栈有“先进后出”的特点(队列是“先进先出”的特点)

栈顶和栈底都有指针,栈顶指针就是esp,栈底指针是ebp,换句话说,esp指向栈顶,ebp指向栈底。

每个函数被调用时都会建立栈帧,等下我们在调试过程中就可以看到main函数是如何调用Add函数,实参的值是如何传给形参的。

1.3 源代码

汇编语言是比较难看懂,我们就以一次简单的程序作为范例。

#include
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 10;
	int b = 20;
	int c = 0;
	c = Add(a, b);
	printf("%d\n", c);
	return 0;
}

1.4 如何查看汇编代码

在main函数的第一行设置断点,然后按F10到main函数的‘{’后面,右击,选择“转到反汇编”。

函数栈帧的创建和销毁(全网最全,最详细)_第1张图片函数栈帧的创建和销毁(全网最全,最详细)_第2张图片

 待VS出现了反汇编代码后,我们右击 取消 显示符号表

函数栈帧的创建和销毁(全网最全,最详细)_第3张图片

 最终,我们就可以看到没有符号名的反汇编代码。准备工作就做完了,我接下来会每一步每一步的讲解。

函数栈帧的创建和销毁(全网最全,最详细)_第4张图片

函数栈帧的创建和销毁(全网最全,最详细)_第5张图片


2、真正的干货 

 我用序号代表程序执行步骤

  1. push  ebp :      push  对象,是把该对象压入栈顶,把ebp压入栈顶,此时的栈顶就是main函数的栈底。
  2. mov   ebp,esp :  mov a,b,把b的值赋值给a,把esp的值赋值给ebp,寄存器存放了谁的地址,就指向谁。现在ebp和esp指向同样的位置。
  3. sub  esp  0E4h :  这一步其实是在建立main函数的栈帧,让esp的值减去0E4h,0E4h为main函数所占的空间(如果有朋友对这个0E4h就是它的空间存在疑问的话,我的建议就是记住,每次到这一步esp所减去的这个数值就是main函数所建立的栈帧空间),此时esp指向main函数所创立的栈帧的栈底,ebp指向main函数所创立的栈帧的栈顶。
  4. push  ebx: 把ebx压入栈顶
  5. push  esi : 把esi压入栈顶
  6. push  edi : 把edi压入栈顶
  7. lea  edi,[ebp+FFFFFF1Ch]  : lea(Load Effective Address的简称)加载有效值,lea a b 把b的值放到a中。右击勾选“显示符号名”就会变成“edi,[ebp-24h]”,现在edi的值为 ebp的值24h,让edi从目前的栈顶往低地址方向前进(这是在干什么我们等下就知道了,别着急)
  8. mov ecx 9 :把9赋值给ecx
  9. mov eax 0CCCCCCCCh:把0CCCCCCCCh赋值给eax
  10. rep stos    dword ptr es:[edi] :rep stos 的意思是重复stos指令,dword ptr(double word pointer的缩写),这个指的是双字指针(一个字是两个字节,1字==2byte,双字就是占了四个字节)。这个stos指令是指 把eax的值拷贝到 es:[edi]指向的地址(edi到ebp指向地址) ecx 是重复的次数。
  11. mov ecx,59C003h :把59C003h赋值给ecx
  12. mov dword ptr [ebp-8],0Ah :ebp-8 这就是在给变量a赋值,a=10,记住变量a的地址是ebp-8。0Ah(十六进制,转换成十进制是10),就是把10这个值赋值ebp-8处变量,且这个变量是个双字节变量,占四个字节的变量。
  13. mov dword ptr [ebp-14h],14h :ebp-20 这就是在给变量b赋值,b=20,记住变量b的地址是ebp-14h
  14. mov dword ptr [ebp-20h],0:ebp-32 这就是在给变量c赋值,c=0,记住变量c的地址是ebp-20h
  15. mov    eax,dword ptr [ebp-14h]  :把变量b的值也就是20给寄存器eax
  16. push        eax :把eax push到栈顶
  17. mov         ecx,dword ptr [ebp-8] :把变量a的值也就是10给寄存器ecx
  18. push        ecx  :把ecx push到栈顶(在此我说明一下,我们可以发现,函数传参确实是从右往左传的,这里我们先是把b的值给寄存器,再把a的值给寄存器)
  19. call    005910B4:现在开始调用Add函数了(此时调试时,我们要按照F11进入函数内部),并且push call指令的下一条地址(005918F7),目的:调用完Add函数后还能回到call指令的下一条指令处,并继续执行。
  20. push  ebp : 把ebp压入栈顶(现在开始是在为Add函数建立栈帧,跟main函数相同,我也就不赘述了)
  21. .....................................
  22. mov      dword ptr [ebp-8],0  :建立z变量,赋值为0
  23. mov      eax,dword ptr [ebp+8] :把a的值传给eax(可见,传参时并不是把a直接传过去,而是通过寄存器存放a变量的值)
  24. add       eax,dword ptr [ebp+0Ch] :把a+b的值存放eax
  25. mov      dword ptr [ebp-8],eax :把eax中的值,也就是a+b的值赋值给变量z
  26. mov      eax,dword ptr [ebp-8] :把变量z的值赋值给eax(注意:此时的eax在main函数栈帧中)
  27. pop  edi、esi、ebx:把这三个寄存器弹出栈,esp就会往下移动三个。
  28. add  esp,0CCh : 销毁Add函数的栈帧
  29. mov      esp,ebp :把ebp的值赋给esp,让esp跟ebp同时指向Add函数的栈底
  30. pop  ebp:把ebp的值弹到回到main函数栈底(现在ebp和esp又回到main函数的栈底和栈顶)
  31. ret :返回call的下一个指令
  32. add     esp,8 :让esp+8,esp往下走
  33. mov    dword ptr [ebp-20h],eax :把eax的值也就是a+b的值赋值给c
  34. 后面的内容就是销毁main函数的栈帧了,跟销毁Add函数的栈帧一样,我就不再赘述了。

刚开始我们对这些还不太熟悉,可以操作一步画一步的图。

函数栈帧的创建和销毁(全网最全,最详细)_第6张图片

这里提醒一下:main函数并不是只调用别的函数,而不被调用,main还是也是会被调用的

main
__tmainCRTStartup
__mainCRTSartup

 文章就到此结束了,这篇干货满满,喜欢的朋友请给我点个赞再走吧(真的狠狠走心了)。

你可能感兴趣的:(C语言,jvm,c语言,c++,数据结构)