栈帧简介
关于函数调用的参数传递,参数是怎么传入的呢?局部变量又是怎么存储的呢?函数执行完又是怎么知道要返回哪里的呢?如果你好奇这些东西,不妨往下看看~
栈帧(Stack frame),就是利用栈帧指针EBP寄存器进行访问栈内的局部变量、参数、函数返回地址等
本次分析用到的工具:x32dbg
栈帧的运行原理
这里以一个简单的函数调用的过程为例来介绍栈帧的运行原理
源程序
#include<stdio.h>
int mul(int a,int b) {
int x = a;
int y = b;
return (x*y);
}
int main() {
int a = 0,b = 0;
scanf_s("%d %d", &a, &b);//不加个scanf这个mul函数就让编译器给优化没了
int c = mul(a, b);
printf("The Mul is %d",c);
return 0;
}
栈帧的工作
编译完成后,随手用x32dbg打开,查找字符串,定位到主函数,在mul函数前处下断点,开始分析:
scanf函数里我输入的分别是2和4,我们重点观察mul函数的整个运行过程,首先scanf函数输入完成之后,平栈,然后开始将mul函数的参数入栈,我们单步执行到call mul函数那里,看看栈的情况:
栈顶,参数2和4依次入栈,因为栈是一种先入后出的数据结构,所以是参数从右往左依次入栈,然后我们进入函数,再看看栈:
栈里已经被压入了又一个东西,这是函数调用的返回地址,就是call之后的下一条指令的地址;
我们再看看函数调用的反汇编代码:
前两行:
00461A40 | 55 | push ebp | stackframe.c:2
00461A41 | 8BEC | mov ebp,esp |
先将EBP入栈保存,然后把ESP的值给EBP,这样可以把栈底指针EBP提高到栈顶指针ESP一样的位置,这样一来通过EBP可以以一个固定的偏移来访问刚刚传进来的参数了:
第一个参数的值是2,对应的偏移是[ebp+8],第二个参数的值是4,则对应的偏移是[ebp+c]
接着向下看:
00461A43 | 81EC D8000000 | sub esp,D8 |
00461A49 | 53 | push ebx |
00461A4A | 56 | push esi | esi:__enc$textbss$end+348
00461A4B | 57 | push edi |
00461A4C | 8DBD 28FFFFFF | lea edi,dword ptr ss:[ebp-D8] |
00461A52 | B9 36000000 | mov ecx,36 | 36:'6'
00461A57 | B8 CCCCCCCC | mov eax,CCCCCCCC |
00461A5C | F3:AB | rep stosd |
00461A5E | B9 03C04600 | mov ecx,stackframe.46C003 | stackframe.c:15732480
00461A63 | E8 BEF7FFFF | call stackframe.461226 |
这一段呢,也不是我们自己写的代码的内容,这里是把ESP栈顶抬高,然后把栈的内容刷成CCCCCCC,算是一个初始化操作吧,这不是本次的重点,就跳过吧,接着往下看:
00461A68 | 8B45 08 | mov eax,dword ptr ss:[ebp+8] | stackframe.c:3
00461A6B | 8945 F8 | mov dword ptr ss:[ebp-8],eax |
00461A6E | 8B45 0C | mov eax,dword ptr ss:[ebp+C] | stackframe.c:4
00461A71 | 8945 EC | mov dword ptr ss:[ebp-14],eax |
00461A74 | 8B45 F8 | mov eax,dword ptr ss:[ebp-8] | stackframe.c:5
00461A77 | 0FAF45 EC | imul eax,dword ptr ss:[ebp-14] |
这里的功能呢,是将参数取出来放入局部变量里,[ebp+8]是2,[ebp+C]是4,程序将2存入了[ebp-8],将4存入了[ebp-14],我们执行到imul那一行来看一下ebp附近是什么样的:
因为EBP寄存器的位置在一个函数里是固定的,所以不难发现,EBP往下是函数调用前入栈的内容,EBP往上,则是函数调用后存储的东西,也就是局部变量,因为函数调用完毕之后,ESP和EBP都要回到原来的位置,所以当函数执行完毕返回后,局部变量就不再有用了
所以EBP往上的部分用来存储局部变量,往下的部分用来存储函数参数
接着看程序,通常函数执行完之后,运行结果都通过寄存器EAX来返回,这里进行了乘法运算并把返回值放到了EAX里
接着往下看:
00461A7B | 5F | pop edi | stackframe.c:6
00461A7C | 5E | pop esi | esi:__enc$textbss$end+348
00461A7D | 5B | pop ebx |
00461A7E | 81C4 D8000000 | add esp,D8 |
00461A84 | 3BEC | cmp ebp,esp |
00461A86 | E8 A5F7FFFF | call stackframe.461230 |
这里是将刚刚入栈存起来的edi、esi、ebx取出来,然后把刚刚抬高的ESP给降回来,然后这个CALL应该是检查栈的情况的,不管他,接着往下看程序:
00461A8B | 8BE5 | mov esp,ebp |
00461A8D | 5D | pop ebp |
00461A8E | C3 | ret |
这个时候不出意外的话,ESP和EBP的值是一样的:
这里应该是以防万一,把前面存到ebp的esp的值取回来,然后取回之前保存到栈内的ebp的值,然后函数就返回了,在执行到返回指令这里的时候,栈顶的内容刚好是函数返回的地址:
执行完,刚好跳转到0x461992的位置:平栈,把前面作为参数入栈的内容弹出
这个时候,栈已经恢复到了函数调用前的状态了~