C语言进阶——函数栈帧的创建和销毁

目录

1、函数栈帧相关概念     

2、函数栈帧的创建与销毁

2.1main函数的创建

2.2main函数中变量的创建

2.3 Add函数栈帧的创建

2.4 Add函数栈帧的销毁

在C语言的学习中你可能有很多问题:

        局部变量是怎么创建的?为什么局部变量的值是随机的?函数是怎么传参的?传参的顺序是怎样的?形参和实参的关系是什么?函数是如何被调用的?函数调用结束后又是怎么返回的?了解函数栈帧的创建和销毁之后,这些你就都明白了。不同的编译器函数栈帧的创建是略有差异的,具体的细节则取决于编译器的实现。对于初学者而言,越高级的编译器越不容易学习和观察,所以不要使用太高级的编译器,本博客在学习栈帧的创建和销毁时使用的是VS2013。   

1、函数栈帧相关概念     

        在了解函数栈帧之前需要先了解的什么是寄存器。寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。比如eax、ebx、ecx、edx、ebp 和esp等等。最值得注意的两个寄存器是ebp (栈底指针)和esp(栈顶指针),这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。

        push 压栈:给栈顶放元素进去

        pop 出栈:从栈顶删除元素

        接着我们来了解函数栈帧的概念。

        函数栈帧是编译器实现函数调用的一种数据结构,记录函数调用时产生的相关信息。每一个函数调用,都要在栈区创建一个空间。以main函数为例,我们来看看是如何分配栈空间的:

 2、函数栈帧的创建与销毁

          我们以一段代码为例,看看函数栈帧是如何创建的。

 对上方代码进行调试,选中调用堆栈窗口,我们发下其实main函数也是被其他函数调用的。

        调用main函数的函数是__tmainCRTStartup函数,而__tmainCRTStartup函数又是在mainCRTStartup函数内部调用的。

        为了帮助我们更清楚的看大栈帧的创建过程,我们看看上述代码对应的反汇编代码。

int main()
{
007318B0  push        ebp  
007318B1  mov         ebp,esp  
007318B3  sub         esp,0E4h  
007318B9  push        ebx  
007318BA  push        esi  
007318BB  push        edi  
007318BC  lea         edi,[ebp-24h]  
007318BF  mov         ecx,9  
007318C4  mov         eax,0CCCCCCCCh  
007318C9  rep stos    dword ptr es:[edi]  
007318CB  mov         ecx,73C003h  
007318D0  call        0073131B  

    int a = 10;
007318D5  mov         dword ptr [ebp-8],0Ah  
    int b = 20;
007318DC  mov         dword ptr [ebp-14h],14h  
    int c = 0;
007318E3  mov         dword ptr [ebp-20h],0  
    c = Add(a, b);
007318EA  mov         eax,dword ptr [ebp-14h]  
007318ED  push        eax  
007318EE  mov         ecx,dword ptr [ebp-8]  
007318F1  push        ecx  
007318F2  call        007310B4  
007318F7  add         esp,8  
007318FA  mov         dword ptr [ebp-20h],eax  
    printf("%d\n", c);
007318FD  mov         eax,dword ptr [ebp-20h]  
00731900  push        eax  
00731901  push        737B30h  
00731906  call        007310D2  
0073190B  add         esp,8  

    return 0;
0073190E  xor         eax,eax  
}
00731910  pop         edi  
00731911  pop         esi  
00731912  pop         ebx  
00731913  add         esp,0E4h  
00731919  cmp         ebp,esp  
0073191B  call        00731244  
00731920  mov         esp,ebp  
00731922  pop         ebp  
00731923  ret  

2.1main函数的创建

007318B0  push        ebp  //将edp压入栈帧
007318B1  mov         ebp,esp  //将esp的值赋给edp
007318B3  sub         esp,0E4h  //esp-0E4h 将esp向上(低地址方向)移动4个字节
007318B9  push        ebx  //接下来三行是将 ebx esi edi 压入栈顶
007318BA  push        esi  
007318BB  push        edi  
007318BC  lea         edi,[ebp-24h]  //然后将main函数的函数栈帧初始化为0cccccccch
007318BF  mov         ecx,9  
007318C4  mov         eax,0CCCCCCCCh  
007318C9  rep stos    dword ptr es:[edi] 

 2.2main函数中变量的创建

