函数调用时堆栈的变化情况
来源:互联网 发布:手机大型网络枪战游戏 编辑:程序博客网 时间:2024/05/21 21:41
代码编译运行环境:VS2012+Debug+Win32
函数的正常运行必然要利用堆栈,至少,函数的返回地址是保存在堆栈上的。函数一般要利用参数,而且内部也会用到局部变量,在对表达式进行求值时,编译器还会生成一些无名临时对象,这些对象都是存放在堆栈上的。
下面以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);}
在VS2012环境下,以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
1.mixAdd()函数汇编代码详解
在进入mixAdd后,可以马上看到这样三条汇编指令:
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)。
接下来的汇编指令:
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决定。
因此,上面四条指令的作用是从栈的低地址到高地址将所有的预留空间填满0cccccccch,这样也解释了未赋值的局部变量默认被设置为CCCCCCCCH。
接下来的汇编指令:
int tmpi=i;00F713FE mov eax,dword ptr [i] //i赋值给eax00F71401 mov dword ptr [tmpi],eax //eax赋值给tmpi char tmpc=c;00F71404 mov al,byte ptr [c] //c赋值给寄存器ax低8位al00F71407 mov byte ptr [tmpc],al //al赋值给tmpc return tmpi+tmpc;00F7140A movsx eax,byte ptr [tmpc] //带符号扩展传送指令,将rmpc赋值给eax00F7140E add eax,dword ptr [tmpi] //tmpi与eax相加
以下汇编指令,用于函数结束的清理工作:
001E1411 pop edi //edi出栈,还原edi001E1412 pop esi // esi出栈,还原esi001E1413 pop ebx // ebx出栈,还原ebx001E1414 mov esp,ebp // 清空栈,释放局部变量001E1416 pop ebp //源ebp出栈,恢复ebp001E1417 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. main()函数对应的汇编代码注意要点
main()函数的汇编代码大致与mixAdd()相似,但也有不同之处,需要注意一下几点。
printf(“%c”,res);对应的几条汇编代码
(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] http://www.cnblogs.com/awpatp/archive/2012/08/05/2623628.html
[2]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.[3.3(P97-P100)]
- 函数调用时堆栈的变化情况
- 函数调用堆栈变化情况
- 函数调用堆栈变化情况
- Win32程序函数调用时堆栈变化情况分析(zt)
- Win32程序函数调用时堆栈变化情况分析
- Win32程序函数调用时堆栈变化情况分析
- Win32程序函数调用时堆栈变化情况分析
- Win32程序函数调用时堆栈变化情况分析
- 函数调用时程序堆栈的变化
- 函数调用时堆栈变化
- C/C++函数调用时的堆栈变化
- 函数调用和返回时,堆栈是怎么变化的?
- 缓冲区溢出[函数调用时的堆栈变化]
- 汇编调用C函数时的堆栈变化
- C函数调用与堆栈的变化
- C函数调用与堆栈的变化
- C函数调用与堆栈的变化
- C函数调用与堆栈的变化
- IOS开发系列--Objective-C之协议、代码块、分类
- Graph DataBase介绍
- 64位Linux编译cximage手记
- 排序算法
- SimHash
- 函数调用时堆栈的变化情况
- TreeSet的两种排序方法
- [leetcode] Rotate and Reverse
- iOS巅峰之Swift笔记详解(上)
- LeetCode 13:Roman to Integer
- Android layout布局属性、标签属性总结大全。
- Unity人工智能学习—高级随机运动
- Java 引用,栈 堆 的理解
- Java计算n-m之间质数/素数,打印出全部素数、总和以及个数