[逆向基础] C++中基本数据类型的表现形式

一、整数类型

1、无符号整数

无符号整数的取值范围:0x00000000 ~ 0xFFFFFFFF。

2、有符号整数

有符号整数的最高位为符号位,取值范围:0x80000000 ~ 0x7FFFFFFF

转换成二进制,分别为:

0x80000000 = 1000 0000 0000 0000 0000 0000 0000 0000
0x7FFFFFFF = 0111 1111 1111 1111 1111 1111 1111 1111

正数的表示区间为:0x00000000 ~ 0x7FFFFFFF
负数的表示区间为:0x80000000 ~ 0xFFFFFFFF

负数在内存中都是以补码的形式存放的,补码的规则是用0减去这个数的绝对值,也可以表示为,对这个数取反加1。例如,对于-3,可以表达为0-3,而 0xFFFFFFFD + 3 等于0 (进位丢失),所以-3的补码也就是0xFFFFFFFD了。

为了计算方便,通常采用取反加1的方式来获得补码,因为对于任何4字节的数值,都有x + x(反)= 0xFFFFFFFF,于是 x + x(反) + 1 = 0

对于4字节的补码,0x80000000所表达的意义,可以是负数0 ,也可以是0x80000001 减 1,由于0的正负值是相等的,因此没有必要还来个负数0,因此将这个值的意义规定为:0x8000000 - 1,这样,0x80000000也就成了4字节负数的最小值,负数区间总是比正数区间多一个最小值的原因了。

在数据分析中,如果将内存解释为有符号整数,则查看16进制表示的最高位,最高位小于8则为正数,大于8则为负数。如果是负数,则需要转换成真值,从而得到对应的负数数值。

问:如何判断一段数据是有符号类型还是无符号类型呢?

答:需要查看指令或者已知的函数如何操作此内存地址,根据操作方式或函数相关定义得出该地址的数据类型。 如,MessageBoxA,它有4个参数,查看帮助得知,第四个参数是一个无符号整数。

二、浮点数类型

1、浮点数类型的编码方式(Float)

只所以叫做浮点数,是因为小数点可以浮动,因此,叫做浮点数。
浮点数是采用IEEE规定的标准编码,float和double这两种类型的转换原理相同。

float类型在内存占4个字节(32位)。 最高位用于表示符号,在剩余的32一位中,从右往左取8位用于表示指数,其余用于表示尾数。

符号位     指数位        尾数位    
0         00000000     00000000000000000000000

double类型在内存中占8个字节。最高位表示符号,指数位占11位,剩余的42位用于表示尾数。

关于浮点数到二进制的转换,参考:http://www.0x520.com/2007/10/16/171.html

下面演示,如何将float类型12.25f转换为IEEE编码:

1、将12.25f转换成二进制数1100.01,整数部分为1100,小数部分为01。
2、小数点向左移动,每移动一次,指数加一。移动到符号位的最高位为1处,停止移动,这里仅移动3次。
3、在IEEE编码中,在二进制情况下,尾数的最高位始终为1,为一个恒定值,故将其忽略不计。

12.25f经IEEE转换后的情况:
符号位:0
指数位:十进制的3+127 ,转换成二进制 10000010
尾数为:10001 000000000000000000(不足23位时,低位补0填充)

4、由于尾数位中最高位始终为1,是恒定值,故省略不计,只要在转换为十进制时加1即可。

5、为什么指数为要加127呢? 由于指数可能出现负数,十进制数127可表示为二进制 01111111 IEEE规定,当指数小于 01111111时为一个负数,反之为正数,因此 01111111为0 。

6、12.25f转换后二进制表示为0 10000010 10001000000000000000000 ,转换成16进制为 0x41440000

下面演示,如何将float类型-0.125转换为IEEE编码:

1、根据科学记数法,小数点向整数部分移动,指数做加法;向小数部分移动,指数做减法。
2、-0.125转换为二进制位 0.001,用科学记数法表示为:1.0 指数位 -3

-0.125转换后的情况
符号位:0
指数位:十进制 127 + (-3),转换为二进制:01111100,如果不足8位,则高位补0
尾数位:00000000000000000000000(23个0)

如果尾数部分转换为二进制后是一个无穷值,则会舍弃部分位数,所以进行IEEE转换后得到的是一个近似值,存在一定的误差,这就解释了为什么C++在比较浮点数是否为0时,要在一个区间比较,而不是直接等于0比较,如下:

float fTemp = 0.0001f; //精确范围
if(fFloat >= -fTemp && fFloat <= fTemp)
{
    //fFloat 等于0 
}

2、基本的浮点数指令

int main(int argc, char* argv[])
{
    float fFloat = (float) argc;
    printf("%f",fFloat);
    argc = (int) fFloat;
    printf("%d",argc);
    return 0;
}

我将这段代码进行了反编译

