引入:
学习函数的时候我们会有许多的疑问
例如:
局部变量是怎样创建的?
为什么局部变量的值是随机值?
函数是怎么传参的?传参的顺序是怎样的?
形参和实参是什么关系?
函数调用是怎么做的?
函数调用结束后怎么返回的?
本文所用环境是vs2013,建议不要使用太高级的编译器,越高级的编译器,越不容易学习和观察,同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现
引入概念:
1.寄存器
有eax ebx ecx edx ebp esp
本文(函数栈帧的创建与销毁)主要运用 ebp esp
ebp,esp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的.
问题:
ebp和esp是怎么来维护函数栈帧的?
每一个函数调用,都要在栈区创建一个空间
先写一段代码:(方便理解)
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;
}
main函数调用的时候我们会在栈区为main函数开辟一块空间,我们可以将之称为:main函数的函数栈帧
延伸:我们将esp称为栈顶指针,edp称为栈底指针
我们按 F5 启动调试,然后按照开始调试,在:
中打开“调用堆栈”,会出现窗口:
此时,我们会发现:“main”函数被调用,但是他是被谁调用的呢?
我们此时一直按 F10 调试,一直到这个界面:
我们从图中黄色标记出可以看出“main”是被“__tmainCRTStartup”函数调用(我们可以往上面翻,可以找到这个函数)
即:main函数(主函数)也是被别的函数调用的
问题:“_tmainCRTStartup”是不是也是被谁调用的?我们继续往上面翻
我们可以得出结论:在vs2013中,由黄色阴影可以看出来“_tmainCRTStartup”函数是被“mainCRTStartup”函数调用的。
我们从上面可以看出来“mainCRTStartup”函数会返回“_tmainCRTStartup”,而“_tmainCRTStartup”会返回“main”,我们写的代码中“main”会“return 0”;(即下图),黄色阴影部分显示:
即:“mainCRTStartup”调用了“_tmainCRTStartup”,“_tmainCRTStartup”调用了“main”。
我们在栈区使用内存的时候,我们每一次函数调用的时候,我们都要在栈区为函数分配空间,
即:系统会在如图位置分配栈帧空间。
此时我们重新调试代码,按F5启动调试,然后按一次F10
然后鼠标右键(1)位置,选择“转到反汇编”
此时我们便可以看到c语言的所对应的汇编代码:
如果没有“显示符号名”这个选项,可以在下图两个位置:
1.点击一号位置,然后会出现
2.右击空白,然后将显示符号名去掉
原因:我们为了看具体地址,而不是符号名字,所以我们将黄色阴影处的“显示符号名”去掉,此时:
我们调用“main”函数时,需要执行下图阴影部分:
我们从前面得知,我们在调用“main”函数的时候,我们需要调用别的函数,在此之前edp和esp维护的是:“__tmainCRTStartup”这个函数,即:
当我们进入“main”函数的时候
我们此时:push ebp(push压栈)
在调用“main”函数之前,我们调用了“__tmainCRTStartup”,此时维护“__tmainCRTStartup”“edp和esp”的位置将会发生改变:
我们从vs2013内存中验证一下:
先按“F5”启动调试,然后按“F10”开始调试,此时在监视窗口查找“esp和edp”的地址值
然后鼠标单击上图蓝色部分,然后按“F11”逐语句开始调试,此时:
由上面两个图可以看出,esp向上平移了一个位置,指向了edp的上方位置。
也可以在内存中查找原因:
然后在内存搜索栏中搜索esp:
然后我们结合监视窗口和内存窗口来观察一下:
即:esp指向的内存就是ebp的地址。
接下来指向这一条语句:
延伸:mov 就是将后面esp的值赋给edp
即:edp指向了esp
我们先鼠标单击前面,然后再按一下“F11”
此时在监视窗口中,edp和esp的值相等。
接下来执行:
延伸:sub 减,即:给esp减去0E4h(十六进制数字)(228)
此时我们鼠标单击前面,然后按“F11”
此时esp将会缩小,即:此时esp将会指向上面的一块区域
画图表示:
通过反汇编和监视窗口可以得知,esp和ebp中间开辟的空间为两地址相减
接下来执行三个push
即:将这三个指针依次放在最上面,并且每次放一个时,esp就会向上调节一个单位(即:esp永远指向最上面)
画图表示:
接下来执行:
延伸:
lea:load effecitve address加载有效地址
即:将[ebp+FFFFFF1Ch] 这个地址加载到edi中去(给edi中放了一个[ebp+FFFFFF1Ch] 这样的地址)
此时我们把显示符号名勾上,这个时候我们就能知道[ebp+FFFFFF1Ch]代表什么意思
由上可知:[ebp-0E4h]的位置在如图所示:
接下来两个mov,给ecx和eax赋值
接下来执行:
即:要把刚刚从edi这个位置开始向下的39h这么多空间全部改成0CCCCCCCCh
延伸:
dword:double word 四个字节(一个word双字节,double word四个字节)
此时执行这一行代码:
先看反汇编和监视和内存中从ebp-0E4h开始的内存中的样子,接下来按“F10”
此时内存中:
一直到ebp为止所有变为 cc cc cc cc
画图表示:
即以下反汇编代码为将main函数即将调用的空间全部初始化为CCCCCCCC
此时:为main函数栈帧的开辟已经准备完全了
此时我们设一个红色方格为四个字节,地址为ebp-4,ebp-8,,,
画图表示:
接下来执行反汇编代码:
mov dword ptr [ebp-8],0Ah
这句代码的含义为:将0Ah这个值放到ebp-8这个空间中
画图表示:
延伸一下:我们有时候写的错误代码,我们在输出时,有时候会打印出 “烫烫烫烫烫烫烫烫”那么我们可以找到原因:我们只是定义了这个变量,而没有给这个变量赋值,或者在后续中没有给这个变量进行运行赋值,也可以反推,即出现“烫烫烫烫烫烫烫烫””那么就是我们的赋值出现问题。
结合反汇编和内存看:上图没有执行“int a = 10;”这行代码,内存中没有任何变化,还是CCCCCCCC
那么我们执行这行代码:
可以看出0x010FFC68这行内存中被赋值 0a 00 00 00
同理:
b和c的初始化为:
画图表示:
接下来执行:
第一行:
即:将[ebp-14h]中的值放到eax中,也就是说将b中的值放到eax中去
接下来:push eax,也就是压栈,将eax中的值放到栈顶,然后将esp放到栈顶
此时我们没有执行Add这个函数的时候,我们先看一下,栈顶是怎样的?
然后我们执行:
上述过程也就是我们所说的传参。
接下来:
延伸:call 调用,即:调用Add这个函数,然后将call这条指令下面这行反汇编代码中的地址压栈
此时我们按F11
反汇编代码跳转到左图所示,内存中我们可以看到20,10上面紧挨着的地址中放的是我们所说的地址
画图表示:
为什么这么做呢?(call)
call接下来就会去调用别的函数,并且去往别的函数中去执行代码,我们需要记住这个地址才能找到“回家的路”
此时我们按F11,我们便会进入Add这个函数中去
此时我们执行以下反汇编代码:因为前面有相似的所以我们省略为主:
接下来执行:
反汇编翻译一下:
将ebp+8 中的值放到eax中去
将ebp+0Ch中的值加到eax中去
将eax中的值放到ebp-8中去
我们对于上面的ebp+8,ebp+0Ch,,挺迷惑的,我们可以画图看看其位置在哪?
接下来执行:(将[ebp-8]的值放到eax中去)
接下来执行:
pop出栈,即:将esp的值指向的释放,也就是指向下一行反汇编的代码中去
将ebp中的内容放到esp中去,此时esp和ebp指向同一块位置
接下来执行:
pop一下ebp,我们即:ebp指向了我们记录的那个地址:
然后ebp根据现在指向的内容找到了它最开始的位置,esp也pop了一下指向原来的位置:
接下来执行 ret 指令:
这个指令将 语句指令 返回到原来call下面的反汇编代码中去,此时我们按F10就会返回到我们call下面的反汇编代码中
加下来执行上图这一行代码,我们给esp加8,也就是释放了eax和ecx
接下来执行下一行反汇编,将eax中的值放到[ebp-20h]也就是我们创建main函数时,定义c变量的空间中去
接下来我们回答最开始的问题?
局部变量是怎样创建的?
答:首先我们为函数创建栈帧空间,栈帧空间我们初始化一部分空间,然后再给这个局部变量分配空间
为什么局部变量的值是随机值?
答:因为随机值是我们放进去的(CCCCCCCC)
函数是怎么传参的?传参的顺序是怎样的?
答:当我们调用这个函数的时候我们就已经将参数从右向左依次压栈,当我们真正进入这个形参函数时,在Add这个函数的函数栈帧中通过指针的偏移量找到这两个形参
形参和实参是什么关系?
形参是我们在压栈的时候开辟的一份空间,他和我们的实参只是数值上相同,空间是独立的,即:形参是实参的一份临时拷贝,改变形参不会改变实参。
函数调用是怎么做的?
详情请看上面。
函数调用结束后怎么返回的?
call指令执行的时候,我们会压栈一份call指令原来的地址的下一个地址,当我们需要返回的时候,我们pop ebp,ebp就会指向一个地址,然后ebp就会返回到原来的位置。