[原创]从反汇编的角度学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 |
由上可以看出,在联合体中只保留了可以保存一个变量大小的内存,而且我们每一次对联合体中的数据进行操作都是对同一片内存进行操作。最终输出结果如下: