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**,否則為零。
以下 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 被認為是用於複製字串、陣列或資料流的最有效通用方法之一。