一篇文章带你充分了解函数栈帧

目录

1.相关寄存器和汇编指令

1.1相关寄存器

1.2相关汇编命令

2.函数栈帧的创建和销毁

 2.1函数栈帧的创建

 2.2函数栈帧的销毁


1.相关寄存器和汇编指令

1.1相关寄存器

eax:通用寄存器,保留临时数据,常用于返回值。

ebx:通用寄存器,保留临时数

ebp:栈底寄存器。

esp:栈顶寄存器。

eip:指令寄存器,保存当前指令的下一条指令的。

1.2相关汇编命令

mov:数据转移指令。

push:数据入栈,同时esp栈顶寄存器也要发生改变。

pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变。

sub:减法命令。

add:加法命令。

call:函数调用,1. 压入返回地址2.转入目标函数。

jump:通过修改eip,转入目标函数,进行调用。

ret:恢复返回地址,压入eip,类似popeip命令。

2.函数栈帧的创建和销毁

首先我们要知道,每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。

这块空间的维护是使用了2个寄存器:esp和ebp,ebp和esp存放的是地址,用来维护函数栈帧的

如下图所示:

一篇文章带你充分了解函数栈帧_第1张图片

函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,本次演示以VS2019为例。

演示代码:

#include 

int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}

int main()
{
	int a = 3;
	int b = 5;
	int ret = 0;
	ret = Add(a, b);
	printf("%d\n", ret);
	return 0;
}

那接下来我们从main函数的栈帧创建开始讲解:

调试到main函数开始执行的第一行,右击鼠标转到反汇编。

一篇文章带你充分了解函数栈帧_第2张图片

 2.1函数栈帧的创建

我们接下来一行行解析汇编代码

在main函数之前也是有函数去调用main函数的,这个函数可能根据编译器而定,这里不细说明。

(1)00BE1820    push    ebp

push ebp就是把ebp的值放到函数顶,也就是压栈,ebp里面的值就是调用main函数的函数的ebp的值,而esp就指向了ebp的顶部。

(2)00BE1821    mov     ebp  ,  esp

move指令会将esp的值存放到ebp中,就相当于ebp和esp指向同一个地址。

(3)00BE1823    sub      esp  ,  0E4h

sub指令会让esp的地址减去逗号后面的0E4h(16进制),产生一个新的esp,然后新的esp就指向了之前操作的地址,现在ebp和esp之间的空间就是为了main函数开辟的,也就是main函数的栈帧空间。

(4)00BE1829    push    ebx

同(1) 将ebx的值进行压栈,ebx - 4.

(5)00BE182A    push    esi

同(1)将esi的值进行压栈,esi - 4.

(6)00BE182B    push    edi

同(1)将edi的值进行压栈,dei - 4.

(7)00BE182C    lea    edi  ,  [ebp-24h]

将 edi - 24的值放到 lea 中。

(8)00BE182F    mov    ecx  ,  9

将9放到 ecx 中

(9)00BE1834    mov    eax  ,  0CCCCCCCCh

将0CCCCCCCCh放到 eax 中

(10)00BE1839    rep  stos    dword  ptr  es : [edi]

从edi(dbp - 24h)到ebp之间所有的元素赋值为0CCCCCCCCh,也就是将为main函数开辟的空间全部赋值为0CCCCCCCCh。

(11)int a = 3;

           00BE183B    movd    word  ptr  [ebp - 8]  ,  3

将3储存到 ebp - 8 的地址处,也就是存放到变量a的空间里。

(12)int b = 5;

            00BE1842    movd    word  ptr  [ebp - 14h]  , 5

将5储存到 ebp - 14h的地址处,也就是存放到变量b的空间里。

(13)int ret = 0;

           00BE1849    movd    word   ptr  [ebp - 20h]  ,  0

将0储存到 ebp - 20h的地址处,也就是存放到变量ret的空间里。

(11)~ (13)的代码其实就是为变量 a , b , ret 做初始化,也就是说局部变量是在函数的栈帧空间里创建的。

(14)ret = Add(a, b);

调用Add函数,并且进行传参。

(15)00BE1850    mov     eax  ,  dword   ptr  [ebp - 14h]

将 ebp - 14h 地址中的数据存放到 eax 中,也就是传递b的值。

(16)00BE1853    push     eax

对 eax 进行压栈,eax - 4.

(17)00BE1854    mov      ecx  ,  dword   ptr  [ebp - 8]

将 ebp - 8 地址中的数据存放到 ecx 寄存器中,也就是传递a的值。

(18)00BE1857    push     ecx

对 ecx 进行压栈,ecx - 4.

(19)00BE1858    call       00BE10B4

调用函数 地址为  00BE10B4。

(20)00BE185D    add      esp , 8

call指令的下一条地址

(21)00BE1860     mov    dword   ptr  [ebp - 20h]  ,  eax

 call指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。

接下来嗯f11就进入Add函数里面去了

 代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。

在Add函数中创建栈帧的方法和在main函数中是相似的,只是在栈帧空间的大小上可能略有差异。

一篇文章带你充分了解函数栈帧_第3张图片

一篇文章带你充分了解函数栈帧_第4张图片

图片中的a'和b'其实就是Add函数的形参x,y。这里的分析很好的说明了函数的传参过程,以及函数在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。

 2.2函数栈帧的销毁

当函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。

那具体是怎么销毁的呢?我们看一下反汇编代码。

一篇文章带你充分了解函数栈帧_第5张图片

(1)00BE177F    pop    edi 

在栈顶弹出一个值存放到 edi 中,esp+4。

(2)00BE1780    pop    esi

同(1),在栈顶弹出一个值存放到 esi 中,esp+4。

(3)00BE1781    pop    ebx

同(1)在栈顶弹出一个值存放到ebx中,esp+4。

(4)00BE1782    mov    esp  ,  ebp

再将 Add 函数的 ebp 的值赋值给 esp ,相当于回收了 Add 函数的栈帧空间

(5)00BE1784    pop    ebp

弹出栈顶的值存放到 ebp ,栈顶此时的值恰好就是main函数的 ebp ,esp+4,此时恢复了main函数的栈帧维护,esp 指向main函数栈帧的栈顶,ebp 指向了main函数栈帧的栈底。

(6)00BE1785    ret

ret 指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是 call 指令下一条指令的地址,此时esp+4,然后直接跳转到 call 指令下一条指令的地址处,继续往下执

回到了call指令的下一条指令的地方: 

一篇文章带你充分了解函数栈帧_第6张图片

但调用完Add函数,回到main函数的时候,继续往下执行,可以看到:

一篇文章带你充分了解函数栈帧_第7张图片

到这里就给大家完整的演示了main函数栈帧的创建,Add函数栈帧的创建和销毁的过程 。

你可能感兴趣的:(c语言)