目录
前言
我们学习语法学习编程逻辑都是基于封装好的知识上来进行学习,知其然而不知其所以然,如果想要更好的掌握理解所学知识,我们对知识应该有一个更深层次理解,函数怎样传参,调用后怎么返回? 局部变量怎样创建 ?这里就需要我们去了解函数栈帧的创建与销毁;
在此之前我们先了解一下寄存器和部分汇编指令的概念。
寄存器
寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。其实寄存器就是一种常用的时序逻辑电路,但这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器或触发器构成的,因为一个锁存器或触发器能存储1位二进制数,所以由N个锁存器或触发器可以构成N位寄存器。寄存器是中央处理器内的组成部分。寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和位址。
1. 寄存器的种类与功能
种类 | 功能 |
EAX | 累加器(加法乘法指令) |
EBX | 基址寄存器(在内存寻址时存放基地址) |
ECX | 计数器(在循环和串操作中充当计数器) |
EDX | 数据寄存器(在I/O指令中可用作端口地址寄存器,乘除指令中用作辅助累加器) |
EBP | 寄存器存放当前线程的栈底指针 |
ESP | 寄存器存放当前线程的栈顶指针 |
ESI | 源变址寄存器 |
EDI | 目的变址寄存器 |
C语言汇编指令介绍
汇编指令 | 功能 |
push | 压栈操作 |
mov | 指令将源操作数复制到目的操作数 |
sub | 两个操作数的相减,即从A中减去B,其结果放在A中 |
lea | 将存储器操作数mem的4位16进制偏移地址送到指定的寄存器 |
pop | 将操作对象弹出栈帧,出栈 |
call | 调用函数,调用前会将call下一条语句的地址压栈在栈顶 |
ret | 将栈顶的地址弹出并返回到该地址的地方 |
函数栈帧的创建与销毁过程
ps:每个编译器上的展示可能略有差异,但是大体逻辑是一样的,版本越低的编译器越好观察,编译器越高级越不容易观看整个创建与销毁的过程,因为它的封装过程会复杂一些;这里我用vs2013为大家演示一下。
为了演示函数栈帧的创建与销毁,我们用下面这段简单的代码来观察:
#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 c = 0; c = Add(a, b); printf("%d", c); return 0; }
1.函数栈帧的概念
函数栈帧:每一个函数调用的时候都需要在栈区上开辟一块内存空间.栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的 一种数据结构。
函数的压栈出栈:
push压栈: 每当有一个新的函数被调用的时候,在内存中就会在上一个函数的顶上创建新的函数栈帧,这个就叫压栈,(给栈顶放一个元素)
pop出栈:当函数内代码执行结束时,函数栈帧就会重新归还内存,这叫出栈。(从栈顶删除一个元素)
对于栈,我们都知道栈是由高地址向低地址延伸的,每个函数的每次调用,都有它自己独立的一个栈帧,一个函数栈帧的创建就需要esp和ebp这两个寄存器的维护,ebp存放它的高地址,esp存放它的低地址,这两个地址之间的内存就形成了当前函数的函数栈帧,每当需要调用一个新的函数esp和ebp就会去维护那个新的函数栈帧,两个寄存器里面就会存放两个新的地址.
这样我们就了解了寄存器ebp和寄存器esp中存放的是地址,这两个地址是用来维护函数栈帧的。比如:我们常说调用main函数, 那么我们为main函数分配栈帧空间, 那么栈帧维护如下:
2.函数栈帧的创建与销毁
main函数的创建
我们打开调试,选中调用堆栈窗口可以看到,main函数是在__tmainCRTStartup函数内部被调用的,而__tmainCRTStartup函数又是在mainCRTStartup函数内部调用的
为了能更加直观看到栈帧创建和销毁的过程,我们转到上面代码对应的反汇编代码:
这里就能看出main函数函数栈帧的创建过程
第一步是进行push命令,将ebp压入栈顶 将ebp压入栈顶后,esp的地址也会随之改变。
第二步进行mov指令,将esp的值给ebp
第三步进行sub指令,将esp指向的地址减去0E4h(十进制228)
随后分别把ebx,esi,edi压入栈顶
随后四句指令main函数栈帧初始化 ,把从edi(ebp-0E4h)开始的ecx(39h)个 空间初始化为eax(0CCCCCCCCh)。
执行main函数内部代码
Add函数函数栈帧的创建
当变量abc创建好了之后,就要开始调用add函数。
随后分别将ebx(20)和ecx(10)压入栈顶。
这两个指令实际上是在为Add函数传参
进入Add函数后,前面的几条指令跟进入main之前的push mov sub 等指令一样,是为了给函数准备栈帧和对其进行初始化
Add函数栈帧的销毁
第一步edi出栈
第二步esi出栈
第三步ebx出栈
随后将ebp赋给esp
再将以前存着main函数ebp地址的内存弹出到现在的ebp内,使ebp回到main的顶部,系统中也根据刚刚在内存中的存储,使esp再向上走,找到main的底部,此时再通过ret使刚刚调用分支函数和参数拷贝的内存释放,而编译器也会通过call指令的下一条指令的地址回到main函数的原来位置。此时esp向高地址移动8字节,esp,ebp重新维护main函数,eax中存放的返回值将被传递给地址(ebp - 20h)即ret的地址。至此,Add函数返回完毕。main函数栈帧销毁过程与前述过程类似
后记
问题与收获
1.局部变量是怎么创建的?
函数栈顿创建后编译器分配由高到低地址创建变量
2.为什么局部变量不初始化的值是随机值?
函数栈顿创建后会默认将所有内容初始化为0cccccccch:
3.函数是怎么传参的? 传参的顺序是怎么样的?
传参参是将实参值拷贝后进行压栈在栈顶,顺序是由右到左
4.形参和实参是什么关系?
形参是实参的临时拷贝,只是值相同却是不同的地址:
5.函数调用是怎么做的?
call;
6.函数调用结束后怎么返回的?
ret;
好了,本篇学习就到此为止了,如有问题欢迎指正,感谢各位佬的支持!