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 位元組寬。