欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

函数的调用过程(栈帧)

程序员文章站 2022-06-14 22:29:50
...

函数是我们C语言中经常接触到的一个内容。我们的编程和代码都会有函数。那么函数的调用有是怎样的一个过程呢?这个过程在内存中又是如何来实现呢?这些问题的答案就是函数的调用,这个调用的过程中要为函数开辟栈空间,这块空间就是函数栈帧。

我在学习了函数的调用和栈帧之后,深感这一知识的重要和理解的困难,所以写了这篇关于函数栈帧的博客。其中有理解的不清楚或不对的地方也请大家指正。

以下是我这次理解函数栈帧时所用的代码,是一个十分简单的ADD加法函数。

(注意,我用的编程工具是Visual Studio 2013)

//ADD函数的栈帧


#include <stdio.h>
int ADD(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 2;
	int b = 3;
	int ret;
	ret = ADD(a, b);
	printf("%d\n", ret);
	return 0;
}

将以上代码进行调试,“调试--窗口--反汇编”进入反汇编窗口,则界面如下

函数的调用过程(栈帧)

上面这张图片中的11--12行之间的反汇编代码的作用是为main函数做准备工作。

1.

“00901410  push        ebp ”:压栈ebp,ebp存放了指向函数栈帧栈底的地址。

2.

“00901411  mov         ebp,esp ”:将esp的值赋给了ebp,产生了新的ebp。

3.

“00901413  sub         esp,0E4h ”;将esp减去一个16进制的数字,产生了新的esp。(esp为存放了指向函数栈帧栈顶的地址。)

(注意:栈顶的地址经常会发生变化,这是因为函数的栈帧就是经常变化的。而栈帧在开辟新空间时是在栈顶进行的,所以此时esp也会随之变化。)

4.

“00901419  push        ebx “ 
”0090141A  push        esi“              :这三条代码指令将ebx,esi,edi压栈。(注意:此时随着它们的压入,esp也要随之上移
”0090141B  push        edi”

5.

 ”0090141C  lea         edi,[ebp-0E4h]  ”        :加载有效地址。
 “0901422  mov         ecx,39h  “                     :给ecx赋值。
 “00901427  mov         eax,0CCCCCCCCh”:给eax赋值。
 ”0090142C  rep stos    dword ptr es:[edi]“   :进行重复存储,存储内容为:“ecx”个“eax”。

(以上这四条指令的实质为:把栈帧开辟的空间全部初始化为“oxcccccccc”)

只靠文字的描述可能并不直观,下面我用图形的方式再次进行理解。

函数的调用过程(栈帧)

接下来,我们直接跳到调用ADD函数时的反汇编代码

函数的调用过程(栈帧)

这张图片的16--17行之间的反汇编代码就是调用ADD函数的指令代码,我们来分析一下它们:

1.

“0090143C  mov         eax,dword ptr [b]”  
”0090143F  push        eax“                         :将要传递给ADD函数的参数“b”赋值给寄存器eax,压入eax。

2.

"00901440  mov         ecx,dword ptr [a]"  
"00901443  push        ecx  "                       :将要传递给ADD的参数“a”赋值给寄存器ecx,压入ecx。

3.

“00901444  call        _ADD (0901127h)”  :call指令的调用,要先进行压栈。

4.

"00901449  add         esp,8 "        :这里得到了call指令下一条指令的地址,然后跳转到ADD函数的地址。(注意:这个地址十分重要,有了这个地址,在函数调用结束后才可以返回到这里继续执行指令。) 

5.

“0090144C  mov         dword ptr [ret],eax”    :将ADD函数的返回值赋给ret。

我们在上面已经知道了函数的初始化和函数中再调用一个函数的过程是如何进行的,接下来再让我们看一下函数的返回过程:

 函数的调用过程(栈帧)

这张图片的第9行后面就是函数的返回部分的反汇编代码,我们来分析一下:

1.

“009013EE  mov         eax,dword ptr [z]”    :将函数的返回值z赋值给寄存器eax。

2.

“009013F1  pop         edi“
“009013F2  pop         esi”       :将edi,esi,ebx逐次进行出栈。(注意:此时栈顶变化,esp也要随之发生变化。)
”009013F3  pop         ebx“  

3.

”009013F4  mov         esp,ebp“   :将ebp赋值给esp。

(注意:此时esp,ebp指向同一地址,所以此时栈帧无空间。)

4.

”009013F6  pop         ebp“  :出栈,将出栈的内容保存到ebp回到main函数的栈帧。

5.

“009013F7  ret”          :此指令会使得出栈一次,并将出栈的内容当作地址,将程序执行跳转到该地址处。
                       

下面,用图形来模拟这一过程:

函数的调用过程(栈帧)

以上就是函数的调用过程(栈帧)。

有什么缺失或者理解的不正确的地方欢迎大家和我交流。