今天研究了一下vc6函数调用,看看vc6调用函数时候都做了什么。有些意思。
我写下了如下代码:
int fun(int a,int b)
{
int i = 3;
return a+b+i;
}
int main()
{
int a = 1,b=2;
int result ;
result = fun(1,2);
return result;
}
非常简单。反汇编后(Debug版)变成这样
1: int fun(int a,int b)
2: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,44h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
3: int i = 3;
00401038 mov dword ptr [ebp-4],3
4: return a+b+i;
0040103F mov eax,dword ptr [ebp+8]
00401042 add eax,dword ptr [ebp+0Ch]
00401045 add eax,dword ptr [ebp-4]
5: }
00401048 pop edi
00401049 pop esi
0040104A pop ebx
0040104B mov esp,ebp
0040104D pop ebp
0040104E ret
7: int main()
8: {
00401060 push ebp
00401061 mov ebp,esp
00401063 sub esp,4Ch
00401066 push ebx
00401067 push esi
00401068 push edi
00401069 lea edi,[ebp-4Ch]
0040106C mov ecx,13h
00401071 mov eax,0CCCCCCCCh
00401076 rep stos dword ptr [edi]
9: int a = 1,b=2;
00401078 mov dword ptr [ebp-4],1
0040107F mov dword ptr [ebp-8],2
10: int result ;
11: result = fun(1,2);
00401086 push 2
00401088 push 1
0040108A call @ILT+5(fun) (0040100a)
0040108F add esp,8
00401092 mov dword ptr [ebp-0Ch],eax
12: return result;
00401095 mov eax,dword ptr [ebp-0Ch]
13: }
00401098 pop edi
00401099 pop esi
0040109A pop ebx
0040109B add esp,4Ch
0040109E cmp ebp,esp
004010A0 call __chkesp (004010c0)
004010A5 mov esp,ebp
004010A7 pop ebp
004010A8 ret
我们主要来看看函数调用部分
1.参数压栈push 2
push 1
参数从右向左压栈(__cdcel),esp递减
2.调用函数 call @ILT+5(fun) (0040100a)
这条指令会把下一行代码的地址压栈,也就是函数返回地址。同时跳转到函数入口处
3.进入函数体push ebp
mov ebp,esp
首先保存ebp的地址,然后把esp保存到ebp中去
00401023 sub esp,44h
00401026 push ebx
00401027 push esi
00401028 push edi
减小stack的指针(注意,stack是从内存的高端向低端生长的),为局部变量保留一些空间,这里的44h不是固定的,由编译器计算得来
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
用0xCC填充局部变量空间。这是Debug模式特有的,如果是字符串,你就看到被初始化成"烫烫烫烫烫烫"
至此,整个堆栈变成
|-----------------|
| 局部变量2 |
|-----------------|
| 局部变量1 |<----ebp-4
|-----------------|
| old ebp |<----ebp
|-----------------|
| 函数返回地址| <----ebp+4
|-----------------|
| 参数1 | <----ebp+8
|-----------------|
| 参数2 |
|-----------------|
Next:
int i = 3;
00401038 mov dword ptr [ebp-4],3
这里你看到[ebp-4]就是第一个局部变量i了
0040103F mov eax,dword ptr [ebp+8]
00401042 add eax,dword ptr [ebp+0Ch]
00401045 add eax,dword ptr [ebp-4]
[ebp+8],[ebp+0Ch]分别是a和b了
4.函数返回函数的结果都是放在eax中(ps:你可以在vc的watch窗口输入@EAX,就可以直接看到函数返回值了)
00401048 pop edi
00401049 pop esi
0040104A pop ebx
0040104B mov esp,ebp
0040104D pop ebp
0040104E ret
把edi,esi,ebx恢复,然后恢复esp,ebp,这时函数的返回地址就在栈顶,调用ret就可以返回了。
那如果改变函数的返回地址会怎样?
ok,我们修改一下代码:
#include <stdio.h>
void fun2()
{
printf("fun2() called");
}
int fun(int a,int b)
{
int i = 3;
printf("return address:0x%x\n",&i+2);
printf("fun2 address:0x%x\n",&fun2);
/*int *p = (int*)&fun2;
__asm
{
mov ebx,p
mov dword ptr[ebp+4],ebx
}*/
*(&i+2)=(int)&fun2; //modify return address
return a+b+i;
}
int main()
{
int a = 1,b=2;
int result ;
result = fun(1,2);
return result;
}
Wow,这时,我们就会发现fun2被调用了。这就是Buffer overrun(缓冲溢出)所做的事情吧。
5.最后一步,调用者调整堆栈指针call @ILT+5(fun) (0040100a)
add esp,8
为什么要调整呢,因为调用之前push两个参数进入栈,现在要恢复它
mov dword ptr [ebp-0Ch],eax
这句话就是享用函数调用的果实了(EAX保存了函数的返回值)