C&汇编基础-数据类型01
9unk Lv5

什么是裸函数

裸函数的定义

1
2
3
4
返回值类型 __declspec(naked) 自定义函数名()
{

}

裸函数的本质

  1. 空裸函数源码
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
0040EB88   call        @ILT+60(Function) (00401041)
00401041 jmp Function (0040eba0)
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. 函汇编代码的裸函数
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
0040EB88   call        @ILT+60(Function) (00401041)

00401041 jmp Function (0040eba0)

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
{
//保存ebp
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
{
//保存ebp
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
0040B658   push        2
0040B65A push 1
0040B65C call @ILT+35(plus1) (00401028)
0040B661 add esp,8
  1. 这里用的是 push 来存放参数的

  2. 堆栈平衡是用调用者(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
0040B658   push        2
0040B65A push 1
0040B65C call @ILT+45(plus2) (00401032)


00401032 jmp plus2 (0040b610)


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
  1. 使用 push 存放参数

  2. 由函数自身(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
0040B658   mov         edx,2
0040B65D mov ecx,1
0040B662 call @ILT+40(plus2) (0040102d)


0040102D jmp plus3 (0040b530)


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
0040B658   push        4
0040B65A push 3
0040B65C mov edx,2
0040B661 mov ecx,1
0040B666 call @ILT+50(plus4) (00401037)


00401037 jmp plus4 (0040b560)


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
  1. 前 2 个参数存储到 ecx 和 edx 寄存器中,后面的参数用 push 存储参数

  2. 有函数自身(plus4)进行堆栈平衡,属于内平栈

总结:fastcall 相对于其他两个调用约定,运行起来会比较快。当要频繁使用一个函数时,就可以使用这个调用约定

数据类型与数据存储

数据类型的三要素

  1. 存储数据的宽度

  2. 存储数据的格式

  3. 作用范围(作用域)

数据类型的分类

  1. 基本类型
1
2
3
整数类型

浮点类型
  1. 构造类型
1
2
3
4
5
数组类型

结构类型

共用体(联合)类型
  1. 指针类型

  2. 空类型(void)

基本类型

  1. 基本类型的数据宽度
1
2
3
4
5
6
7
char    8bit    1字节

short 16bit 2字节

int 32bit 4字节

long 32bit 4字节

char、short、int类型的赋值 和 反汇编

  1. 示例一
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;
}


//反汇编

0040B5D8 mov byte ptr [ebp-4],0FFh


0040B5DC mov word ptr [ebp-8],offset plus+20h (0040b5e0)


0040B5E2 mov dword ptr [ebp-0Ch],0FFh

总结: char、short、int 类型的宽度为 byte、short、int

  1. 示例二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void plus()
{
char i = 0x12345678; //i=0x78
short x = 0x12345678; //x=0x5678
int y = 0x12345678; //y=0x12345678
}

//反汇编

char i = 0x12345678;
0040B5D8 mov byte ptr [ebp-4],78h

short x = 0x12345678;
0040B5DC mov word ptr [ebp-8],offset plus+20h (0040b5e0)

int y = 0x12345678;
0040B5E2 mov dword ptr [ebp-0Ch],12345678h

总结: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; //i=0xFF
unsigned char k = 0xFF; //k=0xFF

printf("%d %d\n",i,k); //i=-1;k=255
}

//反汇编

10: char i = 0xFF;
0040B5D8 mov byte ptr [ebp-4],0FFh

11: unsigned char k = 0xFF;
0040B5DC mov byte ptr [ebp-8],0FFh

浮点数

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],41480000h

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 的存储步骤

  1. 先将这个实数的绝对值化为二进制格式

  2. 将这个二进制格式实数的小数点左移或右移n位,直到小数点移动到第一个有效数字的右边。

  3. 从小数点右边第一位开始数出二十三位数字放入第22到第0位。

  4. 如果实数是正的,则在第31位放入“0”,否则放入“1”。

  5. 如果n 是左移得到的,说明指数是正的,第30位放入“1”。如果n是右移得到的或n=0,则第30位放入“0”。

  6. 如果n是左移得到的,则将n减去1后化为二进制,并在左边加“0”补足七位,放入第29到第23位。
    ;如果n是右移得到的或n=0,则将n减去1化为二进制后在左边加“0”补足七位,再各位求反(补的 0 取反),再放入第29到第23位。

将 8.25 转换成二进制

  1. 整数部分转换成二进制

计算方法:整数不停地取余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
  1. 小数部分转换成二进制

计算方法:小数部分不停地乘以2,直到小数为 0。获取整数,高位在上,低位在下

1
2
3
4
0.25*2=0.5  0
0.5*2=1.0 1

结果:10
  1. 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位。

  1. (n-1) 转换成二进制
    左移 3 位,就是我们指数 3

3-1=2=0010

  1. 在左边加 0,不足7位

指数部分(值):0000010

8.25 存储的结果

  1. 将之前算出来的值整合一下得到如下二进制
符号部分 指数部分 尾数部分
0 10000010 00001000000000000000000
  1. 将得出来的值以字节分开,并算出最终结果
二进制 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],41040000h

最后我们通过反汇编算出了小数 8.25 存储的值。

计算float 8.25 存储的值

二进制 1100 0001 0000 0100 0000 0000 0000 0000
十六进制 C 1 0 4 0 0 0 0

0.25 存储转换成2进制

  1. 整数部分转换(取余2)
1
0/2=0 0
  1. 小数部分转换(乘2,取整)
1
2
0.25*2=0.5 0
0.5*2=1.0 1
  1. 0.25 转换成二进制为:0.01

进行右移

0.01=1.0*2 的 -2 次方

存放尾数

尾数部分:00000000000000000000000

存放符号位

如果实数是正的,则在第31位放入“0”,否则放入“1”。

右移(正数):0

指数

如果 n 是左移得到的,说明指数是正的,第30位放入“1”。如果n是右移得到的或n=0,则第30位放入“0”。

  1. 符号位(右移):0

如果n是右移得到的或n=0,则将n减去1化为二进制后在左边加“0”补足七位,再各位求反(补的 0 取反),再放入第29到第23位。

  1. 指数位

-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 是 “语法规定的用户入口”,而不是 “应用程序的入口”。应用程序的入口通常是启动函数

  1. mainCRTStartup 和 wmainCRTStartup 是控制台环境下多字节编码和Unicode 编码的启动函数.

  2. 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 编译器的特点,其他版本的编译器会有些区别。

练习

  1. 用__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"
/*
分析:
1、压入参数 x,y、z
2、进入函数(push eip)
3、保存栈底
4、提升堆栈
5、保护现场
6、填充缓冲区
7、存储局部变量
8、程序功能(固定的:eax 存储返回值)
9、恢复现场
10、降低堆栈
11、函数返回
*/


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;
}
  1. 将 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[])
{
// int s;
// s = plus4(1,3,4,6,7);
// printf("%X",s);
plus4(1,3,4,6,7);
return 0;
}
  • 本文标题:C&汇编基础-数据类型01
  • 本文作者:9unk
  • 创建时间:2021-04-01 14:18:28
  • 本文链接:https://9unkk.github.io/2021/04/01/c-hui-bian-ji-chu-shu-ju-lei-xing-01/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!