函数的调用过程(栈帧)
话说,什么是函数栈帧?我之前也是一脸懵逼的(┭┮﹏┭┮),举个栗子,先看一段简单的代码:
#include<stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("ret=%d\n", ret);
return 0;
}
这段代码我想应该没人看不懂的,那么我们想知道如何调用函数的,程序调试的时候,查看【调用堆栈】,如下图:
我们发现,其实main函数在mainCRTStartup()函数中调用的
提到栈帧的维护我们就必须了解ebp和esp这两个寄存器。在函数调用过程中这两个寄存器放了维护这个栈的栈底和栈顶指针。
例如,在调用main函数是,我们为main函数分配栈帧空间,那么栈帧维护如下:
ebp存放了指向函数栈帧栈底的地址
esp存放了指向函数栈帧栈顶的地址
接下来我们具体分析上面一段代码:
上面我们知道,main函数是被调用过来的,那么这个程序第一步会为main函数分配栈空间调试并转到反汇编我们会看到下面一段一眼望去并不怎么懂得代码: ![这里写图片描述](https://img-blog.csdn.net/201807251957445?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNTUwMDE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
在这里我简单解释一下上图:
002B17F0 push ebp
002B17F1 mov ebp,esp
002B17F3 sub esp,0E4h
002B17F9 push ebx
002B17FA push esi
002B17FB push edi
第一步:将ebp压栈
第二步:将esp赋给ebp
第三步:是给esp减去0E4h的大小
接下来几步:分别将ebx,esi,edi压栈
我们知道,一开始ebp存放了指向mainCRTStartup函数栈帧栈底的地址,那么第一步将ebp压栈就会在原来esp上面多了一块空间,而这块空间此时就是ebp压栈进去的,然后将esp指向的地址给ebp,再给esp减去0E4h,即将esp上移了,图解如下:
从上图中我们很容易的看出在调用main函数是为main函数开辟的栈空间即栈帧。
那么紧接着,又将ebx,esi,edi压栈,会出现如下图所示:
002B17FC lea edi,[ebp-0E4h]
002B1802 mov ecx,39h
002B1807 mov eax,0CCCCCCCCh
002B180C rep stos dword ptr es:[edi]
接下来这四句,lea是加载有效地址,从上图中,我们知道ebp指向的地址,那么edi存放的就是ebp-0E4h的地址也就是③esp处的地址,最后一句rep stos dword ptr es:[edi] 意思是从edi里面重复拷贝ecx次eax的内容。
接下来的汇编代码如下:
int a = 10;
002B180E mov dword ptr [a],0Ah
int b = 20;
002B1815 mov dword ptr [b],14h
002B180E mov dword ptr [a],0Ah
这一行其实就是将10移动到0Ah的位置上,其实就是ebp-8
002B1815 mov dword ptr [b],14h注:emmmmmmm这里有必要解释一下为什么是ebp-8和ebp-20,因为自己刚开始就非常的不解,其实在定义a的时候就将a压栈进去,那么ebp就要上移,即ebp要减去一个数,至于减多少,这个是跟编译器有关的,我们可以内存查一下ebp和&a很容看出这两者之间相差了8,图如下:
这一行和上面一行意思是一样的,将20放在14h的位置上,就是ebp-20
接着就调用Add函数了:
int ret = Add(a, b);
002B181C mov eax,dword ptr [b]
int ret = Add(a, b);
002B181F push eax
002B1820 mov ecx,dword ptr [a]
002B1823 push ecx
这段就比较容易懂了,仔细看过之后就会知道,其实是将a和b压栈,用ecx和eax来接收a和b,起到形参作用
002B1824 call _Add (02B1109h)
002B1829 add esp,8
002B182C mov dword ptr [ret],eax
接下来就是调用Add函数了,我们具体转到Add函数的反汇编分析一下:(未截完)
我们还是和之前一样分块分析
002B16E0 push ebp
002B16E1 mov ebp,esp
002B16E3 sub esp,0CCh
002B16E9 push ebx
002B16EA push esi
002B16EB push edi
002B16EC lea edi,[ebp-0CCh]
002B16F2 mov ecx,33h
002B16F7 mov eax,0CCCCCCCCh
002B16FC rep stos dword ptr es:[edi]
int z = 0;
002B16FE mov dword ptr [z],0
这里Add函数创建栈帧的过程其实和main函数一样的,先将ebp压栈,再将esp的地址存放到ebp里面,此时,esp和ebp指向同一位置,再将esp上移0CCh个位置,然后就是ebx,esi,edi压栈,这里不多作说明,详细图解如下:
z = x + y;
002B1705 mov eax,dword ptr [x]
002B1708 add eax,dword ptr [y]
002B170B mov dword ptr [z],eax
return z;
002B170E mov eax,dword ptr [z]
这里就更容易懂了,eax存放x的值,再给这个值加上y的值,再把这个值赋给z,最后将z的值返回给eax
002B1711 pop edi
002B1712 pop esi
002B1713 pop ebx
002B1714 mov esp,ebp
002B1716 pop ebp
002B1717 ret
这一步,显然我们看到pop,出栈了,就是销毁栈帧的过程,因为这个时候,Add函数已经用完了,将edi,esi,ebx按顺序出栈,这时,我们要将ebp出栈,就得将esp指到ebp的位置,这时esp和ebp指向同一位置,这个位置就是我们所保存的main()函数的ebp,然后再pop ebp,这样ebp就维护到main函数的栈帧了。 当ret指令执行之后,会pop一下,把这个地址pop以后,就从Sub函数返回了main()函数,这也是最初为什么要保存这个地址的原因。这样call指令就完成了。
到这里我们可以看到销毁Add栈帧出来会指向call指令的下一个指令
002B1829 add esp,8
002B182C mov dword ptr [ret],eax
printf("ret=%d\n", ret);
002B182F mov eax,dword ptr [ret]
002B1832 push eax
002B1833 push offset string "ret=%d\n" (02B7B30h)
002B1838 call _printf (02B132Fh)
002B183D add esp,8
return 0;
给esp+8之后显然把刚开始在main函数里用来接收a和b的形参也弹了出去,然后把eax(就是计算出来的值)放到ret里。再把ret给eax,将eax压栈,offset是字符串的偏移地址,再调用printf函数。 最后销毁main函数栈帧和上面销毁Add函数栈帧是一个意思,这里不多做说明。以上是我个人的理解,如有纰漏和错误的地方,还请各方大神指正,感激不尽!