在初学汇编调试的情况下,一开始对各种函数调用方式感觉困惑,查找网上博客介绍,如
http://hi.baidu.com/bmrs/blog/item/0d27ccfa488a3e1b6d22ebaf.html写得很详细,但是如果没有经过自己一番亲身体验,发现实在很难记住。
因此本文经过windbg单机或者双机调试,验证各种函数调用方式。
(1)首先,我们跟踪__stdcall.
main函数如下:
DWORD main()
{
__asm
{
int 3
push 1
call F0
}
}
跟踪如下函数的调用:
DWORD
__stdcall
F0(DWORD a)
{
return
a;
}
查看反汇编代码如下:
001b:00401000 55 push ebp //保存调用函数的ebp
001b:00401001 8bec mov ebp,esp//ebp作为一个浮标
001b:00401001 8bec mov ebp,esp//ebp作为一个浮标
001b:00401003 8b4508 mov eax,dword ptr [ebp+8]
001b:00401006 5d pop ebp //恢复调用函数的ebp
001b:00401007 c20400 ret 4 //ret imm8 根据参数的字节而定,从堆栈上弹出返回地址给eip,cs,然后给esp加上一个number值。
001b:00401007 c20400 ret 4 //ret imm8 根据参数的字节而定,从堆栈上弹出返回地址给eip,cs,然后给esp加上一个number值。
以上是一个参数的情况,_stdcall函数调用方式默认为我们在被调用函数头部和尾部做了一些工作,并自动恢复堆栈,也就是由被调用函数清理参数占用的堆栈。那么如果是多个参数传递的话,参数传递顺序如何呢?
main函数如下:
int 3
push 1
push 2
call F2
F2函数如下:
DWORD __stdcall F2(DWORD a,DWORD b)
{
printf("%d/n", a);
return(a+b);
}
查看反汇编代码如下:
00401000 55 push ebp
00401001 8bec mov ebp,esp
00401003 56 push esi
00401004 8b7508 mov esi,dword ptr [ebp+8] ss:0023:0012ff74=00000002//从这里我们可以看出a=2
00401007 56 push esi
00401008 68a0994000 push 4099A0h
0040100d e828000000 call 0040103a//调用printf
00401012 8b450c mov eax,dword ptr [ebp+0Ch]
00401015 83c408 add esp,8
00401018 03c6 add eax,esi
0040101a 5e pop esi
0040101b 5d pop ebp
0040101c c20800 ret 8 //两个int参数,8个字节
00401001 8bec mov ebp,esp
00401003 56 push esi
00401004 8b7508 mov esi,dword ptr [ebp+8] ss:0023:0012ff74=00000002//从这里我们可以看出a=2
00401007 56 push esi
00401008 68a0994000 push 4099A0h
0040100d e828000000 call 0040103a//调用printf
00401012 8b450c mov eax,dword ptr [ebp+0Ch]
00401015 83c408 add esp,8
00401018 03c6 add eax,esi
0040101a 5e pop esi
0040101b 5d pop ebp
0040101c c20800 ret 8 //两个int参数,8个字节
从 a=2,我们可以判断出参数入栈顺序方式是从右到左。
当我在写驱动函数调试的时候,反汇编用IDA查看,也是使用_stdcall调用。
(2)接下来,我们看看__cdecl的函数调用方式。
,main函数:
int 3
push 1
push 2
call F2
被调用函数如下:
DWORD
__cdecl
F1(DWORD a,DWORD b)
{
printf("%d/n",a);
return
(a+b);
}
反汇编代码如下:
00401000 55 push ebp
00401001 8bec mov ebp,esp
00401003 56 push esi
00401004 8b7508 mov esi,dword ptr [ebp+8] //2
00401007 56 push esi
00401008 68a0994000 push 4099A0h
0040100d e828000000 call 0040103a//printf a=2
00401012 8b450c mov eax,dword ptr [ebp+0Ch]
00401015 83c408 add esp,8
00401018 03c6 add eax,esi
0040101a 5e pop esi
0040101b 5d pop ebp
00401001 8bec mov ebp,esp
00401003 56 push esi
00401004 8b7508 mov esi,dword ptr [ebp+8] //2
00401007 56 push esi
00401008 68a0994000 push 4099A0h
0040100d e828000000 call 0040103a//printf a=2
00401012 8b450c mov eax,dword ptr [ebp+0Ch]
00401015 83c408 add esp,8
00401018 03c6 add eax,esi
0040101a 5e pop esi
0040101b 5d pop ebp
0040101c c3 ret//注意这边并没有number,因为被调用函数并不知道参数的个数,所以栈的平衡由调用函数去维护。
从以上代码,我们知道参数很明显也是从右到左入栈的,但是当我们退出被调用函数的时候,查看esp如下:
0012ff74 00000002 00000001 0040122b
也就是说被调用函数退出的时候,栈没有回收传递的参数。故而我们运行到main函数如下:
00401025 e8d6ffffff call 00401000
0040102a c3 ret
0040102a c3 ret
在ret的时候,本来main函数应该返回到0040122b这个地址,可是ret返回的是00000002。
No prior disassembly possible
00000002 ?? ???
00000002 ?? ???
因此作为调用函数的main必须回收参数占用的空间。故而,_cdecl适用于变参传递。因为在编译阶段,被调用函数不需要懂得去清理多少的堆栈空间(参数不定,ret number无法确定)
加上:add esp,8
(3)__fastcall
被调用函数如下:
DWORD __fastcall F3(DWORD a,DWORD b,DWORD c,DWORD d)
{
__asm
{
int 3
mov eax,d
mov ebx,c
add eax,ebx
}
}
main函数如下:
F3(1,2,3,4);
反汇编代码:
main函数:
1!main:
01111020 6a04 push 4 //从右边参数先入栈
01111022 ba02000000 mov edx,2//edx保存参数2
01111027 6a03 push 3
01111029 8d4aff lea ecx,[edx-1]//ecx保存参数1,请问为什么不是mov ecx,edx-1
0111102c e8cfffffff call 1!F3 (01111000)
01111031 33c0 xor eax,eax
01111033 c3 ret
01111020 6a04 push 4 //从右边参数先入栈
01111022 ba02000000 mov edx,2//edx保存参数2
01111027 6a03 push 3
01111029 8d4aff lea ecx,[edx-1]//ecx保存参数1,请问为什么不是mov ecx,edx-1
0111102c e8cfffffff call 1!F3 (01111000)
01111031 33c0 xor eax,eax
01111033 c3 ret
被调用函数如下:
1!F3:
01111000 55 push ebp
01111001 8bec mov ebp,esp
01111003 53 push ebx
01111004 cc int 3
01111005 8b450c mov eax,dword ptr [ebp+0Ch]//4
01111008 8b5d08 mov ebx,dword ptr [ebp+8]//3
0111100b 03c3 add eax,ebx
0111100d 5b pop ebx
0111100e 5d pop ebp
0111100f c20800 ret 8 //3,4两个参数通过栈去传递
01111000 55 push ebp
01111001 8bec mov ebp,esp
01111003 53 push ebx
01111004 cc int 3
01111005 8b450c mov eax,dword ptr [ebp+0Ch]//4
01111008 8b5d08 mov ebx,dword ptr [ebp+8]//3
0111100b 03c3 add eax,ebx
0111100d 5b pop ebx
0111100e 5d pop ebp
0111100f c20800 ret 8 //3,4两个参数通过栈去传递
总结如下,__fastcall名副其实,就是快速调用,利用寄存器去传递有限的(我在win7和winxp3下都是2,这个和编译器相关)的参数,
其他的参数利用栈传递,传递方式和__stdcall一样,从右向左入栈,同时由被调用函数去负责清理栈。
(4)naked
main函数:
DIY(1,2,3);
被调用函数:
DWORD _declspec(naked) DIY( DWORD a, DWORD b, DWORD c )
{
__asm
{
push ebp
mov ebp,esp
mov eax,a
add eax, b
add eax, c
pop ebp
ret
}
}
反汇编代码如下:
1!main:
01321010 6a03 push 3//右边参数先入栈
01321012 6a02 push 2
01321014 6a01 push 1
01321016 e8e5ffffff call 1!DIY (01321000)
0132101b 83c40c add esp,0Ch//调用函数自己清理栈
0132101e 33c0 xor eax,eax
01321020 c3 ret
01321010 6a03 push 3//右边参数先入栈
01321012 6a02 push 2
01321014 6a01 push 1
01321016 e8e5ffffff call 1!DIY (01321000)
0132101b 83c40c add esp,0Ch//调用函数自己清理栈
0132101e 33c0 xor eax,eax
01321020 c3 ret
1!DIY:
01321000 cc int 3
01321001 55 push ebp
01321002 8bec mov ebp,esp
01321004 8b4508 mov eax,dword ptr [ebp+8]
01321007 03450c add eax,dword ptr [ebp+0Ch]
0132100a 034510 add eax,dword ptr [ebp+10h]
0132100d 5d pop ebp
0132100e c3 ret
01321000 cc int 3
01321001 55 push ebp
01321002 8bec mov ebp,esp
01321004 8b4508 mov eax,dword ptr [ebp+8]
01321007 03450c add eax,dword ptr [ebp+0Ch]
0132100a 034510 add eax,dword ptr [ebp+10h]
0132100d 5d pop ebp
0132100e c3 ret
可以看出,naked顾名思义,裸露的,需要你为自己去维护入栈,出栈。这个和别的调用方式是不同的,别的调用方式会帮助我们保存恢复寄存器。
如果我再DIY中修改esi寄存器,那么,等我们推出函数,是否会恢复esi,我们在DIY中加入
mov esi, 44444
1!DIY:
012e1000 cc int 3
012e1001 be9cad0000 mov esi,0AD9Ch
012e1006 55 push ebp
012e1007 8bec mov ebp,esp
012e1009 8b4508 mov eax,dword ptr [ebp+8]
012e100c 03450c add eax,dword ptr [ebp+0Ch]
012e100f 034510 add eax,dword ptr [ebp+10h]
012e1012 5d pop ebp
012e1001 be9cad0000 mov esi,0AD9Ch
012e1006 55 push ebp
012e1007 8bec mov ebp,esp
012e1009 8b4508 mov eax,dword ptr [ebp+8]
012e100c 03450c add eax,dword ptr [ebp+0Ch]
012e100f 034510 add eax,dword ptr [ebp+10h]
012e1012 5d pop ebp
012e1013 c3 ret
我们查看寄存器发现esi从0变化为
ad9c,从而说明naked调用方式并没有为我们保存恢复寄存器。故而需要我们自己去为这个裸露的函数穿上外衣。