LLVM bugpoint 工具:設計與用法

描述

bugpoint 能夠縮小 LLVM 工具和過程 (passes) 中問題的來源範圍。它可以用於偵錯三種類型的錯誤:優化器當機、優化器編譯錯誤,或原生程式碼產生錯誤(包括靜態和 JIT 編譯器中的問題)。它的目標是將大型測試案例簡化為小型、有用的案例。例如,如果 opt 在優化檔案時當機,它會找出導致當機的優化(或優化組合),並將檔案簡化為一個觸發當機的小型範例。

有關詳細的案例情境,例如偵錯 opt 或其中一個 LLVM 程式碼產生器,請參閱 如何提交 LLVM 錯誤報告

設計理念

bugpoint 的設計目標是在不需要任何 LLVM 基礎架構的情況下成為一個有用的工具。它適用於任何和所有 LLVM 過程和程式碼產生器,並且不需要「知道」它們是如何工作的。因此,它可能會做一些看似愚蠢的事情,或者錯過一些顯而易見的簡化。 bugpoint 的設計也旨在用編譯器偵錯過程中的電腦時間來換取程式設計師的時間;因此,它可能需要很長時間(無人值守)才能簡化一個測試案例,但我們認為這仍然是值得的。請注意,除非是在偵錯一個編譯錯誤(每個程式的測試都需要執行很長時間),否則 bugpoint 通常非常快。

自動偵錯器選擇

bugpoint 會讀取命令列中指定的每個 .bc.ll 檔案,並將它們連結在一起成為一個稱為測試程式的單一模組。如果在命令列中指定了任何 LLVM 過程,它會在測試程式上執行這些過程。如果任何過程當機,或者它們產生了格式錯誤的輸出(導致驗證器中止),bugpoint 會啟動 當機偵錯器

否則,如果未指定 -output 選項,bugpoint 會使用「安全」後端(假設其會產生良好的程式碼)執行測試程式,以產生參考輸出。一旦 bugpoint 取得測試程式的參考輸出後,它會嘗試使用選定的程式碼產生器執行測試程式。如果選定的程式碼產生器當機,bugpoint 會在程式碼產生器上啟動當機偵錯器。否則,如果產生的輸出與參考輸出不同,它會假設差異是由程式碼產生器錯誤所導致,並啟動程式碼產生器偵錯器

最後,如果選定程式碼產生器的輸出與參考輸出相符,bugpoint 會在所有 LLVM 編譯流程都套用至測試程式後執行它。如果其輸出與參考輸出不同,它會假設差異是由其中一個 LLVM 編譯流程的錯誤所導致,並進入編譯錯誤偵錯器。否則,就沒有 bugpoint 可以偵錯的問題了。

當機偵錯器

如果優化器或程式碼產生器當機,bugpoint 會盡可能減少編譯流程的數量(針對優化器當機)和測試程式的規模。首先,bugpoint 會找出是哪個優化器編譯流程的組合觸發了錯誤。這在偵錯 opt 所暴露的問題時很有用,例如,因為它會執行超過 38 個編譯流程。

接下來,bugpoint 會嘗試從測試程式中移除函數,以減少其規模。通常在偵錯程序內優化的時候,它能夠將測試程式縮減為單一函數。一旦函數數量減少後,它會嘗試刪除控制流程圖中的各種邊緣,盡可能減少函數的規模。最後,bugpoint 會刪除任何不存在也不會消除錯誤的個別 LLVM 指令。最後,bugpoint 會告訴您哪些編譯流程當機,提供您一個位元碼檔案,並提供您如何使用 optllc 重現錯誤的說明。

程式碼產生器偵錯器

程式碼產生器偵錯器會嘗試縮小被選定程式碼產生器錯誤編譯的程式碼數量。為此,它會取得測試程式並將其分為兩部分:一部分使用「安全」後端(編譯成共用物件),另一部分則使用 JIT 或靜態 LLC 編譯器執行。它使用多種技術來減少透過 LLVM 程式碼產生器推送的程式碼數量,以減少潛在問題的範圍。完成後,它會發出兩個位元碼檔案(分別稱為「test」(使用程式碼產生器編譯)和「safe」(使用「安全」後端編譯)),以及重現問題的說明。程式碼產生器偵錯器假設「安全」後端會產生良好的程式碼。

編譯錯誤偵錯器

編譯錯誤除錯器的運作方式與程式碼產生器除錯器類似。它會將測試程式分成兩部分,對其中一部分執行指定的最佳化,將兩部分重新連結在一起,然後執行結果。它會嘗試將 pass 列表縮小到導致編譯錯誤的一個(或幾個),然後減少正在被編譯錯誤的測試程式部分。編譯錯誤除錯器假設所選的程式碼產生器運作正常。

使用 bugpoint 的建議

