Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >函数调用时堆栈的变化情况

函数调用时堆栈的变化情况

作者头像
恋喵大鲤鱼
发布于 2018-08-03 09:08:31
发布于 2018-08-03 09:08:31
82000
代码可运行
举报
文章被收录于专栏:C/C++基础C/C++基础
运行总次数:0
代码可运行

代码编译运行环境:VS2012+Debug+Win32


函数的正常运行必然要利用堆栈,至少,函数的返回地址是保存在堆栈上的。函数一般要利用参数,而且内部也会用到局部变量,在对表达式进行求值时,编译器还会生成一些无名临时对象,这些对象都是存放在堆栈上的。

下面以Visual C++编译器为例进行研究,考察如下程序。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <stdio.h>

int mixAdd(int i,char c){
    int tmpi=i;
    char tmpc=c;
    return tmpi+tmpc;
}

int main()
{
    int res=mixAdd(4,'A');
    printf("%c",res);
}

在VS2012环境下,以C/C++默认的函数调用约定__cdecl来生成该程序的调试版本(Debug)的汇编代码。

mixAdd()函数对应的汇编代码是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int mixAdd(int i,char c){
00F713E0  push        ebp  
00F713E1  mov         ebp,esp  
00F713E3  sub         esp,0D8h  
00F713E9  push        ebx  
00F713EA  push        esi  
00F713EB  push        edi  
00F713EC  lea         edi,[ebp-0D8h]  
00F713F2  mov         ecx,36h  
00F713F7  mov         eax,0CCCCCCCCh  
00F713FC  rep stos    dword ptr es:[edi]  
    int tmpi=i;
00F713FE  mov         eax,dword ptr [i]  
00F71401  mov         dword ptr [tmpi],eax  
    char tmpc=c;
00F71404  mov         al,byte ptr [c]  
00F71407  mov         byte ptr [tmpc],al  
    return tmpi+tmpc;
00F7140A  movsx       eax,byte ptr [tmpc]  
00F7140E  add         eax,dword ptr [tmpi]  
}
001E1411  pop         edi  
001E1412  pop         esi  
001E1413  pop         ebx  
001E1414  mov         esp,ebp  
001E1416  pop         ebp  
001E1417  ret  

main()函数对应的汇编代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
001E1430  push        ebp  
001E1431  mov         ebp,esp  
001E1433  sub         esp,0CCh  
001E1439  push        ebx  
001E143A  push        esi  
001E143B  push        edi  
001E143C  lea         edi,[ebp-0CCh]  
001E1442  mov         ecx,33h  
001E1447  mov         eax,0CCCCCCCCh  
001E144C  rep stos    dword ptr es:[edi]  
    int res=mixAdd(4,'A');
001E144E  push        41h  
001E1450  push        4  
001E1452  call        mixAdd (01E1168h)  
001E1457  add         esp,8  
001E145A  mov         dword ptr [res],eax  
    printf("%c",res);
001E145D  mov         esi,esp  
001E145F  mov         eax,dword ptr [res]  
001E1462  push        eax  
001E1463  push        1E5858h  
001E1468  call        dword ptr ds:[1E92C0h]  
001E146E  add         esp,8  
001E1471  cmp         esi,esp  
001E1473  call        __RTC_CheckEsp (01E1136h)  
}
001E1478  xor         eax,eax  
}
001E147A  pop         edi  
001E147B  pop         esi  
001E147C  pop         ebx  
001E147D  add         esp,0CCh  
001E1483  cmp         ebp,esp  
001E1485  call        __RTC_CheckEsp (01E1136h)  
001E148A  mov         esp,ebp  
001E148C  pop         ebp  
001E148D  ret  

1.mixAdd()函数汇编代码详解

在进入mixAdd后,可以马上看到这样三条汇编指令:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
push ebp      //保留主调函数的帧指针
mov ebp,esp   //建立本函数的帧指针
sub esp,xxx   //为函数局部变量分配空间

这是所有C/C++函数的汇编代码所共同遵循的规范。其中,ebp被称为“帧指针”,扩展基址指针寄存器(extended base pointer),其存放一个指针,该指针指向系统栈最上面一个栈帧的底部。这里的帧指的是每一个函数在被调用时所占有的内存空间,该空间内存放函数的局部数据。

