跳转至内容

x86 反汇编/浮点数

来自 Wikibooks,开放的书,为开放的世界

浮点数

[编辑 | 编辑源代码]

本页面将介绍如何在汇编语言结构中使用浮点数。本页面不会讨论新的结构,也不会解释 FPU 指令的工作原理、浮点数的存储和操作方式,以及浮点数据表示之间的差异。但是,本页面将简要演示如何在代码和数据结构中使用浮点数,这些代码和数据结构我们已经考虑过。

x86 架构没有专门用于浮点数的寄存器,但它有一个专门的堆栈用于浮点数。浮点堆栈直接内置到处理器中,并且具有与普通寄存器类似的访问速度。请注意,FPU 堆栈与常规系统堆栈不同。

调用约定

[编辑 | 编辑源代码]

随着浮点堆栈的添加,传递参数和返回值又增加了一个全新的维度。我们将在这里检查我们的调用约定,看看它们如何受到浮点数存在的影响。这些是我们将在使用 GCC 和 cl.exe 的情况下进行汇编的函数。

 __cdecl double MyFunction1(double x, double y, float z)
 {
 	return (x + 1.0) * (y + 2.0) * (z + 3.0);
 }
 
 __fastcall double MyFunction2(double x, double y, float z)
 {
 	return (x + 1.0) * (y + 2.0) * (z + 3.0);
 }
 
 __stdcall double MyFunction3(double x, double y, float z)
 {
 	return (x + 1.0) * (y + 2.0) * (z + 3.0);
 }


以下是 MyFunction1 的 cl.exe 汇编清单

 PUBLIC	_MyFunction1
 PUBLIC	__real@3ff0000000000000
 PUBLIC	__real@4000000000000000
 PUBLIC	__real@4008000000000000
 EXTRN	__fltused:NEAR
 ;	COMDAT __real@3ff0000000000000
 CONST	SEGMENT
 __real@3ff0000000000000 DQ 03ff0000000000000r	; 1
 CONST	ENDS
 ;	COMDAT __real@4000000000000000
 CONST	SEGMENT
 __real@4000000000000000 DQ 04000000000000000r	; 2
 CONST	ENDS
 ;	COMDAT __real@4008000000000000
 CONST	SEGMENT
 __real@4008000000000000 DQ 04008000000000000r	; 3
 CONST	ENDS
 _TEXT	SEGMENT
 _x$ = 8							; size = 8
 _y$ = 16						; size = 8
 _z$ = 24						; size = 4
 _MyFunction1 PROC NEAR
 ; Line 2
 	push	ebp
 	mov	ebp, esp
 ; Line 3
 	fld	QWORD PTR _x$[ebp]
 	fadd	QWORD PTR __real@3ff0000000000000
 	fld	QWORD PTR _y$[ebp]
 	fadd	QWORD PTR __real@4000000000000000
 	fmulp	ST(1), ST(0)
 	fld	DWORD PTR _z$[ebp]
 	fadd	QWORD PTR __real@4008000000000000
 	fmulp	ST(1), ST(0)
 ; Line 4
 	pop	ebp
 	ret	0
 _MyFunction1 ENDP
 _TEXT	ENDS

我们的第一个问题是:参数是通过堆栈传递的,还是通过浮点寄存器堆栈传递的,还是通过其他位置传递的?这个问题以及这个函数的关键在于了解fldfstp 的作用。fld(浮点加载)将浮点值压入 FPU 堆栈,而 fstp(浮点存储和弹出)将浮点值从 ST0 移动到指定位置,然后从 ST0 中弹出该值。请记住,cl.exe 中的double 值被视为 8 字节存储位置 (QWORD),而浮点数仅存储为 4 字节数量 (DWORD)。同样重要的是要记住,即使读者精通二进制,浮点数也不会以人类可读的形式存储在内存中。请记住,这些不是整数。不幸的是,浮点数的精确格式超出了本章的范围。