10:       float fFloat = (float) argc;
//将地址 ebp+8 处的 argc 这个整型转换为浮点型,并放入ST(0)中
00410648 DB 45 08             fild        dword ptr [ebp+8]
//从ST(0)中取出数据,以浮点编码方式放入地址ebp-4中,对应变量fFloat
0041064B D9 55 FC             fst         dword ptr [ebp-4]

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
11:       printf("%f",fFloat);
//这里对esp-8操作,是由于浮点数作为`变参函数`的参数时,需要转换为double类型
//这步操作提前准备8个字节的栈空间,以便于存放double数据
0041064E 83 EC 08             sub         esp,8
//将ST(0)中的数据传入esp,并弹出ST(0)
00410651 DD 1C 24             fstp        qword ptr [esp]
//以下为printf函数调用
00410654 68 2C 60 42 00       push        offset string "%f" (0042602c)
00410659 E8 B2 02 00 00       call        printf (00410910)
0041065E 83 C4 0C             add         esp,0Ch

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
12:       argc = (int) fFloat;
//将地址ebp-4处的数据,以浮点型压入 ST(0) 中
00410661 D9 45 FC             fld         dword ptr [ebp-4]
//调用__ftol将浮点数转换为int,转换后的结果会放在eax中
00410664 E8 3B 02 00 00       call        __ftol (004108a4)
//将转换后的结果,放入到 ebp+8 地址处
00410669 89 45 08             mov         dword ptr [ebp+8],eax

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
13:       printf("%d",argc);
0041066C 8B 45 08             mov         eax,dword ptr [ebp+8]
0041066F 50                   push        eax
00410670 68 1C 60 42 00       push        offset string "%d" (0042601c)
00410675 E8 96 02 00 00       call        printf (00410910)
0041067A 83 C4 08             add         esp,8
14:       return 0;
0041067D 33 C0                xor         eax,eax

三、字符和字符串

1、字符的编码

在C++中字符的编码分两种方式:ASCII和Unicode。
关于Unicode的详细介绍参考我这篇文章:http://www.0x520.com/2014/06/25/152.html

2、字符串的存储方式

在C++中使用结束符 '\0' 作为字符串的结束标识。ASCII使用一个字节 '\0',Unicode使用两个字节 '\0'

四、布尔类型

C++中定义 0 为假,非 0 为真。

五、地址、指针和引用

在C++中,地址标号使用16进制表示。取一个变量的地址使用 & 符号,只有变量才存在内存地址,常量没有地址。

指针的定义使用 TYPE*,TYPE为数据类型。任何数据类型都可以定义为指针。指针本身也是一种数据类型,它用于保存各种数据类型在内存中的地址。指针变量同样可以取地址。

引用的定义使用 TYPE&,TYPE为数据类型。在C++中是不可单独定义的,并且在定义时就要初始化。引用表示一个变量的别名,对它的任何操作,本质上都是在操作它所表示的变量。

指针也是变量,也包含在内存中,所以可以取出指针变量在内存中的位置–地址。指针变量在内存中占4个字节的内存空间。
指针可以根据指针类型对地址对应的数据进行解析。由于每种数据类型在内存中占用的空间不同,指针只保存了存放数据的首地址,而没有指明在哪里结束。这时,就需要根据对应的类型,来寻找解释数据的结束地址。

1、各种指针的工作方式

我写了一段测试代码

int main(int argc, char* argv[])
{
    int     nVar        = 0x12345678;
    int     *pnVar      = &nVar;
    char    *pcVar      = (char*)&nVar;
    short   *psVar      = (short*)&nVar;
    printf("%08x \r\n", *pnVar);
    printf("%08x \r\n", *pcVar);
    printf("%08x \r\n", *psVar);
    return 0;
}

反汇编代码如下:

10:       int     nVar        = 0x12345678;
//为地址 ebp-4 赋值4字节数据 12345678h
0040D778 C7 45 FC 78 56 34 12 mov         dword ptr [ebp-4],12345678h

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
//定义一个int类型的指针
11:       int     *pnVar      = &nVar;
//取ebp-4处的地址,并放在eax中
0040D77F 8D 45 FC             lea         eax,[ebp-4]
//并将eax中的地址值放入到ebp-8处
0040D782 89 45 F8             mov         dword ptr [ebp-8],eax

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
//定义一个char类型的指针
12:       char    *pcVar      = (char*)&nVar;
0040D785 8D 4D FC             lea         ecx,[ebp-4]
0040D788 89 4D F4             mov         dword ptr [ebp-0Ch],ecx

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
//定义一个short类型的指针
13:       short   *psVar      = (short*)&nVar;
0040D78B 8D 55 FC             lea         edx,[ebp-4]
0040D78E 89 55 F0             mov         dword ptr [ebp-10h],edx

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
14:       printf("%08x \r\n", *pnVar);
//取出pnVar中保存的地址,放入到eax中
0040D791 8B 45 F8             mov         eax,dword ptr [ebp-8]
//以4字节方式读取数据
0040D794 8B 08                mov         ecx,dword ptr [eax]
0040D796 51                   push        ecx
0040D797 68 80 2E 42 00       push        offset string "%08x \r\n" (00422e80)
0040D79C E8 3F FF FF FF       call        printf (0040d6e0)
0040D7A1 83 C4 08             add         esp,8

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
15:       printf("%08x \r\n", *pcVar);
0040D7A4 8B 55 F4             mov         edx,dword ptr [ebp-0Ch]
//以1字节方式读取数据
0040D7A7 0F BE 02             movsx       eax,byte ptr [edx]
0040D7AA 50                   push        eax
0040D7AB 68 80 2E 42 00       push        offset string "%08x \r\n" (00422e80)
0040D7B0 E8 2B FF FF FF       call        printf (0040d6e0)
0040D7B5 83 C4 08             add         esp,8

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
16:       printf("%08x \r\n", *psVar);
0040D7B8 8B 4D F0             mov         ecx,dword ptr [ebp-10h]
//以2字节方式读取数据
0040D7BB 0F BF 11             movsx       edx,word ptr [ecx]
0040D7BE 52                   push        edx
0040D7BF 68 80 2E 42 00       push        offset string "%08x \r\n" (00422e80)
0040D7C4 E8 17 FF FF FF       call        printf (0040d6e0)
0040D7C9 83 C4 08             add         esp,8

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
17:       return 0;
0040D7CC 33 C0                xor         eax,eax

程序输出的结果:

12345678
00000078
00005678

变量nVar在内存中的数据为 “78 56 23 12” ,首地址从78开始。 所以才得到上面这种结果。

2、引用

引用在c++中被描述为变量的别名,C++为了简化指针的操作,对指针进行了封装,产生了引用。实际上引用就是指针,只不过它用于存放地址的内存空间对使用者而言是隐藏的。

下面是我写的一段程序:

int main(int argc, char* argv[])
{
    int     nVar        = 0x12345678;
    int     &nVarType   = nVar;
    Add(nVar);
    return 0;
}

反汇编代码:

14:       int     nVar        = 0x12345678;
00401078 C7 45 FC 78 56 34 12 mov         dword ptr [ebp-4],12345678h

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
15:       int     &nVarType   = nVar;
//取出nVar的地址放入eax中
0040107F 8D 45 FC             lea         eax,[ebp-4]
//将eax中存放的地址值,放入到ebp-8处,这个ebp-8便是引用类型nVarType的地址
//也就是说明,引用类型在内存中是占有位置的。
00401082 89 45 F8             mov         dword ptr [ebp-8],eax

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
16:       Add(nVar);
00401085 8D 4D FC             lea         ecx,[ebp-4]
00401088 51                   push        ecx
00401089 E8 7C FF FF FF       call        @ILT+5(Add) (0040100a)
0040108E 83 C4 04             add         esp,4
17:       return 0;
00401091 33 C0                xor         eax,eax

下面是Add函数的源码:

void Add(int &nVar)
{
    nVar ++;
}

下面是Add函数的反汇编代码:

7:    void Add(int &nVar)
8:    {
9:        nVar ++;
00401038 8B 45 08             mov         eax,dword ptr [ebp+8]
0040103B 8B 08                mov         ecx,dword ptr [eax]
0040103D 83 C1 01             add         ecx,1
00401040 8B 55 08             mov         edx,dword ptr [ebp+8]
00401043 89 0A                mov         dword ptr [edx],ecx
10:   }

常量

常量数据在程序运行前就已经存在,它被编译到可执行文件中。程序运行后,他们会被加载进来。这些数据通常都会在常量数据区中保存,该section的属性是没有写权限的,所以对常量修改时,程序会报错。

常量数据的地址减去基址,便是它在文件中的偏移地址。

如果,常量字符串的首地址为 0x00423FA8 ,该程序的基址为 0x00400000,所以对应的偏移地址为:字符串首地址 - 基地址 = 0x00023FA8 ,使用十六进制查看器打开可执行文件,到 0x00023FA8 处,可以看到该字符串。

常量的定义

#define NUMBER_ONE    1
const int nVar = NUMBER_ONE;

#define 是一个真常量,而const却是由编译器判断实现的变量,是一个假常量。 在实际应用中,使用const定义的常量,最终还是一个变量,只是在编译器内进行了检查,发现有修改则报错。

修改const常量:

const int nConst     = 5;
int *pConst          = (int*)&nConst;
*pConst              = 6;
int nVar             = nConst;

转载请注明出处:鸡哥的博客http://www.0x520.com/2014/07/01/175.html

你可能感兴趣的:([逆向基础] C++中基本数据类型的表现形式)