一帧的数据的起始位置由帧指针ebp指明,而帧的另一端由栈指针esp动态维护。ESP就是当前函数的栈顶指针。在函数运行期间,帧指针ebp的值保持不变。

内存管理中,与栈对应是堆。对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方式是向下的,是向着内存地址减小的方向增长。在内存中,“堆”和“栈”共用全部的自由空间,只不过各自的起始地址和增长方向不同,它们之间并没有一个固定的界限,如果在运行时,“堆”和 “栈”增长到发生了相互覆盖时,称为“栈堆冲突”,程序将会崩溃。

在Debug模式下,一个C/C++函数即使没有定义一个局部变量,仍然会分配192Bytes空间,供临时变量使用。如果定义了局部变量,则会为每个局部变量分配12字节的空间(大于任何基本数据类型)。mixAdd()函数中定义了两个局部变量,所以给局部变量和临时变量预留空间大小是192+12+12=216(D8h)。

接下来的汇编指令:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
00F713E9  push        ebx  //保存扩展基址寄存器,入栈
00F713EA  push        esi  //保存扩展源变址寄存器,入栈
00F713EB  push        edi  //保存扩展目的变址寄存器,入栈

以上汇编指令保存本函数可能改变的几个寄存器的值,这些寄存器在函数结束后恢复到进入本函数的时候的值。

接下来的汇编指令:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
00F713EC  lea         edi,[ebp-0D8h]      //获取栈顶地址
00F713F2  mov         ecx,36h             //赋36H至扩展计数寄存器
00F713F7  mov         eax,0CCCCCCCCh      //给扩展累加寄存器赋值
00F713FC  rep stos    dword ptr es:[edi]  //作用见下面解释

stos指令:字符串存储指令,将eax中的值拷贝至es:[edi]指向的空间,如果设置了direction flag, 那么edi会在该指令执行后减小, 如果没有设置direction flag, 那么edi的值会增加, 这是为了下一次的存储做准备。

rep指令:重复指令,重复执行后面制定的指令操作,重复次数由计数寄存器ecx决定。

因此,上面四条指令的作用是从栈的低地址到高地址将所有的预留空间填满0cccccccch,这样也解释了未赋值的局部变量默认被设置为CCCCCCCCH。

接下来的汇编指令:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    int tmpi=i;
00F713FE  mov         eax,dword ptr [i]     //i赋值给eax
00F71401  mov         dword ptr [tmpi],eax  //eax赋值给tmpi
    char tmpc=c;
00F71404  mov         al,byte ptr [c]    //c赋值给寄存器ax低8位al
00F71407  mov         byte ptr [tmpc],al //al赋值给tmpc
    return tmpi+tmpc;
00F7140A  movsx       eax,byte ptr [tmpc]  //带符号扩展传送指令,将rmpc赋值给eax
00F7140E  add         eax,dword ptr [tmpi] //tmpi与eax相加

以下汇编指令,用于函数结束的清理工作:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
001E1411  pop         edi     //edi出栈,还原edi
001E1412  pop         esi     // esi出栈,还原esi
001E1413  pop         ebx     // ebx出栈,还原ebx
001E1414  mov         esp,ebp // 清空栈,释放局部变量
001E1416  pop         ebp     //源ebp出栈,恢复ebp
001E1417  ret                 //子程序的返回指令,结束函数

注意:以上汇编代码对mixAdd()函数的调用采用的函数调用约定是__cdecl,这是C/C++程序的默认函数调用约定,其重要的一点就是在被调用函数 (Callee) 返回后,由调用方 (Caller)调整堆栈,因此在main()函数中调用mixAdd()的地方会出现add esp 8这条指令。esp加上8,是因为main()函数将两个参数压入栈,用于传给mixAdd()。感兴趣的读者将mixAdd()函数的定义改为如下形式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int __stdcall mixAdd(int i,char c){
    int tmpi=i;
    char tmpc=c;
    return tmpi+tmpc;
}

即将mixAdd()函数的调用约定改为标准调用约定,那么mixAdd()函数结束时的汇编代码会变成ret 8,main()函数调用mixAdd()的地方会原本出现的add esp 8这条指令将会消失,这是因为__stdcall约定被调函数自身清理堆栈。有关函数调用约定的介绍见我的另一篇blog:关于函数参数入栈的思考

