跳轉到內容

x86 反彙編/最佳化示例

來自華夏公益教科書,開放的書本,開放的世界

示例:最佳化與非最佳化程式碼

[編輯 | 編輯原始碼]

以下示例改編自 Knuth(卷 1,第 1 章)中介紹的一種演算法,用於查詢兩個整數的最大公約數。比較該函式的清單檔案,開啟和關閉編譯器最佳化時。

 /*line 1*/
 int EuclidsGCD(int m, int n) /*we want to find the GCD of m and n*/
 {
 	int q, r; /*q is the quotient, r is the remainder*/
 	while(1)
 	{
 		q = m / n; /*find q and r*/
 		r = m % n;
 		if(r == 0) /*if r is 0, return our n value*/
 		{
 			return n;
 		}
 		m = n; /*set m to the current n value*/
 		n = r; /*set n to our current remainder value*/
 	} /*repeat*/
 }

使用 Microsoft C 編譯器編譯,我們使用無最佳化生成一個清單檔案

 PUBLIC	_EuclidsGCD
 _TEXT	SEGMENT
 _r$ = -8	; size = 4
 _q$ = -4	; size = 4
 _m$ = 8	; size = 4
 _n$ = 12	; size = 4
 _EuclidsGCD PROC NEAR
 ; Line 2
 	push	ebp
 	mov	ebp, esp
 	sub	esp, 8
 $L477:
 ; Line 4
 	mov	eax, 1
 	test	eax, eax
 	je	SHORT $L473
 ; Line 6
 	mov	eax, DWORD PTR _m$[ebp]
 	cdq
 	idiv	DWORD PTR _n$[ebp]
 	mov	DWORD PTR _q$[ebp], eax
 ; Line 7
 	mov	eax, DWORD PTR _m$[ebp]
 	cdq
 	idiv	DWORD PTR _n$[ebp]
 	mov	DWORD PTR _r$[ebp], edx
 ; Line 8
 	cmp	DWORD PTR _r$[ebp], 0
 	jne	SHORT $L479
 ; Line 10
 	mov	eax, DWORD PTR _n$[ebp]
 	jmp	SHORT $L473
 $L479:
 ; Line 12
 	mov	ecx, DWORD PTR _n$[ebp]
 	mov	DWORD PTR _m$[ebp], ecx
 ; Line 13
 	mov	edx, DWORD PTR _r$[ebp]
 	mov	DWORD PTR _n$[ebp], edx
 ; Line 14
 	jmp	SHORT $L477
 $L473:
 ; Line 15
 	mov	esp, ebp
 	pop	ebp
 	ret	0
 _EuclidsGCD ENDP
 _TEXT	ENDS
 END

請注意 C 程式碼行與 ASM 程式碼行之間存在非常清晰的對應關係。"; 行 x" 指令在這方面非常有幫助。

接下來,我們使用一系列最佳化編譯相同函式,以強調速度而非大小

cl.exe /Tceuclids.c /Fa /Ogt2

我們生成以下清單

 PUBLIC	_EuclidsGCD
 _TEXT	SEGMENT
 _m$ = 8	; size = 4
 _n$ = 12       ; size = 4
 _EuclidsGCD PROC NEAR	
 ; Line 7
 	mov	eax, DWORD PTR _m$[esp-4]
 	push	esi
 	mov	esi, DWORD PTR _n$[esp]
 	cdq
 	idiv	esi
 	mov	ecx, edx
 ; Line 8
 	test	ecx, ecx
 	je	SHORT $L563
 $L547:
 ; Line 12
 	mov	eax, esi
 	cdq
 	idiv	ecx
 ; Line 13
 	mov	esi, ecx
 	mov	ecx, edx
 	test	ecx, ecx
 	jne	SHORT $L547
 $L563:
 ; Line 10
 	mov	eax, esi
 	pop	esi
 ; Line 15
 	ret	0
 _EuclidsGCD ENDP
 _TEXT	ENDS
 END

