第6章 函数的工作原理
阅读本章前,先思考两个问题:
当函数执行时,程序流程会转到函数体的实现地址处,只有遇到return语句或者“}”
符号才返回到下一条语句的地址处,请问编译器是如何确定应该回到什么地址处的?
为什么很多高级语言在传递参数时会执行将实参复制给形参这一操作呢?
思考5分钟后,请仔细阅读本章,以印证你的想法。
6.1 栈帧的形成和关闭
栈在内存中是一块特殊的存储空间,它的存储原则是“先进后出”,即最先被存储的数据最后被释放。汇编过程通常使用PUSH指令与POP指令对栈空间执行数据压入和数据弹出操作。栈的结构示意图如图6-1所示。
图 6-1 栈的结构示意图
图6-1为栈结构在内存中的表现形式。栈结构在内存中占用一段连续的存储空间,通过esp与ebp这两个栈指针寄存器来保存当前栈的起始地址与结束地址(又称为栈顶与栈底)。在栈结构中,每4字节的栈空间保存一个数据,像这样的栈顶到栈底之间的存储空间被称为栈帧。
栈帧是如何形成的呢?当栈顶指针esp小于栈底指针ebp时,就形成了栈帧。通常,在VC++中,栈帧中可以寻址的数据有局部变量、函数返回地址、函数参数等。
不同的两次函数调用,所形成的栈帧也不相同。当由一个函数进入到另一个函数中时,就会针对调用的函数开辟出其所需的栈空间,形成此函数的栈帧。当这个函数结束调用时,需要清除掉它所使用的栈空间,关闭栈帧,我们把这一过程称为栈平衡。
为什么要进行栈平衡呢?这就像借钱一样,“有借有还,再借不难”。如果某一函数在开辟了新的栈空间后没有进行恢复,或者过度恢复,那么将会造成栈空间的上溢或下溢,极有可能给程序带来致命性的错误,如图6-2所示。
图 6-2 栈平衡错误
图6-2中的错误是由栈平衡引发的,只有在Debug版下才会出现这个错误提示,方便开发人员找到错误并修正。在进入某个函数的具体实现代码之前,一般会预先保存栈底指针ebp,以便退出函数时还原以前的栈底。在退出函数时,会将栈底指针ebp与栈顶指针esp进行对比,检测当前栈帧是否被正确关闭,以及栈顶与栈底是否平衡。如果不平衡,则调用函数__chkesp,弹出如图6-2所示的栈平衡错误提示对话框。示例代码如代码清单6-1所示。
代码清单6-1 栈指针保存与平衡检查
//C++源码说明:一个空函数
int main(){
return 0;
}
//C++源码与对应的汇编代码讲解
int main(){
;以下是进入函数时的代码
00401010 push ebp;进入函数后的第一件事,保存栈底指针ebp
00401011 mov ebp, esp;调整当前栈底指针位置到栈顶
00401013 sub esp,40h;抬高栈顶esp,此时开辟栈空间0x40,作为局部变量的存储空间
00401016 push ebx;保存寄存器ebx
00401017 push esi;保存寄存器esi
00401018 push edi;保存寄存器edi
00401019 lea edi,[ebp-40h];取出此函数可用栈空间首地址
0040101C mov ecx,10h;设置ecx为0x10
00401021 mov eax,0CCCCCCCCh;将局部变量初始化为0CCCCCCCCh
;根据ecx的值,将eax中的内容,以4字节为单位写到edi指向的内存中
00401026 rep stos dword ptr[edi]
;以下是用户编写的函数实现代码
return 0;
0040102A xor eax, eax;设置返回值为0
}
;以下是函数退出时的代码
0040102C pop edi;还原寄存器edi
0040102D pop esi;还原寄存器esi
0040102E pop ebx;还原寄存器ebx
0040102F add esp,40h;降低栈顶esp,此时局部变量空间被释放
00401032 cmp ebp, esp;检测栈平衡,如ebp与esp不等,则不平衡
00401034 call_chkesp(00401050);进入栈平衡错误检测函数
00401039 mov esp, ebp;还原esp
0040103B pop ebp
0040103C ret
在代码清单6-1中,进入函数后,先保存原来的ebp,然后调整ebp的位置到esp,接下来通过“sub esp,40h”这句指令打开了0x40字节大小的栈空间,这是留给局部变量使用的。如果编译选项组为Debug,则为了调试方便将局部变量初始化为0CCCCCCCCh。
由于在进入函数前打开了一定大小的栈空间,在函数调用结束后需要将这些栈空间释放,因此需要还原环境POP与“add esp,40h”,以降低栈顶这样的指令。将栈顶指针esp、栈底指针ebp还原后,当使用Debug编译选项组的时候还要进行平衡检测,以确保栈帧被正确关闭。
函数__chkesp是Debug编译选项组下独有的函数,用于检测栈平衡。在Debug版下,所有的函数退出时都会使用到这个函数。它的实现代码如代码清单6-2所示。
代码清单6-2__chkesp函数的实现
;此条件跳转根据"cmp ebp, esp"决定是否跳转
00401050 jne__chkesp+3(00401053)
;条件跳转指令执行失败,表示当前栈帧被正确关闭,是平衡的
00401052 ret
;略去部分代码
;压入错误提示信息字符串首地址作为函数参数
0040105E push offset string"The value of ESP was not properl"……(0041f030)
00401063 push offset string""(0041f02c)
00401068 push 2Ah
0040106A push offset string"i386\\chkesp.c"(0041f01c)
0040106F push 1
;调用函数,弹出图6-1所示的错误提示信息对话框
00401071 call_CrtDbgReport(00401330)
;略去部分代码
00401087 ret
使用了O2选项后,将不会存在栈平衡检查的代码,还可能没有保存环境、使用ebp保存当前栈底等一系列操作,代码将变得简洁而高效。