bugpoint 是一個非常有用的工具,但它有時會以非顯而易見的方式運作。以下是一些提示和技巧

  • 在程式碼產生器和編譯錯誤除錯器中,bugpoint 僅適用於具有確定性輸出的程式。因此,如果程式輸出 argv[0]、日期、時間或任何其他「隨機」數據,bugpoint 在輸出時可能會將這些數據的差異誤解為編譯錯誤的結果。應該暫時修改程式,以停用可能會因執行而異的輸出。

  • 當機除錯器 中,bugpoint 不會區分縮減期間的不同當機。因此,如果發生新的當機或編譯錯誤,bugpoint 將繼續處理新的當機。如果您想堅持特定的當機,則應該編寫檢查腳本來驗證錯誤訊息,請參閱 bugpoint - 自動測試案例縮減工具 中的 -compile-command

  • 在程式碼產生器和編譯錯誤除錯器中,如果您手動修改程式或其輸入以減少執行時間但仍出現問題,則除錯速度會更快。

  • 在處理新的最佳化時,bugpoint 非常有用:它有助於快速追蹤回歸。但是,為了避免每次更改最佳化時都必須重新連結 bugpoint,請使用 -load 選項讓 bugpoint 動態載入您的最佳化。

  • bugpoint 可以產生大量輸出並長時間執行。將程式的輸出擷取到檔案通常很有用。例如,在 C shell 中,您可以執行

    $ bugpoint  ... |& tee bugpoint.log
    

    以在檔案 bugpoint.log 和終端機上取得 bugpoint 輸出的副本。

  • bugpoint 無法除錯 LLVM 連結器的問題。如果 bugpoint 在您看到其「所有輸入正常」訊息之前當機,您可以嘗試對同一組輸入檔案使用 llvm-link -v。如果那也當機,則您可能遇到了連結器錯誤。

  • bugpoint 對於主動發現 LLVM 中的錯誤很有用。使用 -find-bugs 選項呼叫 bugpoint 將導致指定的最佳化列表被隨機化並應用於程式。這個過程將重複進行,直到發現錯誤或用戶終止 bugpoint

  • bugpoint 可能會產生包含長名稱的 IR。對 IR 執行 opt -passes=metarenamer,使用易於閱讀的元語法名稱重新命名所有內容。或者,執行 opt -passes=strip,instnamer,使用非常簡短(通常完全是數字)的名稱重新命名所有內容。

當 bugpoint 不夠用時該怎麼辦

有時,bugpoint 並不夠用。特別是,InstCombine 和 TargetLowering 都具有訪客結構化代碼,其中包含許多潛在的轉換。如果使用 bugpoint 的過程仍然留下了太多代碼難以理解,並且問題似乎出在 instcombine 中,則以下步驟可能會有所幫助。這些相同的技術也適用於 TargetLowering。

開啟 -debug-only=instcombine,並通過選擇其中包含「IC」的行來查看 instcombine 中觸發了哪些轉換。

此時,您需要做出決定。轉換的數量是否少到足以使用偵錯器逐步執行?如果是,則請嘗試。

如果轉換太多,則修改原始碼的方法可能會有所幫助。在這種方法中,您可以修改 instcombine 的原始碼,以僅停用在您的測試輸入上執行的那些轉換,並對轉換集執行二元搜尋。一組要修改的位置是 InstCombiner 的「visit*」方法(例如 visitICmpInst),方法是在方法的第一行添加「return false」。

如果這仍然無法刪除足夠的內容,請更改 InstCombiner::DoOneIteration 的呼叫者 InstCombiner::runOnFunction 以限制迭代次數。

您現在可能還會發現使用「-stats」來查看 instcombine 的哪些部分正在觸發很有用。這可以指導在何處放置其他報告代碼。

此時,如果轉換的數量仍然太多,則插入代碼以限制是否執行 visit 函數中代碼的主體可能會有所幫助。添加一個靜態計數器,該計數器在每次調用函數時遞增。然後添加僅在所需範圍內返回 false 的代碼。例如

static int calledCount = 0;
calledCount++;
LLVM_DEBUG(if (calledCount < 212) return false);
LLVM_DEBUG(if (calledCount > 217) return false);
LLVM_DEBUG(if (calledCount == 213) return false);
LLVM_DEBUG(if (calledCount == 214) return false);
LLVM_DEBUG(if (calledCount == 215) return false);
LLVM_DEBUG(if (calledCount == 216) return false);
LLVM_DEBUG(dbgs() << "visitXOR calledCount: " << calledCount << "\n");
LLVM_DEBUG(dbgs() << "I: "; I->dump());

可以添加到 visitXOR 中,以將 visitXor 限制為僅應用於調用 212 和 217。這來自一個實際的測試案例,並提出了一個重要的觀點——簡單的二元搜尋可能不夠,因為交互作用的轉換可能需要隔離多個調用。在 TargetLowering 中,使用 return SDNode(); 而不是 return false;

現在轉換的數量已經減少到可以管理的數量,嘗試檢查輸出,看看您是否能找出正在進行哪些轉換。如果可以弄清楚這一點,那麼就進行通常的除錯。如果與正在執行的轉換相對應的程式碼不明顯,請在基於呼叫計數的停用之後設定一個中斷點,並逐步執行程式碼。或者,您可以使用「printf」風格的除錯來報告路徑點。