C语言中函数栈帧的创建和销毁的深层分析

目录
  • 一、本文目标
  • 二、基础知识
    • 1、寄存器
    • 2、代码案例
    • 3、总体栈帧概况
    • 4、所需反汇编代码总览
  • 三、函数栈帧创建销毁过程
    • 1、_tmainCRTStartup函数(调用main函数)栈帧的创建
    • 2、main函数栈帧的创建
    • 3、main函数内执行有效代码(变量)
    • 4、Add函数栈帧的创建
    • 5、Add函数内执行有效代码
    • 6、Add函数栈帧的销毁
    • 7、main函数栈帧的销毁
  • 四、总结

一、本文目标

1、局部变量是怎么创建的?

2、为什么局部变量的值是随机值?

3、函数是怎么传参的?传参的顺序是怎样的?

4、形参和实参是什么关系?

5、函数调用是怎么做的?

6、函数调用结束后是怎么返回的?

当我们深入理解函数栈帧创建和销毁,答案自然就清楚了。正文开始:

二、基础知识

1、寄存器

寄存器名称 简介
eax "累加器"  它是很多加法乘法指令的缺省寄存器。
ebx "基地址"寄存器, 在内存寻址时存放基地址。
ecx 计数器,是重复(REP)前缀指令和LOOP指令的内定计数器。
edx 总是被用来放整数除法产生的余数。
esi 源索引寄存器
edi 目标索引寄存器
ebp (栈底指针)"基址指针",存放的是地址,用来维护函数栈帧
esp (栈顶指针)专门用作堆栈指针,存放的是地址,用来维护函数栈帧

2、代码案例

本文依赖的编译器: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\n", c);
	return 0;
}

3、总体栈帧概况

每一个函数调用都要在栈区为它开辟空间,像上述的代码中,有肉眼可见的main函数和Add函数,相应的需要为它俩开辟空间,但其实main函数也是被调用的,当我们针对上述代码按下F10,按到return 0时再按一次,就会跳出以下界面

由图得知,main函数是被__tmainCRTStartup函数调用的,而 __tmainCRTStartup又是被mainCRTStartup调用的。先看下总体函数栈帧开辟情况:

两个重要知识点:

  • 压栈(push):给栈顶放一个元素
  • 出栈(pop)  :从栈顶删除一个元素

接下来会详细讲解下函数栈帧的开辟情况:

4、所需反汇编代码总览

int main()
{
00031410  push        ebp  
00031411  mov         ebp,esp  
00031413  sub         esp,0E4h  
00031419  push        ebx  
0003141A  push        esi  
0003141B  push        edi  
0003141C  lea         edi,[ebp+FFFFFF1Ch]  
00031422  mov         ecx,39h  
00031427  mov         eax,0CCCCCCCCh  
0003142C  rep stos    dword ptr es:[edi]  
    int a = 10;
0003142E  mov         dword ptr [ebp-8],0Ah  
    int b = 20;
00031435  mov         dword ptr [ebp-14h],14h  
    int c = 0;
0003143C  mov         dword ptr [ebp-20h],0
 
    c=Add(a,b);  
00031443  mov         eax,dword ptr [ebp-14h]  
00031446  push        eax  
00031447  mov         ecx,dword ptr [ebp-8]  
0003144A  push        ecx  
0003144B  call        00C210E1  
00031440  add         esp,8  
00031443  mov         dword ptr [ebp-20h],eax  
    printf("%d", c);
00241456  mov         esi,esp  
00241458  mov         eax,dword ptr [ebp-20h]  
0024145B  push        eax  
0024145C  push        245858h  
00241461  call        dword ptr ds:[00249114h]  
00241467  add         esp,8  
0024146A  cmp         esi,esp  
0024146C  call        0024113B  
    return 0;
00241471  xor         eax,eax  
}
00031451  pop         edi  
00031452  pop         esi  
00031453  pop         ebx  
00031454  add         esp,0E4h  
0003145A  cmp         ebp,esp  
0003145C  call        __RTC_CheckEsp (03113Bh)  
00031461  mov         esp,ebp  
00031463  pop         ebp  
00031464  ret

