Fork me on GitHub

深度理解函数栈帧

一直说main函数是程序的入口,然后我就看见在这之前还发生了些不为人知的事…

打开调试时的查看函数调用堆栈你就会发现在调用main函数之前,
我们的程序先后先调用了mainCRTStartup和__tmainCRTStartup函数…
为求甚解,我继续调查了函数调用的相关知识,发现函数的调用也是有相当多讲究的~

先认识两个寄存器

这么两个寄存器一个是esp,存储栈顶的地址 另一个是ebp,存储栈底的地址
之前说了在调用main函数之前我们已经调用过了其它函数,main函数也会拥有自己的栈空间
我们随便写一小段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
return 0;
}

进入调试打开反汇编看一下main函数开始后发生了什么:
栈帧示意

1
2
3
4
5
6
7
8
9
10
push ebp                    将ebp的值压栈
mov ebp, esp 将esp的值赋给ebp
sub esp, 4Ch 将esp的值减去0x4C
push ebx
push esi
push edi 将ebx,esi,edi压栈
lea edi, [ebp-4Ch] 将ebp减去0x4C这个地址赋给edi
mov ecx, 13h 将0x13赋给ecx
mov eax, 0CCCCCCCCh 将0xCCCCCCCC赋给eax
rep stos dword ptr [edi] 从edi地址处每次拷贝eax4个字节的内容重复ecx次(初始化内存空间)

就这样简单的几步,main函数的栈帧就开好啦,同时也对里面的内容进行了初始化(0xCCCCCCCC)
这个初始化的值刚好对应汉字“烫烫”所以在一个函数里你要是没有初始化一个变量就将其打印大概率打印出来是个很大的数或者“烫烫烫烫…”
再往下看:
栈帧示意

1
2
3
4
5
6
int a = 10;
mov dword ptr [ebp-4], 0Ah 在ebp-4这个地址放入0xA,就是10、;以下同理
int b = 20;
mov dword ptr [ebp-8], 14h
int c = 0;
mov dword ptr [ebp-oCh], 0

这样我们就将a,b,c三个变量放进了main函数栈的空间里
再往下看:
传参示意

1
2
3
4
5
mov   eax, dwoed ptr [ebp-8]      将ebp-8地址放的内容赋给eax,即eax = b = 20;
push eax 将eax压栈 下同理
mov ecx, dword ptr [ebp-4]
push ecx
call ....

这里我们可以看出来这是传值调用,形成了一份临时拷贝,并且右边的参数先传参;
call这里我们开始调用了add函数,在此之前我们将call指令下条指令的地址已经压栈了(防止函数调用完无法返回)
按下F11我们进入add函数
add函数汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...    和main函数一样的开辟空间
int z = 0;
mov dword ptr [ebp-4],0
z = x+y;
mov eax,dword ptr [ebp+8] [ebp+8]正好是参数x的地址
add eax,dword ptr [ebp+0Ch] eax = x+y
mov dword ptr [ebp-4],eax [ebp-4]存放的是z 所以z = x+y;
return z;
mov eax,dword ptr [ebp-4] 把返回值z存到eax寄存器里
pop edi 函数开始时的压栈统统弹掉
pop esi
pop ebx
mov esp,ebp 把ebp的值给esp
pop ebp ebp弹出取栈顶内容(刚好是main函数的栈底!)
ret call指令下一条指令地址在这里起到作用(知道具体返回哪里)

最后,返回main函数:
返回示意

1
2
3
add     esp, 8                     把栈顶的形参清理(两个四字节数所以加8)
mov dword ptr [ebp-0Ch],eax 把存着返回值的eax寄存器的值赋给c ([ebp-0Ch]是c的地址)
...

大胆的想法

至此我们已经完全观察了main函数创建栈帧、add函数创建栈帧、add函数销毁栈帧的全过程
在这个过程中我们也可以发现,两个“距离相当近的函数”在内存角度上也是靠得相当近的
这让我产生了一个“大胆的想法”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void func()
{
int t = 10;
int * p = (int* )(* (&t + 1);
* (p-1) = 20;
}
int main()
{
int a = 0;
func();
printf("%d\n",a);
return 0;
}

在VC6.0环境下,我们发现func函数看似没有对a进行操作,但是打印出来却发现a已经变成了20!!
我们深入内存的角度,在func的栈帧里,t的地址的上一片四字节空间里放的正是main函数的ebp,解引用之后强转成为整型指针在减1,就到了存放main函数里a的地址,我们在func函数里操作了main函数栈帧里的地址,这样一般来说是不被允许的,所以我使用了并不严格的VC6.0(当然了,其他编译器创建栈帧分配内存时会有差异,代码要做相应的修改),成功改变了a的值

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!