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

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


1.栈帧简介

函数的运行是在栈上展开的,每一个函数在被调用时所占用的内存空间就是栈,栈保存了一个函数调用需要维护的信息。函数栈通常被称为栈帧(Stack Frame)或活动记录(Activate Record)。一个栈帧通常包含如下内容:
(1)函数的参数、返回地址;
(2)函数内的非静态局部变量、表达式运算时可能产生的无名临时对象;
(3)函数上下文,比如函数调用前后需要保持不变的寄存器。

一个典型的栈帧结构如下:
在这里插入图片描述

2.实例剖析

下面以Visual C++编译器为例进行研究。

#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);
}

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

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

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()函数对应的汇编代码:

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  

2.1 mixAdd()函数汇编代码详解

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

push ebp      //保留主调函数的帧指针
mov ebp,esp   //建立本函数的帧指针
sub esp,xxx   //为函数局部变量分配空间

这是所有C/C++函数的汇编代码所共同遵循的规范。其中ebp(Extended Base Pointer)为扩展基址指针寄存器,也被称为帧指针(Frame Pointer)寄存器,其存放一个指针,该指针指向系统栈最上面一个栈帧的底部。

一个栈帧起始位置由帧指针ebp指明,在函数运行期间,帧指针ebp的值保持不变。而栈帧的另一端由栈指针esp动态维护。esp(Extended Stack Pointer)为扩展栈指针寄存器,用于存放当前函数的栈顶指针。

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

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

接下来的汇编指令:

00F713E9  push        ebx  //保存扩展基址寄存器,入栈
00F713EA  push        esi  //保存扩展源变址寄存器,入栈
00F713EB  push        edi  //保存扩展目的变址寄存器,入栈

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

接下来的汇编指令:

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决定。

因此,上面四条指令的作用是从栈的低地址到高地址将所有的预留空间填充CCCCCCCCh,未赋值的局部变量也就默认被设置为CCCCCCCCh。

接下来的汇编指令:

	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相加

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

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()函数的定义改为如下形式:

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.2 main()函数对应的汇编代码注意要点

main()函数的汇编代码大致与mixAdd()相似,但也有不同之处,需要注意以下几点。
(1)printf()函数参数的入栈和调用。

push        1E5858h  					//将”%c”入栈
call        dword ptr ds:[1E92C0h]  	//调用printf()函数

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

001E1471  cmp         esi,esp  
001E1473  call        __RTC_CheckEsp (01E1136h)  

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


参考文献

[1]rep stos dword ptr es:[edi] 是做什么的?
[2]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.C3.3.P97-P100
[3]程序员的自我修养[M].北京:机械工业出版社.C10.2栈与调用惯例.P286-P305

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值