如您所見,最佳化版本明顯比非最佳化版本短。一些關鍵差異包括

  • 最佳化版本不準備標準堆疊幀。這很重要,因為許多新的逆向工程人員都認為函式總是以正確的堆疊幀開始和結束,而事實並非如此。EBP 未被使用,ESP 未被更改(因為區域性變數儲存在暫存器中,而不是放在堆疊上),並且沒有呼叫子函式。這減少了 5 條指令。
  • 非最佳化輸出中 "; 行 4" 下的 "test EAX, EAX" 指令序列都是不必要的。while 迴圈由 "while(1)" 定義,因此迴圈總是繼續。這段多餘的程式碼可以安全地刪去。還要注意,迴圈中沒有像預期的那樣出現無條件跳轉: "if(r == 0) return n;" 指令已成為新的迴圈條件。
  • 函式的結構發生了很大改變:m 和 n 的除法以生成 q 和 r 在此函式中執行了兩次:一次在函式開始時進行初始化,一次在迴圈結束時進行。此外,r 的值在相同的位置進行了兩次測試。編譯器在分配函式中的儲存空間方面非常自由,並且會輕易丟棄不需要的值。

示例:手動最佳化

[編輯 | 編輯原始碼]

以下彙編程式碼行未經最佳化,但可以非常輕鬆地進行最佳化。你能找到最佳化這些行的方法嗎?

mov	eax, 1
test	eax, eax
je	SHORT $L473

此行中的程式碼是為 "while( 1 )" C 程式碼生成的程式碼,確切地說,它表示迴圈退出條件。因為這是一個無限迴圈,所以我們可以假設這些行是不必要的。

"mov eax, 1" 初始化 eax。

之後的測試立即測試 eax 的值,以確保它不為零。因為 eax 在此時始終不為零(eax = 1),所以條件跳轉可以與 "mov" 和 "test" 一起刪除。

彙編程式碼實際上檢查的是 1 是否等於 1。另一個事實是,無限 **FOR** 迴圈的 C 程式碼

 for( ; ; )
 {
    ...
 }

一開始就不會建立這種毫無意義的彙編程式碼,並且在邏輯上與 "while( 1 )" 相同。

示例:跟蹤變數

[編輯 | 編輯原始碼]

以下是來自上面示例的 EuclidGCD 函式的 C 程式碼和最佳化後的彙編清單。你能確定哪個暫存器包含變數 **r** 和 **q** 嗎?

 /*line 1*/
 int EuclidsGCD(int m, int n) /*we want to find the GCD of m and n*/
 {
 	int q, r; /*q is the quotient, r is the remainder*/
 	while(1)
 	{
 		q = m / n; /*find q and r*/
 		r = m % n;
 		if(r == 0) /*if r is 0, return our n value*/
 		{
 			return n;
 		}
 		m = n; /*set m to the current n value*/
 		n = r; /*set n to our current remainder value*/
 	} /*repeat*/
 }
 PUBLIC	_EuclidsGCD
 _TEXT	SEGMENT
 _m$ = 8	; size = 4
 _n$ = 12       ; size = 4
 _EuclidsGCD PROC NEAR	
 ; Line 7
 	mov	eax, DWORD PTR _m$[esp-4]
 	push	esi
 	mov	esi, DWORD PTR _n$[esp]
 	cdq
 	idiv	esi
 	mov	ecx, edx
 ; Line 8
 	test	ecx, ecx
 	je	SHORT $L563
 $L547:
 ; Line 12
 	mov	eax, esi
 	cdq
 	idiv	ecx
 ; Line 13
 	mov	esi, ecx
 	mov	ecx, edx
 	test	ecx, ecx
 	jne	SHORT $L547
 $L563:
 ; Line 10
 	mov	eax, esi
 	pop	esi
 ; Line 15
 	ret	0
 _EuclidsGCD ENDP
 _TEXT	ENDS
 END

在函式開始時,**eax** 包含 m,**esi** 包含 n。當執行 "idiv esi" 指令時,eax 包含商(q),edx 包含餘數(r)。"mov ecx, edx" 指令將 r 移動到 **ecx** 中,而 q 在迴圈的剩餘部分沒有使用,因此被丟棄。

示例:反編譯最佳化程式碼

[編輯 | 編輯原始碼]