int Add(int x, int y)
{
000313C0  push        ebp  
000313C1  mov         ebp,esp  
000313C3  sub         esp,0CCh  
000313C9  push        ebx  
000313CA  push        esi  
000313CB  push        edi  
000313CC  lea         edi,[ebp-0CCh]  
000313D2  mov         ecx,33h  
000313D7  mov         eax,0CCCCCCCCh  
000313DC  rep stos    dword ptr es:[edi]  
    int z = 0;
000313DE  mov         dword ptr [ebp-8],0  
    z = x + y;
000313E5  mov         eax,dword ptr [ebp+8]  
000313E8  add         eax,dword ptr [ebp+0ch]  
000313EB  mov         dword ptr [ebp-8],eax  
    return z;
000313EE  mov         eax,dword ptr [ebp-8]  
}
000313F1  pop         edi  
000313F2  pop         esi  
000313F3  pop         ebx  
000313F4  mov         esp,ebp  
000313F6  pop         ebp  
000313F7  ret

三、函数栈帧创建销毁过程

1、_tmainCRTStartup函数(调用main函数)栈帧的创建

根据上文,我们已经知晓main函数是被_tmainCRTStartup函数所调用的,自然要为它开辟栈帧,这块空间应该由ebp和sep俩寄存器来维护,前提是下面高地址,上面低地址。如图:

此时进入main函数,首先要push进行压栈:

push ebp就是把ebp压到栈顶上,此时sep相应的移动到新栈顶上,可以通过监视来验证:

图示如下:

接下来执行mov操作:

此行代码意思就是把sep赋给ebp,所以ebp指向的位置即为sep所指向的位置,但是源操作地址位置不变,可通过监视来验证

接着执行sub操作:

该操作就是给esp减去个0E4h ,此时esp的位置就要往上面去,通过监视观察:

此时此刻执行完sub操作,其实就已经进入到下文的main函数栈帧的开辟,至此_tmainCRTStartup函数栈帧的开辟已完成。图示见下文:

2、main函数栈帧的创建

接上文,图示如下:

接下来进行三次push操作: 把ebx、sei、edi顺次压栈压进去,相应的esp也要往上走。

通过监视看看:

图示如下:

接下来执行下列三个步骤

操作lea(load effecitve address)加载有效地址。就是相当于把[ebp+FFFFFF1Ch]放到edi里头,显示符号名后[ebp+FFFFFF1Ch]就是[ebp-0E4h],前面已经执行过-0E4h,这里再执行一次放到edi里头去。接着mov把39h放到ecx里头去,再mov此时eax放的就是0CCCCCCCCh

上述操作执行后的目的就是从刚才的edi开始向下的39h次这么多个dword(1个word2字节,2dword4个字节)全部改为0CCCCCCCCh

通过监视看下:

图示如下:

至此,main栈帧的开辟已经完成,接下来就要执行正式有效代码,见下文:

3、main函数内执行有效代码(变量)

接下来执行以下操作:

先mov把0Ah(10)放到ebp-8的位置上,同理把14h(20)放到ebp-14h上,把0放到ebp-20h上,如图:

此时此刻a、b、c这三个变量均已创建完成,接下来进行Add函数调用:先进行传参

首先,mov把ebp-14h(b=20)放到eax里头。接下来再push, 压栈把eax(20)放到栈顶,相应esp也要移动,同理mov把ebp-8(a=10)放到ecx里头,再push把ecx放到栈顶。如图所示:

接着执行call操作,调用Add函数,按F10执行到call时,按下F11,此时就跳到Add函数内部并且把call指令的下一条指令的地址压到栈顶。这么做的目的是在接下来跳到Add函数里去回来时方便回到该地址,如图:

按下F11,此时就正式进入Add函数内部 并为其开辟栈帧,详情见下文:

4、Add函数栈帧的创建

