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寻址