什么是裸函数 裸函数的定义 1 2 3 4 返回值类型 __declspec(naked) 自定义函数名() { }
裸函数的本质
空裸函数源码
1 2 3 4 5 6 7 8 9 10 11 #include "stdafx.h" void __declspec(naked) Function(){ } int main (int argc, char * argv[]) { Function(); return 0 ; }
空裸函数反汇编
1 2 3 4 5 6 7 8 9 10 0040 EB88 call @ILT+60 (Function) (00401041 )00401041 jmp Function (0040 eba0) 0040EBA0 int 3 0040EBA1 int 3 0040EBA2 int 3 0040EBA3 int 3 0040EBA4 int 3 0040EBA5 int 3 0040EBA6 int 3 0040EBA7 int 3
可以看到此时裸函数的内容是空的没有任何内容,而普通空函数的反汇编是由一大堆的汇编代码的。
函汇编代码的裸函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include "stdafx.h" void __declspec(naked) Function(){ __asm { ret } } int main (int argc, char * argv[]) { Function(); return 0 ; }
添加指令的裸函数反汇编
1 2 3 4 5 0040 EB88 call @ILT+60 (Function) (00401041 )00401041 jmp Function (0040 eba0) 0040EBA0 ret
从上面的实验可以看到,裸函数是不生成任何汇编指令。它只能由用户在指定这个函数该怎么操作。
函数的返回值框架 无参数无返回值的函数框架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void __declspec(naked) Function(){ __asm { push ebp mov ebp,esp sub esp,0x40 push ebx push esi push edi lea edi,dword ptr ss:[ebp-0x40 ] mov ecx,0x10 mov eax,0xCCCCCCCC rep stos dword ptr es:[edi] pop edi pop esi pop ebx mov esp,ebp pop ebp ret } }
有参数有返回值的框架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 int __declspec(naked) Function(int x,int y){ __asm { push ebp mov ebp,esp sub esp,0x40 push ebx push esi push edi lea edi,dword ptr ss:[ebp-0x40 ] mov eax,0xCCCCCCCC mov ecx,0x10 rep stos dword ptr es:[edi] mov eax,dword ptr ss:[ebp+8 ] add eax,dword ptr ss:[ebp+0xC ] pop edi pop esi pop ebx mov esp,ebp pop ebp ret } }
调用约定 常见的几种调用约定
调用约定
参数压栈顺序
平衡堆栈
__cdecl
从右至做入栈
调用者清理栈
__stdcall
从右至做入栈
自身清理栈
__fastcall
ECX/EDX传送前两个,剩下从右至左入栈
自身清理栈
源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include "stdafx.h" int __cdecl plus1 (int x, int y) { return x+y; } int __stdcall plus2 (int x,int y) { return x+y; } int __fastcall plus3 (int x,int y) { return x+y; } int __fastcall plus4 (int x,int y,int z,int a) { return x+y+z+a; } int main (int argc, char * argv[]) { plus1(1 ,2 ); return 0 ; }
__cdecl 的反汇编特征(plus1) 1 2 3 4 0040B 658 push 2 0040B 65A push 1 0040B 65C call @ILT+35 (plus1) (00401028 )0040B 661 add esp,8
这里用的是 push 来存放参数的
堆栈平衡是用调用者(main函数)来实现堆栈平衡的,属于外平栈
__stdcall 的反汇编特征(plus2) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 0040B 658 push 2 0040B 65A push 1 0040B 65C call @ILT+45 (plus2) (00401032 )00401032 jmp plus2 (0040b 610) 0040B610 push ebp 0040B611 mov ebp,esp 0040B613 sub esp,40h 0040B616 push ebx 0040B617 push esi 0040B618 push edi 0040B619 lea edi,[ebp-40h] 0040B61C mov ecx,10h 0040B621 mov eax,0CCCCCCCCh 0040B626 rep stos dword ptr [edi] 0040B628 mov eax,dword ptr [ebp+8] 0040B62B add eax,dword ptr [ebp+0Ch] 0040B62E pop edi 0040B62F pop esi 0040B630 pop ebx 0040B631 mov esp,ebp 0040B633 pop ebp 0040B634 ret 8
使用 push 存放参数
由函数自身(plus2函数)使用 ret 8 指令进行堆栈平衡,属于内平栈
__fastcall 的反汇编特征 plus3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 0040B 658 mov edx,2 0040B 65D mov ecx,1 0040B 662 call @ILT+40 (plus2) (0040102 d)0040102 D jmp plus3 (0040b 530) 0040B530 push ebp 0040B531 mov ebp,esp 0040B533 sub esp,48h 0040B536 push ebx 0040B537 push esi 0040B538 push edi 0040B539 push ecx 0040B53A lea edi,[ebp-48h] 0040B53D mov ecx,12h 0040B542 mov eax,0CCCCCCCCh 0040B547 rep stos dword ptr [edi] 0040B549 pop ecx 0040B54A mov dword ptr [ebp-8],edx 0040B54D mov dword ptr [ebp-4],ecx 0040B550 mov eax,dword ptr [ebp-4] 0040B553 add eax,dword ptr [ebp-8] 0040B556 pop edi 0040B557 pop esi 0040B558 pop ebx 0040B559 mov esp,ebp 0040B55B pop ebp 0040B55C ret
plus4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 0040B 658 push 4 0040B 65A push 3 0040B 65C mov edx,2 0040B 661 mov ecx,1 0040B 666 call @ILT+50 (plus4) (00401037 )00401037 jmp plus4 (0040b 560) 0040B560 push ebp 0040B561 mov ebp,esp 0040B563 sub esp,48h 0040B566 push ebx 0040B567 push esi 0040B568 push edi 0040B569 push ecx 0040B56A lea edi,[ebp-48h] 0040B56D mov ecx,12h 0040B572 mov eax,0CCCCCCCCh 0040B577 rep stos dword ptr [edi] 0040B579 pop ecx 0040B57A mov dword ptr [ebp-8],edx 0040B57D mov dword ptr [ebp-4],ecx 0040B580 mov eax,dword ptr [ebp-4] 0040B583 add eax,dword ptr [ebp-8] 0040B586 add eax,dword ptr [ebp+8] 0040B589 add eax,dword ptr [ebp+0Ch] 0040B58C pop edi 0040B58D pop esi 0040B58E pop ebx 0040B58F mov esp,ebp 0040B591 pop ebp 0040B592 ret 8
前 2 个参数存储到 ecx 和 edx 寄存器中,后面的参数用 push 存储参数
有函数自身(plus4)进行堆栈平衡,属于内平栈
总结:fastcall 相对于其他两个调用约定,运行起来会比较快。当要频繁使用一个函数时,就可以使用这个调用约定
数据类型与数据存储 数据类型的三要素
存储数据的宽度
存储数据的格式
作用范围(作用域)
数据类型的分类
基本类型
构造类型
指针类型
空类型(void)
基本类型
基本类型的数据宽度
1 2 3 4 5 6 7 char 8bit 1字节 short 16bit 2字节 int 32bit 4字节 long 32bit 4字节
char、short、int类型的赋值 和 反汇编
示例一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void plus () { char i = 0xFF ; short x = 0xFF ; int y = 0xFF ; } 0040B 5D8 mov byte ptr [ebp-4 ],0F Fh0040B 5DC mov word ptr [ebp-8 ],offset plus+20 h (0040b 5e0)0040B 5E2 mov dword ptr [ebp-0 Ch],0F Fh
总结: char、short、int 类型的宽度为 byte、short、int
示例二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void plus () { char i = 0x12345678 ; short x = 0x12345678 ; int y = 0x12345678 ; } char i = 0x12345678 ; 0040B 5D8 mov byte ptr [ebp-4 ],78 h short x = 0x12345678 ; 0040B 5DC mov word ptr [ebp-8 ],offset plus+20 h (0040b 5e0) int y = 0x12345678 ; 0040B 5E2 mov dword ptr [ebp-0 Ch],12345678 h
总结:char、short、int 类型分别能存储 1byte、2byte、4byte 的数据(从后向前存储),超过宽度的数据会被丢弃。
有符号和无符号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void plus () { char i = 0xFF ; unsigned char k = 0xFF ; printf ("%d %d\n" ,i,k); } 10 : char i = 0xFF ;0040B 5D8 mov byte ptr [ebp-4 ],0F Fh11 : unsigned char k = 0xFF ;0040B 5DC mov byte ptr [ebp-8 ],0F Fh
浮点数 1 2 3 4 5 6 7 8 9 void plus () { float i = 12.5f ; } float i = 12.5f ; 00401038 mov dword ptr [ebp-4 ],41480000 h
CPU 在运算的时候,将小数 12.5 变成了 41480000h
存储浮点类型原理 任何一个整数在计算机中存储都是可以转换成二进制进行存储的,但是小数确不能。小数不能转换成二进制进行存储,所以小数的存储方式和整数的存储方式是完全不一样的。
float 和 double 在存储方式上都是遵从 IEEE 规范的
float 存储方式 一共存储 4 字节,32 个二进制数
31
30~22
22~0
1bit
8bit
23bit
符号位
指数部分
尾数部分
double 的存储方式 一共存储 8 字节,64 个二进制数
63
62~51
52~0
1bit
11bit
52bit
符号位
指数部分
尾数部分
float 和 double 的存储步骤
先将这个实数的绝对值化为二进制格式
将这个二进制格式实数的小数点左移或右移n位,直到小数点移动到第一个有效数字的右边。
从小数点右边第一位开始数出二十三位数字放入第22到第0位。
如果实数是正的,则在第31位放入“0”,否则放入“1”。
如果n 是左移得到的,说明指数是正的,第30位放入“1”。如果n是右移得到的或n=0,则第30位放入“0”。
如果n是左移 得到的,则将n减去1 后化为二进制,并在左边加“0”补足七位,放入第29到第23位。 ;如果n是右移 得到的或n=0,则将n减去1 化为二进制后在左边加“0”补足七位,再各位求反(补的 0 取反) ,再放入第29到第23位。
将 8.25 转换成二进制
整数部分转换成二进制
计算方法:整数不停地取余2,直到整数值为 0。获取余数,高位在下,低位在上
1 2 3 4 5 6 8/2=4 0 4/2=2 0 2/2=1 0 1/2=0 1 结果:1000
小数部分转换成二进制
计算方法:小数部分不停地乘以2,直到小数为 0。获取整数,高位在上,低位在下
1 2 3 4 0.25*2=0.5 0 0.5*2=1.0 1 结果:10
8.25 转换成二进制的结果为:1000.01
将二进制格式小数点左移或右移 n 位,知道小数点在第一个有效位的右边 简单来说就是要把小数点移到第一个 1 的右边
1000.01
第一个有效位是第一个 1,也就是说小数点移过去之后变成 1.00001,1000.01 = 1.00001 * 2 的 3 次方
结果是:1.00001
存放尾数部分 计算方法:将小数点右边第一位到第二十三位放到尾数部分
尾为数部分:00001000000000000000000
存放符号部分 计算方法:如果实数是正的,则在第31位放入“0”,否则放入“1”。
这里符号部分是正数,所以符号部分值为0
符号部分:0
存放指数部分(符号位) 计算方法:如果n 是左移得到的,说明指数是正的,第30位放入“1”。如果n是右移得到的或n=0,则第30位放入“0”。
这里是左移,所以第 30 位存 1
指数部分(符号位):1
存放指数部分(值) 计算方法:
如果n是左移得到的,则将n减去1后化为二进制,并在左边加“0”补足七位,放入第29到第23位。
如果n是右移得到的或n=0,则将n减去1化为二进制后在左边加“0”补足七位,再各位求反,再放入第29到第23位。
(n-1) 转换成二进制 左移 3 位,就是我们指数 3
3-1=2=0010
在左边加 0,不足7位
指数部分(值):0000010
8.25 存储的结果
将之前算出来的值整合一下得到如下二进制
符号部分
指数部分
尾数部分
0
10000010
00001000000000000000000
将得出来的值以字节分开,并算出最终结果
二进制
0100
0001
0000
0100
0000
0000
0000
0000
十六进制
4
1
0
4
0
0
0
0
代码演示 1 2 3 4 5 6 7 8 9 void plus () { float i = 8.25f ; } float i = 8.25f ; 00401038 mov dword ptr [ebp-4 ],41040000 h
最后我们通过反汇编算出了小数 8.25 存储的值。
计算float 8.25 存储的值
二进制
1100
0001
0000
0100
0000
0000
0000
0000
十六进制
C
1
0
4
0
0
0
0
0.25 存储转换成2进制
整数部分转换(取余2)
小数部分转换(乘2,取整)
1 2 0.25 *2 =0.5 0 0.5 *2 =1.0 1
0.25 转换成二进制为:0.01
进行右移 0.01=1.0*2 的 -2 次方
存放尾数 尾数部分:00000000000000000000000
存放符号位 如果实数是正的,则在第31位放入“0”,否则放入“1”。
右移(正数):0
指数 如果 n 是左移得到的,说明指数是正的,第30位放入“1”。如果n是右移得到的或n=0,则第30位放入“0”。
符号位(右移):0
如果n是右移得到的或n=0,则将n减去1化为二进制后在左边加“0”补足七位,再各位求反(补的 0 取反),再放入第29到第23位。
指数位
-2-1=-3 转换成二进制:D=1101
左边补0(补足7位):0001101
取反:1111101
指数整合:01111101
整合结果 1 2 0011 1110 1000 0000 0000 0000 0000 0000 3 E 8 0 0 0 0 0
程序入口 程序的真正入口 main 或 WinMain 是 “语法规定的用户入口”,而不是 “应用程序的入口”。应用程序的入口通常是启动函数
mainCRTStartup 和 wmainCRTStartup 是控制台 环境下多字节编码和Unicode 编码的启动函数.
WinMainCRTStartup 和 WinMainCRTStartup 是windows 环境 下多字节编码和Unicode 编码的启动函数.
main 函数的识别 main 函数调用前,需要按顺序调用如下函数:
1 2 3 4 5 6 7 8 GetVersion() _heap_init() GetCommandLineA() _crtGetEnvironmentStringsA() _GetModuleHandleA() _setargv() _setenvp() _cinit()
这些函数调用结束后,就会调用 main 函数。这些函数都是内存初始化的函数。
main 函数的调用特征 1 mainret main (__argc, __argv, _environ)
main 函数在调用时会将 3 个参数压入栈中。
简单来说就是在看到初始化函数后,再找到调用 3 个参数的函数,这个函数极有可能是 main 函数。这只是针对 VC6 编译器的特点,其他版本的编译器会有些区别。
练习
用__declspec(naked)裸函数实现下面的功能
1 2 3 4 5 6 7 int plus (int x,int y,int z) { int a = 2 ; int b = 3 ; int c = 4 ; return x+y+z+a+b+c; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include "stdafx.h" int __declspec(naked) Plus(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 ss:[ebp-0x40 ] mov ecx,0x10 mov eax,0xCCCCCCCC rep stos dword ptr es:[edi] mov dword ptr ss:[ebp-0x4 ],0x2 mov dword ptr ss:[ebp-0x8 ],0x3 mov dword ptr ss:[ebp-0xC ],0x4 mov eax,dword ptr ss:[ebp+0x8 ] add eax,dword ptr ss:[ebp+0xC ] add eax,dword ptr ss:[ebp+0x10 ] add eax,dword ptr ss:[ebp-0x4 ] add eax,dword ptr ss:[ebp-0x8 ] add eax,dword ptr ss:[ebp-0xC ] pop edi pop esi pop ebx mov esp,ebp pop ebp ret } } int main (int argc, char * argv[]) { Plus(1 ,2 ,3 ); return 0 ; }
将 CallingConvention.exe 逆向成C语言
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include "stdafx.h" int __stdcall plus1 (int x,int y,int z) { return x+y+z; } int __cdecl plus2 (int x,int y) { return x+y; } int __cdecl plus3 (int x,int y) { return x+y; } int __fastcall plus4 (int x,int y,int z,int a,int b) { int i,j; j=plus1(x,y,z); i=plus2(x,y); return plus3(i,j); } int main (int argc, char * argv[]) { plus4(1 ,3 ,4 ,6 ,7 ); return 0 ; }