跳轉至內容

x86 反彙編/程式碼最佳化

來自華夏公益教科書,開放的書籍,面向開放的世界

程式碼最佳化

[編輯 | 編輯原始碼]

一個最佳化編譯器可能是現存最複雜、最強大、最有趣的程式之一。本章將討論最佳化,但不會包含常見最佳化的表格。

最佳化階段

[編輯 | 編輯原始碼]

編譯器在兩個時間點可以執行最佳化:首先,在中間表示中,其次,在程式碼生成期間。

中間表示最佳化

[編輯 | 編輯原始碼]

在中間表示中,編譯器可以執行各種最佳化,通常基於資料流分析技術。例如,考慮以下程式碼片段

 x = 5;
 if(x != 5)
 {
   //loop body
 }

最佳化編譯器可能會注意到,在 "if (x != 5)" 的位置,x 的值始終是常量 "5"。這允許用 "5" 代替 x,得到 "5 != 5"。然後編譯器注意到結果表示式完全在常量上操作,因此可以在現在計算其值,而不是在執行時計算,從而最佳化條件為 "if (false)"。最後,編譯器看到這意味著 if 條件語句的語句體永遠不會執行,因此可以完全省略 if 條件語句的整個語句體。

考慮相反的情況

 x = 5;
 if(x == 5)
 { 
    //loop body
 }

在這種情況下,最佳化編譯器會注意到 IF 條件語句始終為真,甚至不會費心編寫程式碼來測試 x。

控制流最佳化

[編輯 | 編輯原始碼]

另一組最佳化可以在中間表示級別或程式碼生成級別執行,它們是控制流最佳化。大多數這些最佳化處理無用分支的消除。考慮以下程式碼

 if(A)
 {
    if(B)
    {
       C;
    }
    else
    {
       D;
    }
    end_B:
 }
 else
 {
    E;
 }
 end_A:

在這段程式碼中,一個簡單的編譯器會從 C 塊生成一個跳轉到 end_B,然後從 end_B 生成另一個跳轉到 end_A(以繞過 E 語句)。顯然,跳轉到一個跳轉是不高效的,因此最佳化編譯器會從 C 塊生成一個直接跳轉到 end_A。

不幸的是,這會使程式碼更加混亂,並且會阻止很好地恢復原始程式碼。對於複雜的函式,可能需要考慮僅由 if()-goto; 序列組成的程式碼,而無法識別更高層次的語句,例如 if-else 或迴圈。

識別高層次語句層次結構的過程稱為“程式碼結構化”。

程式碼生成最佳化

[編輯 | 編輯原始碼]

一旦編譯器篩選了程式碼中所有邏輯上的低效率,程式碼生成器就會接管。程式碼生成器通常會用更快的機器指令替換某些速度較慢的機器指令。

例如,指令

 beginning:
 ...
 loopnz beginning

執行速度比等效指令集

 beginning:
 ...
 dec ecx
 jne beginning

慢得多。那麼,為什麼編譯器會使用 loopxx 指令呢?答案是大多數最佳化編譯器永遠不會使用 loopxx 指令,因此,作為逆向工程師,您可能永遠不會在實際程式碼中看到它被使用。

那麼指令

  
 mov eax, 0

怎麼樣?mov 指令相對較快,但處理器中較快的一部分是算術單元。因此,使用以下指令更有意義

 xor eax, eax

因為 xor 在很少的處理器週期內執行(並且同時節省了三個位元組),因此比 "mov eax, 0" 快。xor 指令的唯一缺點是它會更改處理器標誌,因此它不能用在比較指令和相應的條件跳轉之間。

迴圈展開

[編輯 | 編輯原始碼]

當迴圈需要執行固定的少量迭代時,通常最好展開迴圈,以減少執行的跳轉指令數量,並且在許多情況下防止處理器的分支預測失敗。考慮以下 C 迴圈,它呼叫函式 MyFunction() 5 次

for(x = 0; x < 5; x++) 
{
  MyFunction();
}

轉換為彙編程式碼,我們看到它大致變為

mov eax, 0
loop_top:
cmp eax, 5
jge loop_end
call _MyFunction
inc eax
jmp loop_top
loop_end:

每次迴圈迭代都需要執行以下操作

  1. 將 eax(變數 "x")中的值與 5 進行比較,如果大於或等於 5,則跳轉到末尾
  2. 遞增 eax
  3. 跳轉回迴圈頂部。

請注意,如果我們手動重複對 MyFunction() 的呼叫,我們將刪除所有這些指令

call _MyFunction
call _MyFunction
call _MyFunction
call _MyFunction
call _MyFunction

這個新版本不僅佔用的磁碟空間更小,因為它使用的指令更少,而且執行速度更快,因為它執行的指令更少。這個過程稱為迴圈展開

行內函數

[編輯 | 編輯原始碼]

C 和 C++ 語言允許定義 inline 型別的函式。行內函數是類似於宏的函式。在編譯期間,對行內函數的呼叫將被該函式的語句體替換,而不是執行 call 指令。除了使用 inline 關鍵字宣告行內函數之外,最佳化編譯器還可以決定將其他函式也內聯。

函式內聯類似於迴圈展開,用於提高程式碼效能。非行內函數需要一個 call 指令,幾個指令來建立堆疊幀,然後還需要幾個指令來銷燬堆疊幀並從函式返回。透過複製函式的語句體而不是進行呼叫,機器程式碼的大小會增加,但執行時間會減少

不一定要確定相同的程式碼部分最初是作為宏、行內函數建立的,還是僅僅是複製貼上的。但是,在反彙編時,可以將這些程式碼塊分離出來,將其視為獨立的行內函數,這有助於使程式碼保持一致。

華夏公益教科書