int a = 10;
007318D5  mov      dword ptr [ebp-8],0Ah  //把0Ah赋值给内存地址为ebp-8中的双字节的空间
 int b = 20;
007318DC  mov      dword ptr [ebp-14h],14h  //把14h赋值给内存地址为ebp-14h中的双字节的空间
 int c = 0;
007318E3  mov      dword ptr [ebp-20h],0  //把0赋值给内存地址为ebp-20h中的双字节的空间

2.3 Add函数栈帧的创建

当abc变量创建好了之后,开始调用add函数。分别将eax(20)和ecx(10)压入栈顶。实际上就是在为Add函数传参

c = Add(a, b);
007318EA  mov         eax,dword ptr [ebp-14h]  
007318ED  push        eax  
007318EE  mov         ecx,dword ptr [ebp-8]  
007318F1  push        ecx  
007318F2  call         007310B4  
007318F7  add         esp,8  

         接着使用call指令将call指令的下一条指令的地址压入栈顶,等Add函数调用结束后,就会回到call指令的下一条指令继续执行。

        进入Add函数后,前面的几条指令跟进入main之前的几条指令一样,给函数准备栈帧并对其进行初始化。

        随后在ebp-8的空间创建临时变量z并初始化为0,再通过ebp+8和ebp+0Ch找到main函数中传递的a,b参数作为形参x,y,相加得到的值赋给eax,再由eax把值赋给z。

2.4 Add函数栈帧的销毁

00731910  pop         edi  //将esp所指的地址的值赋给edi,再将esp的值增加4字节
00731911  pop         esi   //将esp所指的地址的值赋给esi,再将esp的值增加4字节
00731912  pop         ebx   //将esp所指的地址的值赋给ebx,再将esp的值增加4字节
00731920  mov         esp,ebp  //将ebp的值赋给esp,并不是将ebp所指向的内存空间的值赋给esp
00731922  pop         ebp  //将esp所指的地址的值赋给ebp,再将esp的值增加4字节
00731923  ret                //执行完该命令后,自动返回call指令的下一行

现在我们可以轻松回答文章开篇提出的问题。

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

        函数开辟栈帧空间初始化之后,为局部变量分配空间。

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

        局部变量的值是随机放进去的,在初始化之后才进行了覆盖。

3)函数是怎么传参的?

        调用函数时,先push压栈,通过指针偏移量来传参。

4)形参和实参的关系是什么?

        形参压栈开辟空间,它与实参在空间上是独立的,它是实参的一份临时拷贝

5)函数是如何被调用的?

        开辟栈空间,然后传参,调用

6)函数调用结束后又是怎么返回的

        将返回值放在寄存器中,以文中的Add函数为例,当返回函数时,放入局部变量c中

  • 11
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 26
    评论
sscanf函数是C语言中一个非常常用的函数,它可以将一个字符串按照指定的格式转换成相应的数据类型。在嵌入式开发中,sscanf函数也是非常常见的,因为很多时候需要从串口或者其他外部设备中读取数据,并将其转换成相应的数据类型行处理。下面是一些sscanf函数的使用技巧: 1. 使用sscanf函数时一定要注意格式字符串的正确性。格式字符串中的占位符必须与待转换的数据类型相对应,否则会发生未知错误。 2. 如果待转换的字符串中包含多个数据,可以使用多个占位符行转换。例如,如果待转换的字符串为"1,2,3",可以使用" %d,%d,%d"的格式字符串行转换。 3. 可以使用sscanf函数的返回值来判断转换是否成功。如果返回值等于待转换字符串的长度,则说明转换成功,否则转换失败。 4. 如果待转换的字符串中包含浮点数,可以使用"%f"或者"%lf"的格式字符串行转换。 5. 如果待转换的字符串中包含十六制数,可以使用"%x"的格式字符串行转换。 6. 如果待转换的字符串中包含字符或字符串,可以使用"%c"或者"%s"的格式字符串行转换。 7. 如果待转换的字符串中包含指针类型的数据,可以使用"%p"的格式字符串行转换。 总之,在使用sscanf函数时一定要注意格式字符串的正确性,否则很容易出现转换错误的情况。同时,还应该注意sscanf函数返回值的判断,以确保转换的正确性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值