x 偏移量为 +8,y 偏移量为 +16,z 偏移量为 +24,它们都相对于 ebp。因此,z 最先被压入,x 最后被压入,参数以从右到左的顺序通过常规堆栈而不是浮点堆栈传递。要了解值是如何返回的,我们需要了解fmulp 的作用。fmulp 是“浮点乘法和弹出”指令。它执行以下指令

ST1 := ST1 * ST0
FPU POP ST0

这将 ST(1) 和 ST(0) 相乘,并将结果存储在 ST(1) 中。然后,ST(0) 被标记为空,堆栈指针被递增。因此,ST(1) 的内容位于堆栈顶部。所以堆栈顶部的两个值相乘,并将结果存储在堆栈顶部。因此,在我们上面的指令“fmulp ST(1), ST(0)”中,这也是函数的最后一条指令,我们可以看到最后的结果存储在 ST0 中。因此,浮点参数通过常规堆栈传递,但浮点结果通过 FPU 堆栈传递。

最后需要注意的是,MyFunction2 清理了自己的堆栈,如清单末尾的ret 20 命令所示。由于没有参数通过寄存器传递,因此该函数看起来与我们期望的 STDCALL 函数完全相同:参数以从右到左的顺序通过堆栈传递,函数清理自己的堆栈。我们将在下面看到,这实际上是一个正确的假设。

为了比较,以下是 GCC 清单

 LC1:
 	.long	0
 	.long	1073741824
 	.align 8
 LC2:
 	.long	0
 	.long	1074266112
 .globl _MyFunction1
 	.def	_MyFunction1;	.scl	2;	.type	32;	.endef
 _MyFunction1:
 	pushl	%ebp
 	movl	%esp, %ebp
 	subl	$16, %esp
 	fldl	8(%ebp)
 	fstpl	-8(%ebp)
 	fldl	16(%ebp)
 	fstpl	-16(%ebp)
 	fldl	-8(%ebp)
 	fld1
 	faddp	%st, %st(1)
 	fldl	-16(%ebp)
 	fldl	LC1
 	faddp	%st, %st(1)
 	fmulp	%st, %st(1)
 	flds	24(%ebp)
 	fldl	LC2
 	faddp	%st, %st(1)
 	fmulp	%st, %st(1)
 	leave
 	ret
 	.align 8

这是一个非常复杂的清单,所以我们将逐步进行(虽然很快)。在堆栈上分配了 16 字节的额外空间。然后,使用 fldl 和 fstpl 指令的组合,将前两个参数从偏移量 +8 和 +16 移动到偏移量 -8 和 -16,它们都相对于 ebp。看起来很浪费时间,但请记住,优化已关闭。fld1 将浮点值 1.0 加载到 FPU 堆栈中。然后,faddp 将堆栈顶部(1.0)与 ST1 中的值([ebp - 8],最初为 [ebp + 8])相加。

以下是 MyFunction2 的 cl.exe 清单

 PUBLIC	@MyFunction2@20
 PUBLIC	__real@3ff0000000000000
 PUBLIC	__real@4000000000000000
 PUBLIC	__real@4008000000000000
 EXTRN	__fltused:NEAR
 ;	COMDAT __real@3ff0000000000000
 CONST	SEGMENT
 __real@3ff0000000000000 DQ 03ff0000000000000r	; 1
 CONST	ENDS
 ;	COMDAT __real@4000000000000000
 CONST	SEGMENT
 __real@4000000000000000 DQ 04000000000000000r	; 2
 CONST	ENDS
 ;	COMDAT __real@4008000000000000
 CONST	SEGMENT
 __real@4008000000000000 DQ 04008000000000000r	; 3
 CONST	ENDS
 _TEXT	SEGMENT
 _x$ = 8							; size = 8
 _y$ = 16						; size = 8
 _z$ = 24						; size = 4
 @MyFunction2@20 PROC NEAR
 ; Line 7
 	push	ebp
 	mov	ebp, esp
 ; Line 8
 	fld	QWORD PTR _x$[ebp]
 	fadd	QWORD PTR __real@3ff0000000000000
 	fld	QWORD PTR _y$[ebp]
 	fadd	QWORD PTR __real@4000000000000000
 	fmulp	ST(1), ST(0)
 	fld	DWORD PTR _z$[ebp]
 	fadd	QWORD PTR __real@4008000000000000
 	fmulp	ST(1), ST(0)
 ; Line 9
 	pop	ebp
 	ret	20					; 00000014H
 @MyFunction2@20 ENDP
 _TEXT	ENDS