int Add(int x, int y)
{
000313C0  push        ebp  
000313C1  mov         ebp,esp  
000313C3  sub         esp,0CCh  
000313C9  push        ebx  
000313CA  push        esi  
000313CB  push        edi  
000313CC  lea         edi,[ebp-0CCh]  
000313D2  mov         ecx,33h  
000313D7  mov         eax,0CCCCCCCCh  
000313DC  rep stos    dword ptr es:[edi]

而前面这些操作跟先前main函数内部操作一样,其实就是在为Add函数准备我们的栈帧

首先,push ebp把ebp压栈到栈顶,再mov把esp赋给ebp,再sub,把esp-去0CCh,此步骤就是在为Add函数开辟空间,接着进行三次push,同main函数那样,同理,依旧是初始化成CCCCCCCC,详细过程不再赘述,跟上文main函数一样,如图所示:

至此,Add栈帧的开辟已基本完成,接下来就要执行正式有效代码,见下文:

5、Add函数内执行有效代码

接上文:

int z = 0;
000313DE  mov         dword ptr [ebp-8],0  
    z = x + y;
000313E5  mov         eax,dword ptr [ebp+8]  
000313E8  add         eax,dword ptr [ebp+0ch]  
000313EB  mov         dword ptr [ebp-8],eax  
    return z;
000313EE  mov         eax,dword ptr [ebp-8]  
}

首先,把0放到ebp-8的位置上,接着mov把ebp+8的值放到eax里头去,此时eax就是10。再add给eax加上ebp+0ch,就是把20加进去,此时eax就是30,加完后再把eax(30)放到ebp-8里头去,最终的结果(30)放到z里头去。

此时Add函数内部有效代码执行完毕,见图:

接下来就要进行返回了,也就是Add函数栈帧的销毁,见下文:

6、Add函数栈帧的销毁

return z;
000313EE  mov         eax,dword ptr [ebp-8]  
}
000313F1  pop         edi  
000313F2  pop         esi  
000313F3  pop         ebx  
000313F4  mov         esp,ebp  
000313F6  pop         ebp  
000313F7  ret

上文已经知道此时已经把ebp-8的值(30)放到eax里头去,接下来执行三次pop,一次弹出,esp就会加加一次,如图:

接着,把ebp赋给esp,再pop把ebp弹出,此时esp也要移动,此时esp和ebp又回到了先前维护main函数栈帧的样子。如图所示:

此时esp指向的就是call指令的下一条指令的地址,再按一次F10,此时反汇编就会这样:

0003144B  call        00C210E1  
00031440  add         esp,8  
00031443  mov         dword ptr [ebp-20h],eax  
    printf("%d", c);
00241456  mov         esi,esp  
00241458  mov         eax,dword ptr [ebp-20h]  
0024145B  push        eax  
0024145C  push        245858h  
00241461  call        dword ptr ds:[00249114h]  
00241467  add         esp,8  
0024146A  cmp         esi,esp  
0024146C  call        0024113B  
    return 0;
00241471  xor         eax,eax  
}

此时我们就会明白先前存放call指令的下一条指令的地址就是为了方便回来,先前ret执行后esp的位置发生变化:

此时Add函数的栈帧算是真正销毁,接下来进行main函数栈帧的销毁 。

7、main函数栈帧的销毁

0003144B  call        00C210E1  
00031440  add         esp,8  
00031443  mov         dword ptr [ebp-20h],eax  
    printf("%d", c);
00241456  mov         esi,esp  
00241458  mov         eax,dword ptr [ebp-20h]  
0024145B  push        eax  
0024145C  push        245858h  
00241461  call        dword ptr ds:[00249114h]  
00241467  add         esp,8  
0024146A  cmp         esi,esp  
0024146C  call        0024113B  
    return 0;
00241471  xor         eax,eax  
}

通过反汇编代码得知,此时指向add操作把esp加上8,此时就把x和y这两个形参释放回来了,指向如图所示位置:

接下来mov把eax放到ebp-20h上,而eax就是我们出Add函数时计算的和,此时和就被我们带回来了,接下来就是main函数栈帧的销毁了,跟上文Add函数栈帧的销毁没有太大区别,这里不多做赘述。

而反汇编代码如下:

00241471  xor         eax,eax  
}
00031451  pop         edi  
00031452  pop         esi  
00031453  pop         ebx  
00031454  add         esp,0E4h  
0003145A  cmp         ebp,esp  
0003145C  call        __RTC_CheckEsp (03113Bh)  
00031461  mov         esp,ebp  
00031463  pop         ebp  
00031464  ret

四、总结

至此,函数栈帧的创建和销毁正式结束,而本文一开始的几个问题(目标)也能清晰得知:

如下:

1、局部变量是怎么创建的?

首先,为函数分配好栈帧空间并初始化后,然后给局部变量在栈帧里头分配一点空间。

2、为什么局部变量的值是随机值?

因为随机值是我们在开辟栈帧时就放进去的,而我们初始化的时候,就是把随机值给覆盖了。

3、函数是怎么传参的?传参的顺序是怎样的?

当我要调用函数之前,就已经push、push把这两个参数从右向左压栈压进去,当我们真正进入形参函数的时候,在Add函数栈帧里头通过指针的偏移量找到了形参。

4、形参和实参是什么关系?

形参确实是在压栈时开辟的空间,形参和实参只是值上是相同的,空间上是独立的,形参是实参的一份临时拷贝,改变形参不会影响实参。

5、函数调用结束后是怎么返回的?

我们在调用之前就已经把call指令下一条指令的地址给压进去,当函数调用完要返回的时候,就会跳转到call指令下一条指令的地址,返回值是通过寄存器带回来的。