2. main()函数对应的汇编代码注意要点

main()函数的汇编代码大致与mixAdd()相似,但也有不同之处,需要注意一下几点。 printf(“%c”,res);对应的几条汇编代码 (1)printf()函数参数的入栈和调用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
push        1E5858h  //将”%c”入栈
call        dword ptr ds:[1E92C0h]  //调用printf()函数

(2)以下两条汇编代码的意思

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
001E1471  cmp         esi,esp  
001E1473  call        __RTC_CheckEsp (01E1136h)  

上面两条汇编用于表示VC编译器提供了运行时刻的对程序正确性/安全性的一种动态检查,可以在项目属性的C++选项中打开来启用Runtime Check。开启与打开步骤如下图:


参考文献

[1] http://www.cnblogs.com/awpatp/archive/2012/08/05/2623628.html [2]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.[3.3(P97-P100)]

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2015年10月22日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
5.5 汇编语言:函数调用约定
函数是任何一门高级语言中必须要存在的,使用函数式编程可以让程序可读性更高,充分发挥了模块化设计思想的精髓,今天我将带大家一起来探索函数的实现机理,探索编译器到底是如何对函数这个关键字进行实现的,并使用汇编语言模拟实现函数编程中的参数传递调用规范等。
王 瑞
2023/08/22
3920
【C语言】汇编角度剖析函数调用的整个过程
压栈操作,他会改变esp所指向的位置,从而适应栈帧空间的扩大,操作方式就是将操作数直接压栈到栈帧空间
举杯邀明月
2023/04/12
1.7K0
【C语言】汇编角度剖析函数调用的整个过程
汇编角度看函数堆栈调用
带着以下一个问题来探索: (1)形参的内存空间的开辟和清理是由调用方还是由被调用方执行的? (2)主函数调用函数结束后,主函数从哪里开始执行?从头开始还是从调用之后开始? (3)返回值是如何带出来的?
lexingsen
2022/02/24
7180
汇编角度看函数堆栈调用
大神洗礼第四讲——函数相关及编程技巧
Author:bakari       Date:2012.11.2 1、参数传递问题: < 1 >、堆栈传参 < 2 >、寄存器传参(利用通用寄存器进行函数参数传递的方法) < 3 >、全局变量或静态变量传参 2、 Call Convention(函数调用约定) < 1 >、_cdecl a、 参数从右向左压入堆栈 b、 函数被调用者修改堆栈 c、 在win32应用程序里,宏APIENTRY,WINAPI,都表示_stdcall,非常常见. d、 C和C++程序的缺省调用方式。每一个调用它的函数都包含清空
Linux云计算网络
2018/01/10
6910
大神洗礼第四讲——函数相关及编程技巧
C语言函数的栈帧详解
一个限定表尾进行删除(出栈)和插入(入栈)操作的线性表,其过程类似与压子弹与退子弹(后进先出)。 一个由系统自动分配的内存空间,譬如调用函数、创建临时变量时内存空间的创建与销毁。 用于存储函数内部的局部变量、方法调用、函数传参数值等。 由高地址向低地址生长。
CtrlX
2022/10/27
2.3K0
C语言函数的栈帧详解
你一定要搞明白的C函数调用方式与栈原理
这绝对不是标题党。而是C/C++开发中你必须要掌握的基础知识,也是高级技术岗位面试中高频题。我真的真的真的希望无论是学生还是广大C/C++开发者,都该掌握此文中介绍的知识。
范蠡
2018/07/25
3.4K0
你一定要搞明白的C函数调用方式与栈原理
你知道函数栈帧的创建和销毁吗?
将想到的烧烤食物写在便条上,一个食材一个便条,最先想到的食材写在便条上后,放在最下面,依次往上放,最后想到的写在便条上后,放在最上面。
南桥
2024/01/26
1730
你知道函数栈帧的创建和销毁吗?
【C语言】函数——栈帧的创建和销毁
✨作者:@平凡的人1 ✨专栏:《C语言从0到1》 ✨一句话:凡是过往,皆为序章 ✨说明: 过去无可挽回, 未来可以改变 ---- 目录 前言😄 什么是栈🔑 什么是函数的栈帧🔑 认识相关寄存器和汇编指令🔑 寄存器🔥 相关的汇编指令:🔥 函数的调用堆栈🔑 函数栈帧的创建🔑 分析栈帧的创建:💧 为什么会出现“烫烫烫”:💧 分析main函数中的核心代码:💧 分析Add函数的传参💧 函数调用过程💧 函数栈帧的销毁下🔑 结语✍ ---- 前言😄 好的,各位,我们前面就已经学过函数的一些相关知识了
平凡的人1
2022/11/15
6950
【C语言】函数——栈帧的创建和销毁
函数栈帧的创建与销毁
最近在学习C语言的过程中遇到了一些问题,在询问老师和查询相关资料的基础上了解到了函数栈帧的相关概念,对下列问题也有了答案。
摘星
2023/04/28
6080
函数栈帧的创建与销毁
C语言——F/函数的栈帧的创建和销毁
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
用户11015888
2024/03/11
1750
C语言——F/函数的栈帧的创建和销毁
奇怪的函数调用
整理移动硬盘时,发现一个名为 attack 的目录,进去以后发现原来是一段简单的 C 语言代码。代码如下:
码农UP2U
2021/09/02
2K0
奇怪的函数调用
C语言与汇编的嵌入式编程:main中模拟函数的调用(两数交换)
注明:swap函数大致处理过程为:把下个地址压入堆栈,然后参数入栈,然后把所有寄存器压入堆栈,分配空间,空间清C然后变量赋值开始程序然后做堆栈平衡清理堆栈
墨文
2020/02/28
1K0
【C语言】函数栈帧的创建和销毁(启航——迎接崭新的自己)
这里我只会笼统的给大家聊一下寄存器的作用,而不会深入的探讨,毕竟有这些知识就足够用了。
埋头编程
2024/10/16
1840
【C语言】函数栈帧的创建和销毁(启航——迎接崭新的自己)
实例分析C程序运行时的内存结构
这段代码包含两个函数,因此可以测试函数调用,此外还包含了静态变量、局部变量、返回值等
李拜六不开鑫
2018/09/04
1.1K0
实例分析C程序运行时的内存结构
函数栈帧的创建和销毁
注: 本次讲解使用的是vs2013,不要使用太高级的编译器,越高级的编译器,越不容易学习和观察;同时,在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
waves浪游
2024/01/22
2480
函数栈帧的创建和销毁
C++函数调用过程深入分析
函数调用的过程实际上也就是一个中断的过程,那么C++中到底是怎样实现一个函数的调用的呢?参数入栈、函数跳转、保护现场、回复现场等又是怎样实现的呢?本文将对函数调用的过程进行深入的分析和详细解释,并在VC 6.0环境下进行演示。分析不到位或者存在错误的地方请批评指正,请与作者联系。
C语言与CPP编程
2021/04/16
2.5K0
详解C/C++堆栈的工作机制
我们经常会讨论这样的问题:什么时候数据存储在堆栈(Stack)中,什么时候数据存储在堆(Heap)中。我们知道,局部变量是存储在堆栈中的;debug时,查看堆栈可以知道函数的调用顺序;函数调用时传递参数,事实上是把参数压入堆栈,听起来,堆栈象一个大杂烩。那么,堆栈(Stack)到底是如何工作的呢?本文将详解C/C++堆栈的工作机制。阅读时请注意以下几点:
混说Linux
2022/11/18
5830
详解C/C++堆栈的工作机制
堆栈基础(一)
本文稿费80软妹币 砸个广告:各位在网络安全方面有新创作的小伙伴,快将你们的心得砸过来吧~ 文章以word形式发至邮箱: minwei.wang@dbappsecurity.com.cn 有偿投稿,
安恒网络空间安全讲武堂
2018/06/26
7880
C函数原理
C语言作为面向过程的语言,函数是其中最重要的部分,同时函数也是C种的一个难点,这篇文章希望通过汇编的方式说明函数的实现原理。
Masimaro
2018/08/31
6150
滴水逆向初级-C语言(二)
1、声明变量 变量类型变量名; 变量类型用来说明宽度是多大 int 4个字节 short 2个字节 char 1个字节
zhang_derek
2021/04/13
1.3K0
相关推荐
5.5 汇编语言:函数调用约定
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档