6.6 回顾

到此为止,对函数工作原理的分析就结束了,大家是否已经掌握了呢?下面再巩固一下所学知识。

1.函数调用的一般工作流程

(1)参数传递

通过栈或寄存器方式传递参数。

(2)函数调用,将返回地址压栈

使用call指令调用参数,并将返回地址压入栈中。

(3)保存栈底

使用栈空间保存调用方的栈底寄存器ebp。

(4)申请栈空间和保存寄存器环境

根据函数内局部变量的大小抬高栈顶让出对应的栈空间,并且将即将修改的寄存器保存在栈内。

(5)函数实现代码

函数实现过程的代码。

(6)还原环境

还原栈中保存的寄存器信息。

(7)平衡栈空间

平衡局部变量使用的栈空间。

(8)ret返回,结束函数调用

从栈顶取出第(2)步保存的返回地址,更新EIP。

在非__cdecl调用方式下,平衡参数占用栈空间。

(9)调整esp,平衡栈顶

此处为__cdecl特有的方式,用于平衡参数占用的栈顶。

2.两种编译选项下的函数识别

Debug编译选项组下的函数识别非常简单,由于其注重调试的特性,其汇编代码基本上就是函数的原貌,只需对照汇编代码逐条分析即可将其还原成高级代码。其识别要点见代码清单6-10。

代码清单6-10 Debug编译选项组下的函数识别


push reg/mem/imm;根据调用函数查看参数使用,可确定是否为参数

……

call reg/mem/imm;调用函数

add esp, xxxx;如果Debug编译选项组下是_cdecl调用方式,由调用方平衡栈顶

jmp FUN_ADDR;call指令调用处,可能存在使用跳转指令执行到函数

FUN_ADDR:

push ebp;保存栈底

……

mov eax,0CCCCCCCCh

rep stos dword ptr[edi];初始化局部变量

……

pop ebp;还原栈底

ret;查看ret是否平衡栈


在O2选项下,_cdecl调用方式的函数调用结束后,并不一定会马上平衡栈顶,极有可能会复写传播并与其他函数一起平衡栈。由于函数实现改用了esp寻址,因此需要注意函数执行过程是否对esp进行了修改,如进行了修改,在平衡栈顶esp时,考察是否有对esp进行平衡恢复。当函数有参数时,检查参数是否在函数实现中被平衡,以确定其调用方式。_cdecl调用方式的函数识别要点见代码清单6-11。

代码清单6-11 Release版函数识别


push reg/mem/imm;根据调用函数查看参数使用,可确定是否为参数

……

call reg/mem/imm;调用函数

add esp, xxxx;在Release版下调用__cdecl方式的函数,栈平衡可能会复写传播,请注意

;函数实现内没有将局部变量初始化为0CCCCCCCCh

;若在函数体内不存在内联汇编或异常处理等代码,则使用esp寻址