我们可以看到,该函数正在获取 20 字节的参数,因为函数名称末尾有 @20 装饰。这是有道理的,因为该函数正在获取两个double 参数(每个 8 字节)和一个float 参数(每个 4 字节)。总共 20 字节。我们可以一目了然地看到,无需实际分析或理解任何代码,这里只有一个寄存器被访问:ebp。这似乎很奇怪,因为 FASTCALL 将其常规的 32 位参数传递给寄存器。但是,这里并非如此:所有浮点参数(即使是 z,它是一个 32 位浮点数)都通过堆栈传递。我们知道这一点,因为通过查看代码,参数不可能来自其他地方。

还要注意,fmulp 再次是执行的最后一条指令,就像在 CDECL 示例中一样。我们可以推断出,无需深入调查,结果通过浮点堆栈顶部传递。

还要注意,x(偏移量 [ebp + 8])、y(偏移量 [ebp + 16])和 z(偏移量 [ebp + 24])以相反的顺序压入:z 最先,x 最后。这意味着浮点参数以从右到左的顺序通过堆栈传递。这与 CDECL 代码完全相同,只是因为我们使用的是浮点值。

以下是 MyFunction2 的 GCC 汇编清单

 	.align 8
 LC5:
 	.long	0
 	.long	1073741824
 	.align 8
 LC6:
 	.long	0
 	.long	1074266112
 .globl @MyFunction2@20
 	.def	@MyFunction2@20;	.scl	2;	.type	32;	.endef
 @MyFunction2@20:
 	pushl	%ebp
 	movl	%esp, %ebp
 	subl	$16, %esp
 	fldl	8(%ebp)
 	fstpl	-8(%ebp)
 	fldl	16(%ebp)
 	fstpl	-16(%ebp)
 	fldl	-8(%ebp)
 	fld1
 	faddp	%st, %st(1)
 	fldl	-16(%ebp)
 	fldl	LC5
 	faddp	%st, %st(1)
 	fmulp	%st, %st(1)
 	flds	24(%ebp)
 	fldl	LC6
 	faddp	%st, %st(1)
 	fmulp	%st, %st(1)
 	leave
 	ret	$20

这是一段棘手的代码,但幸运的是,我们无需仔细阅读就能找到我们想要的东西。首先,请注意,除了ebp 之外没有其他寄存器被访问。再次,GCC 将所有浮点值(即使是 32 位浮点数 z)都通过堆栈传递。此外,浮点结果值通过浮点堆栈顶部传递。

我们再次可以看到,GCC 在开头做了一些奇怪的事情,将堆栈上的值从 [ebp + 8] 和 [ebp + 16] 移动到 [ebp - 8] 和 [ebp - 16]。在移动之后,这些值立即被加载到浮点堆栈中,并执行算术运算。z 直到后面才被加载,并且从未被移动到 [ebp - 24],尽管有这个模式。

