函数调用的工作流程
- 传递参数:通过栈(_cdecl,_stdcall)或寄存器(_fastcall)。
- 函数调用:使用call指令调用函数,并将返回地址压入栈中。
- 保存栈底:将调用方的栈底寄存器ebp压栈。
- 申请栈空间,并保存寄存器环境:根据局部变量的多少提升esp(add esp,n)来开辟栈空间,用于存放函数的局部变量(VC6.0中debug模式这部分空间初始化为0XCC),并将函数中要用到的寄存器压栈进行保护。
- 函数实现代码
- 恢复寄存器环境:还原栈中保存的寄存器信息
- 平衡栈空间:平衡局部变量使用的栈空间,即进行降低esp操作(sub esp,n)。
- 函数返回:从栈顶取出第二步保存的返回地址
- _cdecl调用方式,执行ret;相当于pop eip;(调用方平衡参数占用的栈空间)
- 非_cdecl调用方式,执行ret n;相当于pop eip; add esp, n;(add esp,n 用于平衡参数占用的栈空间)
栈的形成和关闭
栈是内存中一段特殊的内存空间,后进先出原则。使用push、pop指令进行入栈、出栈操作。esp为栈顶寄存器,ebp为栈底寄存器,栈是向下生长的,即往地址低处生长。若栈非空,则esp指示的地址比ebp指示的地址小;若esp指示地址比ebp指示地址大,则为空栈。在VC6.0中,栈中可寻址的数据有局部变量、函数返回地址、函数参数等。当进行函数调用时,系统会开辟此函数所需的栈空间,而函数调用结束时,将释放开辟的栈空间,这一过程称为栈平衡。
函数调用方式说明
- _cdecl: C/C++默认的调用方式,调用方平衡函数参数所使用的栈空间。在函数返回后执行add esp, x(其中x为所有参数所占用栈空间大小,以字节为单位)
- _stdcall:被调方平衡函数参数所使用的栈空间,即在函数返回时使用ret n指令。不定参数的函数无法使用此种调用方式。
- _fastcall: 使用寄存器传递参数,只使用了ecx和edx,分别传递第一个和第二个参数(从左往右分别为第一个、第二个...),其他参数则用栈传递。此种方式调用效率最高。
- _cdecl和_stdcall对比(VC6.0 Debug模式下)注意:在Release模式下_cdecl参数平衡时可能存在复写传播优化)
图(1)源码:_cdecl与_stdcall的对比——Debug模式
图(2) 上图源代码的反汇编代码
- _fastcall调用方式(VC6.0 Debug模式下)
图(3) 源码 _fastcall示例
图(4)上图源代码的反汇编代码
如上所示,最后的ret 8语句便是被调用平衡参数所占用的栈空间。而在调用方call @ILT+15(ShowFast)(00401014)后不用平衡参数所用栈空间。
函数参数
函数参数通过栈结构或寄存器进行传递,在C++代码中,传参顺序是自右向左依次入栈。参数也是函数中的变量。但与函数中局部变量的存放地址有些许不同。相对esp来说,局部变量存放地址偏移量为负数,而参数存放地址偏移量为正数。
采用非引用形参,则push指令将形参复制到栈顶(即为实参的副本),与实参数据在两个不同的地址处,是独立存在的,因此对函数参数的修改,实际上是对当前保存在栈内参数中保存的值进行修改,与实参数据没有关系。所以说,在C/C++中,形参是实参的副本,对形参的修改不影响其实参。
函数返回值
调用函数时,先将参数入栈,再执行call指令后,将返回地址压入栈中,调用完毕后执行ret指令,弹出返回地址到eip,并进行参数所用栈平衡操作。在VC中使用寄存器eax来保存返回值,若返回值大小打于4字节,则使用eax和edx来保存返回值。