文章目录
前言
栈是函数执行的内存区域,是C语言运行时最重要的元素之一
一、寄存器
1.寄存器是什么?
寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。
2.寄存器的类型
寄存器有eax,ebx,ecx,edx,还有ebp,esp。本文主要介绍最后两个,由于寄存器不是本次博客的重点,其他请自行了解!
| ESP | 栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶 |
|---|---|
| EBP | 基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部 |
想要了解函数栈帧,就必须先了解好这两个地址。
ebp,esp这两个寄存器中存放的是地址,且这两个地址是用来维护函数栈帧的
二、栈
1.栈区是什么
栈区:
用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行。
2.栈区的常见操作
栈的最常见操作有两种:
压栈(push)
弹栈(pop)
用于标识栈的属性也有两个:
栈顶(top)
栈底( base)。
什么意思?
可以把栈想象成一摞扑克牌。
| push | 为栈增加一个元素的操作叫做推,相当于在这摞扑克牌的最上面再放上一张。 |
|---|---|
| pop | 从栈中取出一个元素的操作叫做流行,相当于从这摞扑克牌取出最上面的一张。 |
| pop | 从栈中取出一个元素的操作叫做流行,相当于从这摞扑克牌取出最上面的一张。 |
|---|---|
| base | 标识栈底位置,它记录着扑克牌最下面一张的位置。base用于防止栈空后继续弹栈(牌发完时就不能再去揭了)。很明显,一般情况下,base是不会变动的。 |
三、函数栈帧
1.函数调用时发生了什么?
以下面代码为例:
#define _CRT_SECURE_NO_WARNINGS 1
#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\n", c);
return 0;
}
当中央处理器在执行调用Add函数的时候,会从代码区中
主要函数对应的机器指令的区域跳转到Add函数对应的机器指令区域,在那里取指并执行;当Add函数执行完闭,需要返回的时候,又会跳回到主要函数对应的指令区域,紧接着调用Add后面的指令继续执行主要函数的代码。
如下图:
那么中央处理器是怎么知道要去Add函数的代码区取指,
在执行完Add后又是怎么知道跳回到主要函数(而不是其他未知的代码区)的呢?
这是因为
当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。这个栈帧中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,系统栈会弹出该函数所对应的栈帧。
四、寄存器与函数栈帧
每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。
函数栈帧:
esp和ebp之间的内存空间为当前栈帧,esp标识了当前栈帧的顶部,ebp标识了当前栈帧的底部。
函数调用大致包括以下几个步骤。
(1)参数入栈:将参数从右向左依次压入系统栈中。 (2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
(3)代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。
(4)栈帧调整:具体包括。保存当前栈帧状态值,已备后面恢复本栈帧时使用(ebp入栈);将当前栈帧切换到新栈帧(将esp值装入ebp,更新栈帧底部);给新栈帧分配空间(把esp减去所需空间的大小,抬高栈顶);
将上面的代码转到反汇编模式查看
对应观察:
1.函数返回过程
(1)保存返回值:通常将函数的返回值保存在寄存器EAX中。
(2)弹出当前栈帧,恢复上一个栈帧。
1.在堆栈平衡的基础上,给esp加上栈帧的大小,降低栈顶,回收当前栈帧的空间。
2.将当前栈帧底部保存的前栈帧ebp值弹入ebp寄存器,
复出上一个栈帧。
3.将函数返回地址弹给ebi寄存器。
(3)跳转:按照函数返回地址跳回母函数中继续执行。
最后
本文内容来自于《oday安全软件漏洞分析技术》第2版与自己学习笔记所,本文仅记述自己学习历程与仅供自己学习,复习用,不做任何商业用途。
若有兴趣深入了解函数栈帧,请自行查阅王清主编的《Oday安全:软件漏洞分析技术(第2版)》一书。