一、引入问题
随便问一个软件开发人员,函数调用的时候发生了什么,大家都会说出,“函数先保存环境,再执行函数,再恢复环境,再返回”,这样是不错,但是不够具体,我们要知道函数的调用约定、帧栈的形成、平栈、函数的识别等具体的细节;
二、函数调用完整的代码及汇编
函数调用部分:
bool bFlag = testFunction(i, l);
00D2244C 8B 45 94 mov eax,dword ptr [l]
00D2244F 50 push eax ;压栈变量i
00D22450 8B 4D E8 mov ecx,dword ptr [i]
00D22453 51 push ecx ;压栈变量l
00D22454 E8 3B F3 FF FF call testFunction (0D21794h) ;调用函数
00D22459 83 C4 08 add esp,8 ;平栈,__cdecl
00D2245C 88 45 8B mov byte ptr [bFlag],al
函数定义部分:
/****************************************************
12: 模块:
13: 版本:
14: 作者: XX
15: 时间: 2019/03/15
16: 功能: 两个数相加大于10,返回真,其他返回假
17: 输入参数:
18: 输出参数:
19: 返回值:
20: 其他: 一个简单的函数,用来查看调用函数的时候到底发生了什么。
21: *****************************************************/
22: bool testFunction(int param1, int param2)
23: {
00D27BC0 55 push ebp ;进入函数的第一件事情是压栈调用函数的栈底指针ebp
00D27BC1 8B EC mov ebp,esp ;调整当前栈顶指针到栈底;
00D27BC3 81 EC CC 00 00 00 sub esp,0CCh ;拉高esp,给函数局部变量存储空间,即帧栈空间
00D27BC9 53 push ebx ;保存寄存器ebx
00D27BCA 56 push esi ;保存寄存器esi
00D27BCB 57 push edi ;保存寄存器edi
00D27BCC 8D BD 34 FF FF FF lea edi,[ebp-0CCh] ;edi为分配的栈空间的首地址,用于取出此函数可用的栈空间首地址,用于后面的初始化;
00D27BD2 B9 33 00 00 00 mov ecx,33h ;设置ecx为0x33,这个为count = 0x33次,
00D27BD7 B8 CC CC CC CC mov eax,0CCCCCCCCh ; 将局部变量设置为0CCCCCCCh
00D27BDC F3 AB rep stos dword ptr es:[edi] ; 根据ecx中的值将eax中的内容,以4字节为单位写到edi中,共0x33 * 4 = 0xCC 个字节,就是全部的帧栈被初始化为0CCCCCCCh;
24: int retValue = param1 + param2;
00D27BDE 8B 45 08 mov eax,dword ptr [param1]
00D27BE1 03 45 0C add eax,dword ptr [param2]
00D27BE4 89 45 F8 mov dword ptr [retValue],eax
25: if (retValue > 10)
00D27BE7 83 7D F8 0A cmp dword ptr [retValue],0Ah
00D27BEB 7E 06 jle testFunction+33h (0D27BF3h)
26: {
27: return true;
00D27BED B0 01 mov al,1 ;eax作为返回值
00D27BEF EB 04 jmp testFunction+35h (0D27BF5h)
28: }
29: else
00D27BF1 EB 02 jmp testFunction+35h (0D27BF5h)
30: {
31: return false;
00D27BF3 32 C0 xor al,al ;eax为返回值
32: }
33: }
00D27BF5 5F pop edi ;恢复寄存器edi
00D27BF6 5E pop esi ;恢复寄存器esi
00D27BF7 5B pop ebx ;恢复寄存器ebx
32: }
33: }
00D27BF8 8B E5 mov esp,ebp ; 调整栈底为栈顶(还原esp)
00D27BFA 5D pop ebp ;
00D27BFB C3 ret ;取得esp指向的4字节为函数返回地址,更新EIP,程序回到返回地址处,同时,esp + 4进行平栈;
三、调用约定
调用约定很重要,它决定了参数的入栈顺序,参数传入时存入栈区还是寄存器,同时,也决定了由谁来平栈;
_cdecl: C/C++ 默认的调用方式,参数从右向左入栈,调用方平栈,不定参的函数可以使用;
_stdcall: 参数从右向左入栈,被调用方平栈,不定参的函数不能使用;
_fastcall: 寄存器方式传参数,优先将参数存储于ecx (第一个参数) 和 edx(第二个参数) 中,被调用放平栈,不定参的函数不能使用;
被调用方平栈的调用约定,不定参是不能用的,因为被调用者不知道自己会有多少参数,不能确定栈空间大小。
其他:_thiscall: this指针存于ecx,其他和_stdcall 一样;
我们上面的例子中, 调用函数后 add esp,8 表明属于_cdecl 的方式;
具体见:《逆向反汇编:从C/C++到汇编 2.4》
四、帧栈的形成---寄存器ebp和esp
ebp:帧栈的结束地址,栈底,关键字base,高地址在下;
esp:帧栈的起始地址,栈顶,关键字stack,低地址在上;
esp和ebp 就是当前函数的栈帧,里面是该函数的局部变量,所以,ebp也叫做函数框架指针;
进入函数后,esp拉高,开辟帧栈空间,esp小于ebp就形成了帧栈;
在OD调试时,可以在帧栈中选中ebp,enter,就能跳到上层函数的帧栈顶,可以看到函数返回的下一条地址 和当前函数的参数;
ebp 上面四当前函数的帧栈,下面是函数的返回地址和参数(属于调用者);
五、函数的识别
通过上面的分析,我们可以发现,函数的开头都是:
push ebp
mov ebp,esp
sub esp, 0x XX
函数结尾都是:
mov esp,ebp ; 调整栈底为栈顶(还原esp)
00D27BFA 5D pop ebp ;
00D27BFB C3 ret
这就成为我们识别函数的关键所在,逆向时,找到函数头P一下,Hex-ray就能反编译处函数,如果IDA识别错误,就需要进行修改分析,可能时调用约定有问题,可能时平栈的问题,具体的情况见前面发表的《IDA 逆向技巧》7.8~7.10
六、总结
函数调用过程:
1、参数传递
2、函数调用,将返回地址压栈
3、保存栈底 push ebp
4、申请栈空间,保存寄存器环境 sub esp,0CCh ;
00D27BC9 53 push ebx ;保存寄存器ebx
00D27BCA 56 push esi ;保存寄存器esi
00D27BCB 57 push edi ;保存寄存器edi
5、函数代码实现
6、还原环境
7、平栈空间
8、ret 返回,函数调用结束
9、调整esp,平栈顶
七、其他参考(此部分摘自其他博文)
Q: esp指向哪?
A: 就是栈顶,而不是栈顶的下一个元素。
Q: 如何在windbg中查看当前栈?
A: dp esp. p意指 Pointer-sized. 单列模式格式: dp /c1 esp
因为栈是向下生长的,dp是向上显示的,所以dp esp从栈顶开始,向上显示的内存,刚好就是栈的内容。
Q: call指令做了些什么事?
A: call做的仅仅是把返回地址(call的下一条指令的地址)压栈,然后跳转到目标地址。
它不会为参数压栈,也不会保存其它寄存器,更不好为局部变量分配栈空间。这些事都是程序员干的,或是编译器自动完成的。
Q: 为什么需要ebp?
A:esp虽然是栈指针,但它是游动的,只要代码中有栈操作,它就随栈顶变量而变化。在引用局部变量(局部变量存放在脆弱的栈中)时,极不方便。
引入ebp,固定指向调用帧,方便引用局部变量。当然ebp需要手动指定。所以函数开头经常可见如下代码:
push ebp
move ebp esp
调用帧一般结构:(在OD里面,右下角的堆栈窗口,可以看到esp在上,ebp在下)
| | High
| Parameter |
|-------------|
| Parameter |
|-------------|
| Return Addr |
|-------------|
| Old ebp | <-- New ebp
|-------------|
| Local var |
|-------------|
| Local var |
| | Low
---------------------
作者:键盘上的疯兔
来源:CSDN
原文:https://blog.csdn.net/tms_li/article/details/40707549
版权声明:本文为博主原创文章,转载请附上博文链接!