一、裸函数
在正常的函数编译中,即使函数没有定义函数体的内容,编译器也依然会编译出部分汇编指令用来执行函数。
但是如果定义一个裸函数
void _declspec(naked) test()
编译器将不会操作这个函数,不会给其生成汇编指令(但是会在主函数中生成call和jmp指令指向这个裸函数)
可以看到此处全为int 3(断点),编译器没有生成汇编指令,此时函数继续运行将会报错
如果要让这个裸函数正常运行,我们至少要给出一个retn指令
void _declspec(naked) test()
{
_asm //在函数中添加汇编指令的格式 _asm __asm均可
{
retn
}
}
几点注意
- 函数的参数在函数调用之前就已经存在堆栈中了
- 传参顺序由调用约定决定
- 函数的局部变量存在函数缓冲区中
- 函数返回值存在eax或内存中
- 必须平衡堆栈
二、函数的调用约定
1.常见的函数调用约定
调用约定 | 参数入栈顺序 | 平衡堆栈 |
---|---|---|
__cdecl | 从右至左 | 外平栈 |
__stdcall | 从右至左 | 内平栈 |
__fastcall | 前两个参数存ECX/EDX 剩下的:从右至左入栈 |
无需平衡堆栈/内平栈 |
2.__cdecl
c与c++默认使用__cdecl
这种调用约定,使用堆栈传递参数,从最后一个参数开始入栈,平衡堆栈为在函数外平衡,即add ebp,X
的形式。
例子:
int __cdecl test(int x,int y)
{
return x+y;
}
int main(int argc, char* argv[])
{
test(1,2);
return 0;
3.__stdcall
win32操作系统的API函数使用的是__stdcall
这种调用约定,传参规则和__cdecl
一样,但是此调用约定使用内平栈,即函数返回时使用ret x
指令,表示修改eip的值为返回地址并且将esp加立即数。
例子:
int __stdcall Plus2(int x,int y){
return x + y;
}
void main(int argc, char* argv[]){
Plus2();
}
因为有两个参数,所以
ret 8
完成平栈操作
4.__fastcall
使用此调用约定时,前两个参数直接使用寄存器传参,并且这两个参数不需要平衡堆栈,所以当参数少时,使用此约定速度快。
优势:如果一个函数需要不停调用且参数数量少,那么使用__fastcall
效率会高很多,多余2的参数依然会从后到前存入堆栈,并且也是使用内平栈。
例子1:
int __fastcall test(int x,int y)
{
return x+y;
}
int main(int argc, char* argv[])
{
test(1,2);
return 0;
}
可以看到函数没有执行平衡堆栈操作,但需要注意的是:寄存器中的值在参与运算时会先存入缓冲区中,然后再进行运算
例子2:
int __fastcall test(int x,int y,int z,int a)
{
return x+y+z+a;
}
int main(int argc, char* argv[])
{
test(1,2,3,4);
return 0;
}
依然是在call之前将参数存入寄存器,然后多余参数依然放入堆栈,最后使用内平栈(只需要平衡多余参数)
三、判断一个函数参数个数方法
1.错误方法
如果函数使用内平栈,不能单纯根据ret后面的数判断。因为参数可能用push存入堆栈,还可能用mov存入寄存区。平衡堆栈只需要平衡内存中的那一部分,寄存器部分不需要平衡。
也不能根据call指令前面的push和mov指令数量判断参数。可能push的参数不是相邻call指令需要用到的参数。
2.正确方法
一般情况:把push入栈指令或mov指令,和add指令或ret指令结合起来看。
稳妥办法:进入call指令中分析汇编
00401050 push ebp
00401051 mov ebp,esp
00401053 sub esp,48h
00401056 push ebx
00401057 push esi
00401058 push edi
00401059 push ecx
0040105A lea edi,[ebp-48h]
0040105D mov ecx,12h
00401062 mov eax,0CCCCCCCCh
00401067 rep stos dword ptr [edi]
00401069 pop ecx
0040106A mov dword ptr [ebp-8],edx //这里有一个,而且发现函数中没有给edx赋值的语句
0040106D mov dword ptr [ebp-4],ecx //这里有一个,而且发现函数中没有给ecx赋值的语句
00401070 mov eax,dword ptr [ebp-4]
00401073 add eax,dword ptr [ebp-8]
00401076 add eax,dword ptr [ebp+8] //结合下面的ret指令判断
00401079 mov [g_x (00427958)],eax //修改全局变量
0040107E pop edi
0040107F pop esi
00401080 pop ebx
00401081 mov esp,ebp
00401083 pop ebp
00401084 ret 4 //上面两个寄存器,再结合这里的4,可以基本确定函数有3个参数
四、使用裸函数实现功能
int Plus(int x,int y,int z)
{
int a = 2;
int b = 3;
int c = 4;
return x+y+z+a+b+c;
}
int main(int argc, char* argv[]){
Plus(5,6,7);
return 0;
}
裸函数定义:
int __declspec(naked) test(int x,int y,int z)
{
__asm{
push ebp
//提升堆栈
mov ebp,esp
sub esp,0x40
//保留现场
push ebx
push esi
push edi
//填充缓冲区
lea edi,dword ptr es:[ebp-0x40]
mov ecx,0x10
mov eax,0xcccccccc
rep stosd //rep stos dword ptr ds:[edi]
//局部变量存入缓冲区
mov dword ptr es:[ebp-0x4],0x2
mov dword ptr es:[ebp-0x8],0x3
mov dword ptr es:[ebp-0xC],0x4
//实现函数功能
mov eax,dword ptr es:[ebp+0x8]
add eax,dword ptr es:[ebp+0xC]
add eax,dword ptr es:[ebp+0x10]
add eax,dword ptr es:[ebp-0x4]
add eax,dword ptr es:[ebp-0x8]
add eax,dword ptr es:[ebp-0xC]
//恢复现场
pop edi
pop esi
pop ebx
//降低堆栈
mov esp,ebp
pop ebp
//函数返回
ret
}
}
int main(int argc, char* argv[])
{
test(5,6,7);
return 0;
}
标签:调用,函数,int,约定,mov,ebp,dword,ptr
From: https://www.cnblogs.com/yee-l/p/18251118