[原创]从反汇编的角度学C/C++之浮点数,结构体与联合体

一.浮点数

在C/C++中,浮点数分别用float和double来表示。其中,float占4个字节,double占8个字节。由于计算机只能存储整数,不能存储小数。所有浮点数在计算机中的保存与使用是使用了特殊的编码方式,即IEEE编码。这里不讨论IEEE编码,仅仅从反汇编的角度看看浮点数的存储与使用。

首先,与普通的数据类型不同,对于浮点数的操作是通过一组特殊的寄存器来实现的。这组特殊的寄存器就是x87 FPU寄存器,这组寄存器一共有八个用来存储浮点数的寄存器,分别是ST(0)-ST(7),每个寄存器都是八位寄存器。而对于这组寄存器的操作也是通过一组特殊的浮点数指令来进行操作。常见的浮点数指令表如下图所示,而对于其他的指令和基础指令的差别只是多了一个F,如fsub,fdiv,fmul。

其次,这一组浮点寄存器是以栈的形式使用,ST(0)是栈顶,ST(7)是栈底。也就是说当ST(0)有值的时候,如果我们在往里面放数据的话,计算机就会把ST(0)的数据放入ST(1),在把我们的数据放入ST(0),当要取出数据的时候,也是首先从ST(0)中取出数据。下面根据一个实例来看看这些指令是如何应用的,其中的iNum是传入的整型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
7:        float x = (float)iNum;
00410708 DB 45 08             fild        dword ptr [ebp+8]    //将整数的参数压入ST(0)
0041070B D9 5D FC             fstp        dword ptr [ebp-4]    //将ST(0)中的数据赋值给x,并把ST(0)的数据弹出
8:        float y = (float)(iNum + 10);
0041070E 8B 45 08             mov         eax,dword ptr [ebp+8]    //取出参数
00410711 83 C0 0A             add         eax,0Ah                    //将参数加10
00410714 89 45 F4             mov         dword ptr [ebp-0Ch],eax    //将参数放入开辟栈的内存
00410717 DB 45 F4             fild        dword ptr [ebp-0Ch]        //将计算出来的数据放入ST(0)
0041071A D9 5D F8             fstp        dword ptr [ebp-8]         //将ST(0)中的数据赋值给y,并把ST(0)的数据弹出
9:
10:       x += y;
0041071D D9 45 FC             fld         dword ptr [ebp-4]        //将浮点数x压入ST(0)
00410720 D8 45 F8             fadd        dword ptr [ebp-8]        //将ST(0)中的数据与y进行加法操作并把结果放入ST(0)
00410723 D9 55 FC             fst         dword ptr [ebp-4]        //将ST(0)的数据赋值给x,这里ST(0)的数据没有弹出
11:       x -= 123.4;
00410726 DC 25 20 60 42 00    fsub        qword ptr [__real@8@4005f6ccccccccccd000 (00426020)]    //对将ST(0)中的数据与地址0x00426020中保存的数据(123.4对应的IEEE八位数据)进行相减,并把数据放入ST(0)
0041072C D9 5D FC             fstp        dword ptr [ebp-4]    //将ST(0)中的数据赋值给x,并将数据从中弹出

二.结构体

C/C++程序员可以通过使用结构体来自定义一组由程序员自己指定的不同类型的数据,那么他在内存中是如何保存的呢?考虑如下的结构体,为了方便展示,我们用#pragma pack(1)是指定对齐长度为1。

1
2
3
4
5
6
7
#pragma pack(1)
typedef struct _Test
{
    char cTest;
    int iTest;
    char arrTest[10];
}Test;

之后我们对结构体进行初始化和赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    Test test = { 0 };
00BE10E8  xor         eax,eax  
00BE10EA  mov         dword ptr [ebp-14h],eax      
00BE10ED  mov         dword ptr [ebp-10h],eax  
00BE10F0  mov         dword ptr [ebp-0Ch],eax  
00BE10F3  mov         word ptr [ebp-8],ax  
00BE10F7  mov         byte ptr [ebp-6],al          //ebp-0x14是test的地址,这段汇编的行为就是将test初始化为0
    test.cTest = 'a';
00BE10FA  mov         byte ptr [ebp-14h],61h      //对test中的cTest赋值为'a'对应的数字0x61
    test.iTest = 1900;
00BE10FE  mov         dword ptr [ebp-13h],76Ch      //对test中的iTest赋值为1900对应的十六进制数0x76C
    strcpy(test.arrTest, "1900");
00BE1105  push        0C531B0h                      //将字符串"1900"的地址压入栈中
00BE110A  lea         eax,[ebp-0Fh]                 //取出test的地址
00BE110D  push        eax                          //压入test的地址
00BE110E  call        00C1A1B0                     //调用strcpy函数
00BE1113  add         esp,8

