x86 反汇编/浮点数
本页面将介绍如何在汇编语言结构中使用浮点数。本页面不会讨论新的结构,也不会解释 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);
}
cl.exe 不使用这些指令,因此要创建这些函数,需要创建 3 个不同的文件,分别使用 /Gd、/Gr 和 /Gz 选项进行编译。 |
以下是 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
我们的第一个问题是:参数是通过堆栈传递的,还是通过浮点寄存器堆栈传递的,还是通过其他位置传递的?这个问题以及这个函数的关键在于了解fld 和fstp 的作用。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 字节宽。