2.2.2 基本的浮点数指令
前面学习了浮点数的编码方式,下面来学习浮点数指令。浮点数的操作指令与普通数据类型不同,浮点数操作是通过浮点寄存器来现实的,而普通数据类型使用的是通用寄存器,它们分别使用两套不同的指令。
浮点寄存器是通过栈结构来实现的,由ST(0)~ST(7)共8个栈空间组成,每个浮点寄存器占8字节。每次使用浮点寄存器都是率先使用ST(0),而不能越过ST(0)直接使用ST(1)。浮点寄存器的使用就是压栈、出栈的过程。当ST(0)存在数据时,执行压栈操作后,ST(0)中的数据将装入ST(1)中,如无出栈操作,将顺序地向下压栈,直到将浮点寄存器占满。常用浮点数指令的介绍如表2-1所示,其中,IN表示操作数入栈,OUT表示操作数出栈。
其他运算指令和普通指令类似,只需在前面加F即可,如FSUB和FSUBP等。
在使用浮点指令时,都要先利用ST(0)进行运算。当ST(0)中有值时,便会将ST(0)中的数据顺序向下存放到ST(1)中,然后再将数据放入ST(0)中。如果再次操作ST(0),则会先将ST(1)中的数据放入ST(2)中,然后将ST(0)中的数据放入ST(1)中,最后才将新的数据存放到ST(0)。以此类推,在8个浮点寄存器都有值的情况下继续向ST(0)中存放数据,这时会丢弃ST(7)中数据信息。
下面通过一个简单的示例来了解各指令的使用流程,熟悉数据传送类型指令FLD、FLD1、FST、FSTP、FISTP等的使用方法。Microsoft Visual C++6.0通过函数__ftol将float型转换为int型,见代码清单2-3。
代码清单2-3 Debug调试版float指令练习
//C++源码对比,argc为命令行参数
float fFloat=(float)argc;
;将地址ebp+8处的整型数据转换成浮点型,并放入ST(0)中,对应变量argc
0040E9D8 fild dword ptr[ebp+8]
;从ST(0)中取出数据以浮点编码方式放入地址ebp-4中,对应变量fFloat
0040E9DB fst dword ptr[ebp-4]
//C++源码对比,浮点数作为函数参数进行传递
printf("%f",fFloat);
;这里对esp执行减8操作是由于浮点数作为变参函数的参数时需要转换为双精度浮点值
;这步操作是提前准备8字节的栈空间,以便于存放double数据
0040E9DE sub esp,8
;将ST(0)中的数据传入esp中,并弹出ST(0)
0040E9E1 fstp qword ptr[esp]
;以下为printf函数调用,略
0040E9E4 push offset string"%f"(0042302c)
0040E9E9 call printf(0040e940)
0040E9EE add esp,0Ch
//C++源码对比,将float类型转换成int类型
argc=(int)fFloat;
;将地址ebp-4处的数据以浮点型压入ST(0)中
0040E9F1 fld dword ptr[ebp-4]
;调用函数__ftol进行浮点数转换,__ftol的实现见代码清单2-4
0040E9F4 call__ftol(0040e688)
;转换后结果放入eax中,并传递到ebp+8地址处
0040E9F9 mov dword ptr[ebp+8],eax
//C++源码对比,printf函数调用略
printf("%d",argc);
0040E9FC mov eax, dword ptr[ebp+8]
0040E9FF push eax
0040EA00 push offset string"%d"(0042301c)
0040EA05 call printf(0040e940)
0040EA0A add esp,8
代码清单2-3通过浮点数与整数、整数与浮点数间的互相转换演示了数据传送类型的浮点指令的使用方法。从示例中可以发现,float类型的浮点数虽然占4字节,但都是以8字节方式进行处理。当浮点数作为参数时,并不能直接压栈。PUSH指令只能传入4字节数据到栈中,这样会丢失4字节数据。这就是为什么使用printf函数以整数方式输出浮点数时会产生错误的原因。printf以整数方式输出时,将对应参数作为4字节数据,按补码方式解释;而真正压入的参数为浮点类型时,数据长度为8字节,需要按浮点编码方式解释。
浮点数作为返回值的情况也是如此,同样需要传递8字节数据,如代码清单2-4所示。
代码清单2-4 浮点数作为返回值
//C++源码对比,返回值为浮点数的函数调用
fFloat=GetFloat();
;调用函数GetFloat
0040EA3D call@ILT+5(GetFloat)(0040100a)
;由于浮点数需要特殊处理,浮点数占8字节,无法使用eax进行传递
;使用浮点寄存器ST(0)作为返回值
0040EA42 fst dword ptr[ebp-4]
;……
//C++源码对比,GetFloat函数实现
float GetFloat()
{
;Debug附加代码略
//C++源码对比,返回浮点数12.25f
return 12.25f;
;将浮点数保存在ST(0)中,在返回值为浮点数的情况下,无法使用eax
;使用ST(0)作为返回值进行传递
0040E9D8 fld dword ptr[__real@4@4002c400000000000000(0042301c)]
}
;Debug附加代码略
;返回,结束函数调用
0040E9E4 ret
在代码清单2-3中,float型数据被强制转换为int型,编译器通过_ftol实现了转换过程,如代码清单2-5所示。
代码清单2-5 类型转换函数_ftol的实现
;保存环境,预留语句变量空间
0040E688 push ebp
0040E689 mov ebp, esp
0040E68B add esp,0F4h
;浮点异常检查、CPU与FPU的同步工作
0040E68E wait
0040E68F fnstcw word ptr[ebp-2]
0040E692 wait
0040E693 mov ax, word ptr[ebp-2]
0040E697 or ah,0Ch
0040E69A mov word ptr[ebp-4],ax
0040E69E fldcw word ptr[ebp-4]
;从ST(0)中取出8字节数据转换成整型并存入ebp-0Ch中
;将ST(0)从栈中弹出
0040E6A1 fistp qword ptr[ebp-0Ch]
0040E6A4 fldcw word ptr[ebp-2]
;使用eax保存整型数据的低4字节,用于返回
0040E6A7 mov eax, dword ptr[ebp-0Ch]
;使用edx保存整型数据的高4字节,用于返回
0040E6AA mov edx, dword ptr[ebp-8]
;释放栈
0040E6AD leave
0040E6AE ret
在代码清单2-5中,将浮点数转换为长整型的关键指令FISTP,此函数的开始部分只是做了一些浮点异常检查、CPU与FPU的同步工作,最后通过调用FISTP来实现浮点数与整数之间的转换的。由于浮点数都占8字节,而在32位下的整型只占4字节长度,所以使用EDX来保存多出的4字节数据。