以下是上面示例中介紹的 EuclidGCD 函式的最佳化後的清單檔案。你能將這段彙編程式碼清單反編譯成等效的 "最佳化" C 程式碼嗎?最佳化版本在結構上與非最佳化版本有何不同?

 PUBLIC	_EuclidsGCD
 _TEXT	SEGMENT
 _m$ = 8	; size = 4
 _n$ = 12       ; size = 4
 _EuclidsGCD PROC NEAR	
 ; Line 7
 	mov	eax, DWORD PTR _m$[esp-4]
 	push	esi
 	mov	esi, DWORD PTR _n$[esp]
 	cdq
 	idiv	esi
 	mov	ecx, edx
 ; Line 8
 	test	ecx, ecx
 	je	SHORT $L563
 $L547:
 ; Line 12
 	mov	eax, esi
 	cdq
 	idiv	ecx
 ; Line 13
 	mov	esi, ecx
 	mov	ecx, edx
 	test	ecx, ecx
 	jne	SHORT $L547
 $L563:
 ; Line 10
 	mov	eax, esi
 	pop	esi
 ; Line 15
 	ret	0
 _EuclidsGCD ENDP
 _TEXT	ENDS
 END

更改條件以保持相同結構,我們得到

 int EuclidsGCD(int m, int n)
 {
     int r;
     r = m % n;
     if(r != 0) 
     {
         do
         {
             m = n;
             r = m % r;
             n = r;
         }while(r != 0)
     }
     return n;
 }

由讀者自行編譯這段新的 "最佳化" C 程式碼,並確定是否存在任何效能提升。首先嚐試在沒有最佳化的情況下編譯這段新程式碼,然後在開啟最佳化的情況下編譯。將新的彙編清單與之前的清單進行比較。

示例:指令配對

[編輯 | 編輯原始碼]
為什麼 **dec** / **jne** 組合比等效的 **loopnz** 執行得更快?
**dec** / **jnz** 對比 **loopnz** 執行得更快的原因有很多。首先,**dec** 和 **jnz** 在 netburst 管道的不同模組中配對,因此可以同時執行。除此之外,**dec** 和 **jnz** 都需要很少的週期來執行,而 **loopnz**(以及所有 loop 指令,實際上)指令需要更多週期才能完成。好的編譯器很少看到迴圈指令的輸出。

示例:避免分支

[編輯 | 編輯原始碼]

以下是以彙編程式碼形式呈現的表示式 c ? d : 0。程式碼中沒有分支,那麼它是如何工作的?

; ecx = c and edx = d
; eax will contain c ? d : 0 (eax = d if c is not zero, otherwise eax = 0)
neg	ecx
sbb	eax, eax
and	eax, edx
ret

這是一個使用各種算術指令來避免分支的示例。**neg** 指令如果 c 不為零,則設定進位標誌;否則,它會清除進位標誌。下一行取決於此。如果進位標誌被設定,那麼 **sbb** 將導致 eax = eax - eax - 1 = 0xffffffff。否則,eax = eax - eax = 0。最後,對此結果執行 **and** 操作可以確保,如果 **ecx** 最初不為零,則 **eax** 將包含 **edx**,否則為零。

示例:Duff's Device

[編輯 | 編輯原始碼]

以下 C 程式碼函式的功能是什麼?它有用嗎?為什麼或為什麼不?

void MyFunction(int *arrayA, int *arrayB, int cnt)
{
  switch(cnt % 6) 
  {
    while(cnt != 0) 
    {
      case 0:
        arrayA[--cnt] = arrayB[cnt];
      case 5:
        arrayA[--cnt] = arrayB[cnt];
      case 4:
        arrayA[--cnt] = arrayB[cnt];
      case 3:
        arrayA[--cnt] = arrayB[cnt];
      case 2:
        arrayA[--cnt] = arrayB[cnt];
      case 1:
        arrayA[--cnt] = arrayB[cnt];
    }
  }
}

這段程式碼被稱為 **Duff's device** 或 "Duff's machine"。它用於提高效率,部分展開迴圈。請注意 while() 巢狀在 switch 語句中的奇怪方式?兩個整數陣列傳遞給函式,在 while 迴圈的每次迭代中,會將 6 個連續元素從 arrayB 複製到 arrayA。由於 switch 語句位於 while 迴圈之外,因此它只在函式開始時發生。對變數 cnt 進行模 6 操作。如果 cnt 不能被 6 整除,那麼模運算將在旋轉的中間某個位置開始迴圈,從而防止迴圈在每次迭代後測試當前計數的情況下導致緩衝區溢位。

Duff's Device 被認為是用於複製字串、陣列或資料流的最有效通用方法之一。

華夏公益教科書