栈帧的工作原理

selph
selph
发布于 2020-09-26 / 707 阅读
0
0

栈帧的工作原理

栈帧简介

关于函数调用的参数传递,参数是怎么传入的呢?局部变量又是怎么存储的呢?函数执行完又是怎么知道要返回哪里的呢?如果你好奇这些东西,不妨往下看看~

栈帧(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函数前处下断点,开始分析:

image-20200918084059682

scanf函数里我输入的分别是2和4,我们重点观察mul函数的整个运行过程,首先scanf函数输入完成之后,平栈,然后开始将mul函数的参数入栈,我们单步执行到call mul函数那里,看看栈的情况:

image-20200918084438919

栈顶,参数2和4依次入栈,因为栈是一种先入后出的数据结构,所以是参数从右往左依次入栈,然后我们进入函数,再看看栈:

image-20200918084552907

栈里已经被压入了又一个东西,这是函数调用的返回地址,就是call之后的下一条指令的地址;

我们再看看函数调用的反汇编代码:

image-20200918084730518

前两行:

00461A40 | 55                       | push ebp                                | stackframe.c:2
00461A41 | 8BEC                     | mov ebp,esp                             |

先将EBP入栈保存,然后把ESP的值给EBP,这样可以把栈底指针EBP提高到栈顶指针ESP一样的位置,这样一来通过EBP可以以一个固定的偏移来访问刚刚传进来的参数了:

image-20200918085054443

第一个参数的值是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附近是什么样的:

image-20200918090241669

因为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的值是一样的:

image-20200918091001881

这里应该是以防万一,把前面存到ebp的esp的值取回来,然后取回之前保存到栈内的ebp的值,然后函数就返回了,在执行到返回指令这里的时候,栈顶的内容刚好是函数返回的地址:

image-20200918091129553

执行完,刚好跳转到0x461992的位置:平栈,把前面作为参数入栈的内容弹出

image-20200918091202147

这个时候,栈已经恢复到了函数调用前的状态了~


评论