到此这篇关于C语言中函数栈帧的创建和销毁的深层分析的文章就介绍到这了,更多相关C语言 函数栈帧内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言函数栈帧的创建和销毁详解

    目录 写在前面 Add函数的调用 函数传参 Add函数栈帧的创建 Add函数栈帧的销毁 main函数栈帧的销毁 总结 写在前面 我们知道,每一次函数调用都需要在栈区上为其开辟一块空间,这块空间就叫做这个函数的栈帧. 而栈是从高地址向低地址延伸的.每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息.寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址). 这样我们就了解了寄存器ebp和寄存器esp中存放的是地址,这两个地址是用来维护函数栈

  • C语言函数栈帧的创建和销毁介绍

    在初学c语言中,很多时候要记的内容有点多,有时候并不能深入的了解它.关于函数的栈帧可以帮助我们深入了解函数传参的过程,让我们了解c语言. 以下是我们平时接触过,但不了解的问题: 1.为什么局部变量在未赋值前是随机的. 2.局部变量创建的过程. 3.函数传参,传参的顺序问题. 4.形参与实参的关系什么. 5.调用函数是怎么调用的,调用的过程是什么. 6.调用函数结束后,是怎样返回的. 这些问题我们在学校可能并不会接触,也不会出现在考试的试卷上,但是作为计算机专业的学生,做一些认识和了解是很有必要的

  • C语言函数栈帧的创建与销毁详解

    目录 前言 一.函数栈帧是什么? 1.寄存器 2.ebp与esp 二.函数栈帧的创建 1.代码块 2.调用堆栈 3.esp与ebp如何维护栈帧 总结 前言 大家在学习的时候一定有以下困惑: 局部变量是怎么创建的?为什么局部变量的值是随机值?函数是怎么传参?传参的顺序是怎样的?形参与实参是什么关系?函数调用是怎么做到的?函数调用完成不是销毁了吗,如何带回的返回值? 以上这些都可以通过了解函数栈帧的创建与销毁来理解.接下来我就带大家来了解函数栈帧的创建与销毁. 本次使用的编辑器是VS2013,因为越

  • c语言函数栈帧的创建和销毁过程详解

    目录 1 相关知识介绍 1.1 寄存器 1.2 函数栈帧概述 2 栈帧创建与销毁过程 1 相关知识介绍 1.1 寄存器 一般计算机内通用寄存器包括eax,ebx,ecx,edx,esi,edi,esp,edp,其中esp,ebp这两个寄存器是用来存放地址的,这两个地址就是用来维护函数栈帧的 1.2 函数栈帧概述 我们知道c语言中函数都是被调用的,main函数里面能调用其他函数,其实main函数也是被别的函数调用的.main函数是在 _tmainCRTSartup 函数中被调用的,_tmainCR

  • C语言中函数栈帧的创建和销毁的深层分析

    目录 一.本文目标 二.基础知识 1.寄存器 2.代码案例 3.总体栈帧概况 4.所需反汇编代码总览 三.函数栈帧创建销毁过程 1._tmainCRTStartup函数(调用main函数)栈帧的创建 2.main函数栈帧的创建 3.main函数内执行有效代码(变量) 4.Add函数栈帧的创建 5.Add函数内执行有效代码 6.Add函数栈帧的销毁 7.main函数栈帧的销毁 四.总结 一.本文目标 1.局部变量是怎么创建的? 2.为什么局部变量的值是随机值? 3.函数是怎么传参的?传参的顺序是怎

  • C语言超详细讲解函数栈帧的创建和销毁

    目录 1.本节目标 2.相关寄存器 3.相关汇编指令 4.什么是函数栈帧 5.什么是调用堆栈 6.函数栈帧的创建和销毁 (1).main函数栈帧的创建与初始化 (2).main函数的核心代码 (3).Add函数的调用过程 (4).Add函数栈帧的销毁 (5).调用完成 7.对开篇问题的解答 1.本节目标 C语言绝命七连问,你能回答出几个? 局部变量是如何创建的?为什么局部变量不初始化其内容是随机的?有些时候屏幕上输出的"烫烫烫"是怎么来的?函数调用时参数时如何传递的?传参的顺序是怎样的

  • C语言详尽图解函数栈帧的创建和销毁实现

    目录 常见寄存器 基本的汇编语言知识 具体实现 关于栈帧创建与销毁的问答题 注:本文章所使用的编译器是VS2010,由于不同编译器的函数栈帧与销毁略有差异,所以具体细节请读者自行实践! 常见寄存器 寄存器有:eax.ebx.ecx.edx.edi.esi.ebp.esp 其中 ebp 和 esp 是用来维护函数栈帧的,他们里面存放的是地址. 他们维护的是某个正在被调用的函数.通常我们又称 ebp 为栈底指针,称 esp 为栈顶指针 基本的汇编语言知识 push:压栈 pop:出栈 mov:若有变

  • C语言函数栈帧的创建与销毁原理图解

    目录 什么是函数栈帧 什么是栈? 与函数栈帧有关的汇编语句 函数如何创建栈帧并销毁 main函数栈帧开辟 调用Add函数 返回主函数 什么是函数栈帧 我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的. 那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系. 函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放: 函数参数和函数返回值 临时变量

  • 详细理解函C语言的函数栈帧

    目录 一.函数栈帧的创建 1.寄存器 2.函数栈帧 3.函数中调用函数 二.函数栈帧的销毁 总结 一.函数栈帧的创建 1.寄存器 一般来说,计算机中的寄存器有六种 分别是:eax, ebx, ecx,edx,ebp,esp 而ebp,esp这两个寄存器中存放的是地址,与此同时,这两个地址是来维护函数栈帧的. 2.函数栈帧 每一个函数的调用,都需要在栈区为其开辟一个空间,这块为其开辟的空间就是函数栈帧. ebp:栈底指针 esp:栈顶指针 对于栈这种数据结构一共有两种栈操作 1.pop 出栈 2.

  • C语言函数栈帧详解

    目录 前言 一.函数栈帧是什么? 二.栈帧准备知识 1.内存分区 2.什么是栈? 三.详解栈帧创建与销毁全过程 调用函数之前: 将传入函数的值放入栈中 函数执行: 1.保护当前ebp 2.创建所需调用函数的栈帧空间 3.保存局部变量 4.参数运算 函数返回: 1.存储返回值 2.销毁空间 3.ebp回上一栈帧栈底 4.销毁形参 5.main函数拿到返回值 总结 前言 在c语言中我们会将一些功能单独写成一个函数,以供主函数调用,在表面来看调用的过程就是写出一个函数后,只需要在调用时中通过函数名将实

随机推荐