楔子
- 什么是栈
栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)
- 什么是栈帧
从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息
- 寄存器
首先寄存器的功能是存储二进制代码,我们现在先认识这几个寄存器:
寄存器 | 功能 |
---|---|
eax | 通用寄存器,保留临时数据,常用于返回值 |
ebx | 通用寄存器,保留临时数据 |
ebp | 栈底寄存器 |
esp | 栈顶寄存器 |
eip | 指令寄存器,保存当前指令的下一条指令的地址 |
- 一些汇编语言
名称 | 作用 |
---|---|
mov | 数据转移指令 |
push | 数据入栈,同时esp栈顶寄存器也要发生改变 |
pop | 数据弹出至指定位置,同时esp栈顶寄存器也要发生改变 |
sub | 减法命令 |
add | 加法命令 |
call | 函数调用,1. 压入返回地址 2. 转入目标函数 |
jump | 通过修改eip,转入目标函数,进行调用 |
ret | 恢复返回地址,压入eip,类似pop eip命令 |
调用main函数
- 代码如下:
int MyAdd(int a, int b)
{
int c = 0;
c = a + b;
return c;
}
int main()
{
int x = 0xA;
int y = 0xB;
int z = 0;
z = MyAdd(x, y);
printf("%d\n", z);
return 0;
}
- 今天我们要了解这张图的栈区:
- 现在我们大体的框架已经构建好了,接下来就是main函数栈帧的形成
-
绿色部分
将ebp压栈->把esp中的值赋给ebp,此时ebp指向main函数的栈底->esp的值减去0E4h(由于esp的地址高,所以是在ebp上方开辟了大小为0E4h的空间),此时main函数的栈帧形成(由esp和ebp共同维护) -
蓝色部分
ebx,esi,edi按次序入栈 -
紫色部分
注:区分一下mov和lea
lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。而mov指令则恰恰相反,例如:mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。
把[ebp-24h]的值直接赋给edi->把9存到ecx中->把0CCCCCCCCCh存到eax中->rep stos作用是重复拷贝,dword 双字 ,就是四个字节ptr , pointer缩写 即指针,[]里的数据是一个地址值,这个地址指向一个双字型数据,连起来就把mian栈帧里面的内容全部初始化为0CCCCCCCh->把内存地址为5CC00Fh的数据赋给ecx->调用005C1320的函数
形成临时变量
-
粉色部分
分别把x,y,z的值放入[ebp-8],[ebp-14h],[ebp-20h]中 -
灰色部分
把[ebp-14h]的值放入eax,然后压栈,后面同理,
这里我们可以得到一个结论:函数形参是在函数调用前形成的,先y后x,由此可见,形参实例化时从左向右的。
注:
由于x和y是相差4个字节,我们可以直接通过指针访问b来修改b的值
调用函数
call命令的作用
将当前指令的下一条指令入栈(push到栈中,影响esp)
跳转到目标函数地址运行(修改eip,到达目标函数)
形成Add函数栈帧
-
红色部分
直接把ebp的值压栈->把esp的值赋给ebp->esp减去0CCh->此时esp和ebp共同维护add的栈帧 -
蓝色部分
把Add函数栈帧初始化 -
绿色部分
[ebp + 8]和[ebp + 0Ch]分别是形参a,b;然后直接通过寄存器把[ebp-8]的值(也就是c的值)修改为21;然后把c的值放入eax寄存器中
释放add栈帧
将edi,esi,ebx,pop也就是删除->esp的值加上0CCh此时esp和ebp指向同一位置(释放”栈帧“)->把ebp删除(ebp向下访问4个字节,就是main函数的栈底,此时ebp又恢复原来的指向)->esp指向内容为add地址处->ret就是把eip中的值修改为add的地址,然后返回到add处
释放main栈帧
esp加8就是让esp向高地址指向,就是把形参释放->然后把eax的值放到[ebp - 20h]也就是z,这样就完成带回返回值