跳轉至內容

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 比較和跳轉

[編輯 | 編輯原始碼]
華夏公益教科書