从上述的反汇编代码我们可以看出,当在程序中声明了一个结构体的变量,程序就会在内存中开辟一个足够将结构体中所有变量都容纳进去的内存,并按照声明顺序从低地址到高地址存储我们的变量,而test则是这个结构体的地址。

当程序运行到00BE1113的时候,如果查看test地址对应的内存可以看到如下结果。

可以看到我们的数据按照声明顺序放入了内存中。

既然test代表了地址,那么结构体的赋值操作是否只是地址的复制呢?请看下面的示例:

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
    Test test2 = { 0 };
00BE1116  xor         eax,eax  
00BE1118  mov         dword ptr [ebp-2Ch],eax  
00BE111B  mov         dword ptr [ebp-28h],eax  
00BE111E  mov         dword ptr [ebp-24h],eax  
00BE1121  mov         word ptr [ebp-20h],ax  
00BE1125  mov         byte ptr [ebp-1Eh],al      //对test2初始化为0
    test2 = test;
00BE1128  mov         eax,dword ptr [ebp-14h]     
00BE112B  mov         dword ptr [ebp-2Ch],eax  
00BE112E  mov         ecx,dword ptr [ebp-10h]  
00BE1131  mov         dword ptr [ebp-28h],ecx  
00BE1134  mov         edx,dword ptr [ebp-0Ch]  
00BE1137  mov         dword ptr [ebp-24h],edx  
00BE113A  mov         ax,word ptr [ebp-8]  
00BE113E  mov         word ptr [ebp-20h],ax  
00BE1142  mov         cl,byte ptr [ebp-6]  
00BE1145  mov         byte ptr [ebp-1Eh],cl  //ebp-0x14是test的地址,ebp-0x2C是test2的地址,这段汇编代码的作用就是将test结构体中的数据复制到test2中
    printf("%c %d %s\n", test2.cTest, test2.iTest, test2.arrTest);
00BE1148  lea         eax,[ebp-27h]          //取出test2.arrTest的地址,并压入栈中
00BE114B  push        eax  
00BE114C  mov         ecx,dword ptr [ebp-2Bh]  //取出test2.iTest的值压入栈中
00BE114F  push        ecx  
00BE1150  movsx       edx,byte ptr [ebp-2Ch]      //取出test2.cTest的值压入栈中
00BE1154  push        edx  
00BE1155  push        0C531B8h                    //将"%c %d %s\n"的地址压入栈中
00BE115A  call        00BE11C0                    //调用printf函数
00BE115F  add         esp,10h

可以看到对于结构体的赋值操作,程序会把对应的结构体的值全部赋值过去。当程序执行到0x00BE1145的时候,查看ebp-0x2C,即test2地址的内存时候可以看到如下结果。

最终程序的输出结果如下

三.联合体

联合体与结构体十分的相似,唯一的不同就是联合体是所有的变量共用一片内存。考虑如下的定义:

1
2
3
4
5
6
7
#pragma pack(1)
typedef union _Test
{
    int iTest1;
    int iTest2;
    int iTest3;
}Test;

这里,我定义了三个int类型的数据,接下来对其进行赋值与输出操作的反汇编结果如下,其中的ebp-8就是test的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    Test test = { 0 };
006B10E8  mov         dword ptr [ebp-8],0  //初始化为0
    test.iTest1 = 1234;
006B10EF  mov         dword ptr [ebp-8],4D2h  //将数字1234赋值到test对应的地址
    test.iTest2 = 1874;
006B10F6  mov         dword ptr [ebp-8],752h  //将数字1874赋值到test对应的地址
    test.iTest3 = 1900;
006B10FD  mov         dword ptr [ebp-8],76Ch  //将数字1900赋值到test对应的地址
    printf("%d %d %d\n", test.iTest1, test.iTest2, test.iTest3);
006B1104  mov         eax,dword ptr [ebp-8]  //取出iTest3的值并压栈
006B1107  push        eax  
006B1108  mov         ecx,dword ptr [ebp-8]   //取出iTest2的值并压栈
006B110B  push        ecx  
006B110C  mov         edx,dword ptr [ebp-8]   //取出iTest1的值并压栈
006B110F  push        edx  
006B1110  push        7231B0h                  //压入"%d %d %d\n"的地址
006B1115  call        006B1160                  //调用printf函数
006B111A  add         esp,10h

由上可以看出,在联合体中只保留了可以保存一个变量大小的内存,而且我们每一次对联合体中的数据进行操作都是对同一片内存进行操作。最终输出结果如下:

(0)

相关推荐