LC5 和 LC6 是常量值,它们很可能代表浮点值(因为数字本身,1073741824 和 1074266112,在我们示例函数的上下文中没有任何意义。请注意,LC5 和 LC6 都包含两个.long 数据项,总共 8 字节存储空间?因此,它们绝对是double 值。

以下是 MyFunction3 的 cl.exe 清单

 PUBLIC	_MyFunction3@20
 PUBLIC	__real@3ff0000000000000
 PUBLIC	__real@4000000000000000
 PUBLIC	__real@4008000000000000
 EXTRN	__fltused:NEAR
 ;	COMDAT __real@3ff0000000000000
 CONST	SEGMENT
 __real@3ff0000000000000 DQ 03ff0000000000000r	; 1
 CONST	ENDS
 ;	COMDAT __real@4000000000000000
 CONST	SEGMENT
 __real@4000000000000000 DQ 04000000000000000r	; 2
 CONST	ENDS
 ;	COMDAT __real@4008000000000000
 CONST	SEGMENT
 __real@4008000000000000 DQ 04008000000000000r	; 3
 CONST	ENDS
 _TEXT	SEGMENT
 _x$ = 8						; size = 8
 _y$ = 16						; size = 8
 _z$ = 24						; size = 4
 _MyFunction3@20 PROC NEAR
 ; Line 12
 	push	ebp
 	mov	ebp, esp
 ; Line 13
 	fld	QWORD PTR _x$[ebp]
 	fadd	QWORD PTR __real@3ff0000000000000
 	fld	QWORD PTR _y$[ebp]
 	fadd	QWORD PTR __real@4000000000000000
 	fmulp	ST(1), ST(0)
 	fld	DWORD PTR _z$[ebp]
 	fadd	QWORD PTR __real@4008000000000000
 	fmulp	ST(1), ST(0)
 ; Line 14
 	pop	ebp
 	ret	20					; 00000014H
 _MyFunction3@20 ENDP
 _TEXT	ENDS
 END

x 是堆栈中最上面的,z 是最下面的,因此这些参数以从右到左的顺序传递。我们可以从 x 的偏移量最小(偏移量 [ebp + 8])和 z 的偏移量最大(偏移量 [ebp + 24])得知这一点。我们还从最后的 fmulp 指令中看到,返回值通过 FPU 堆栈传递。该函数还清理了自己的堆栈,如调用'ret 20 所示。它正在清理堆栈上的 20 字节,这恰好是我们最初传递的总量。我们还可以注意到,该函数的实现与该函数的 FASTCALL 版本完全相同。这是因为 FASTCALL 仅将 DWORD 大小的参数传递给寄存器,而浮点数不符合此条件。这意味着我们上面的假设是正确的。

以下是 MyFunction3 的 GCC 清单

 	.align 8
 LC9:
 	.long	0
 	.long	1073741824
 	.align 8
 LC10:
 	.long	0
 	.long	1074266112
 .globl @MyFunction3@20
 	.def	@MyFunction3@20;	.scl	2;	.type	32;	.endef
 @MyFunction3@20:
 	pushl	%ebp
 	movl	%esp, %ebp
 	subl	$16, %esp
 	fldl	8(%ebp)
 	fstpl	-8(%ebp)
 	fldl	16(%ebp)
 	fstpl	-16(%ebp)
 	fldl	-8(%ebp)
 	fld1
 	faddp	%st, %st(1)
 	fldl	-16(%ebp)
 	fldl	LC9
 	faddp	%st, %st(1)
 	fmulp	%st, %st(1)
 	flds	24(%ebp)
 	fldl	LC10
 	faddp	%st, %st(1)
 	fmulp	%st, %st(1)
 	leave
 	ret	$20

这里我们还可以看到,在所有开头的无用代码之后,[ebp - 8](最初为 [ebp + 8])是值 x,[ebp - 24](最初为 [ebp - 24])是值 z。因此,这些参数以从右到左的顺序传递。此外,我们可以从最后的 fmulp 指令推断出,结果在 ST0 中传递。同样,STDCALL 函数清理自己的堆栈,正如我们所预期的那样。

浮点值作为参数通过堆栈传递,并作为结果通过 FPU 堆栈传递。浮点值不会被放入通用整数寄存器(eax、ebx 等),因此仅包含浮点参数的 FASTCALL 函数会缩减为 STDCALL 函数。double 值为 8 字节宽,因此在堆栈中会占用 8 字节。但是,float 值仅为 4 字节宽。

浮点数到整数转换

[编辑 | 编辑源代码]

FPU 比较和跳转

[编辑 | 编辑源代码]
华夏公益教科书