撰寫 LLVM Pass

簡介 — 什麼是 Pass?

警告

本文件說明新的 Pass 管理器。LLVM 在程式碼產生管線中使用舊有的 Pass 管理器。更多細節請參閱 撰寫 LLVM Pass (舊版 PM 版本)使用新的 Pass 管理器

LLVM Pass 框架是 LLVM 系統的重要組成部分,因為 LLVM Pass 是編譯器中最有趣部分存在的地方。Pass 執行構成編譯器的轉換和最佳化,它們建立用於這些轉換的分析結果,而且最重要的是,它們是編譯器程式碼的結構化技術。

與舊版 Pass 管理器下透過繼承定義 Pass 介面的 Pass 不同,新版 Pass 管理器下的 Pass 依賴於基於概念的多型,這意味著沒有明確的介面(更多細節請參閱 PassManager.h 中的註解)。所有 LLVM Pass 都繼承自 CRTP mix-in PassInfoMixin<PassT>。Pass 應該有一個 run() 方法,該方法返回 PreservedAnalyses,並接收一些 IR 單元以及一個分析管理器。例如,函數 Pass 將有一個 PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM); 方法。

我們首先向您展示如何建構一個 Pass,從設定建置、建立 Pass,到執行和測試它。查看現有的 Pass 始終是學習細節的好方法。

快速開始 — 撰寫 hello world

在此,我們描述如何撰寫 Pass 的 “hello world”。“HelloWorld” Pass 的設計目的是簡單地印出正在編譯的程式中存在的非外部函數的名稱。它完全不修改程式,只檢查程式。

以下程式碼已存在;您可以隨時在 HelloWorld 原始碼檔案旁邊建立一個不同名稱的 Pass。

設定建置

首先,依照 LLVM 系統入門 中的說明,配置和建置 LLVM。

接下來,我們將重複使用現有的目錄(建立新目錄會牽涉到比我們想要的更多的 CMake 檔案)。對於這個範例,我們將使用 llvm/lib/Transforms/Utils/HelloWorld.cpp,該檔案已經建立。如果您想建立自己的 Pass,請將新的原始碼檔案新增到 llvm/lib/Transforms/Utils/CMakeLists.txt 中(假設您希望您的 Pass 位於 Transforms/Utils 目錄中)。

現在我們已經為新的 Pass 設定了建置,我們需要撰寫 Pass 本身的程式碼。

基本程式碼需求

現在已經為新的 Pass 設定了建置,我們只需要撰寫它。

首先,我們需要在標頭檔中定義 Pass。我們將建立 llvm/include/llvm/Transforms/Utils/HelloWorld.h。該檔案應包含以下樣板程式碼

#ifndef LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H
#define LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H

#include "llvm/IR/PassManager.h"

namespace llvm {

class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {
public:
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);
};

} // namespace llvm

#endif // LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H

這會建立 Pass 的類別,並宣告實際執行 Pass 的 run() 方法。繼承自 PassInfoMixin<PassT> 會設定更多樣板程式碼,這樣我們就不必自己撰寫。

我們的類別位於 llvm 命名空間中,這樣我們就不會污染全域命名空間。

接下來,我們將建立 llvm/lib/Transforms/Utils/HelloWorld.cpp,從以下開始

#include "llvm/Transforms/Utils/HelloWorld.h"

… 包含我們剛建立的標頭檔。

using namespace llvm;

… 是必要的,因為 include 檔案中的函數位於 llvm 命名空間中。這應該只在非標頭檔中完成。

接下來我們有 Pass 的 run() 定義

PreservedAnalyses HelloWorldPass::run(Function &F,
                                      FunctionAnalysisManager &AM) {
  errs() << F.getName() << "\n";
  return PreservedAnalyses::all();
}

… 它只是將函數的名稱印到 stderr。Pass 管理器將確保 Pass 將在模組中的每個函數上執行。PreservedAnalyses 回傳值表示在此 Pass 之後,所有分析(例如支配樹)仍然有效,因為我們沒有修改任何函數。

這就是 Pass 本身的所有內容。現在為了“註冊” Pass,我們需要將其新增到幾個地方。將以下內容新增到 llvm/lib/Passes/PassRegistry.def 中的 FUNCTION_PASS 區段

FUNCTION_PASS("helloworld", HelloWorldPass())

… 這會以名稱 “helloworld” 新增 Pass。

llvm/lib/Passes/PassRegistry.def 因為各種原因被多次 #includellvm/lib/Passes/PassBuilder.cpp 中。由於它建構了我們的 Pass,我們也需要在 llvm/lib/Passes/PassBuilder.cpp 中新增正確的 #include

#include "llvm/Transforms/Utils/HelloWorld.h"

這應該是我們的 Pass 所需的所有程式碼,現在是時候編譯並執行它了。

使用 opt 執行 Pass

現在您有了一個全新的閃亮 Pass,我們可以建置 opt 並使用它來透過 Pass 執行一些 LLVM IR。

$ ninja -C build/ opt
# or whatever build system/build directory you are using

$ cat /tmp/a.ll
define i32 @foo() {
  %a = add i32 2, 3
  ret i32 %a
}

define void @bar() {
  ret void
}

$ build/bin/opt -disable-output /tmp/a.ll -passes=helloworld
foo
bar

我們的 Pass 執行了,並如預期印出了函數的名稱!

測試 Pass

測試我們的 Pass 對於防止未來回歸非常重要。我們將在 llvm/test/Transforms/Utils/helloworld.ll 新增一個 lit 測試。有關測試的更多資訊,請參閱 LLVM 測試基礎架構指南

$ cat llvm/test/Transforms/Utils/helloworld.ll
; RUN: opt -disable-output -passes=helloworld %s 2>&1 | FileCheck %s

; CHECK: {{^}}foo{{$}}
define i32 @foo() {
  %a = add i32 2, 3
  ret i32 %a
}

; CHECK-NEXT: {{^}}bar{{$}}
define void @bar() {
  ret void
}

$ ninja -C build check-llvm
# runs our new test alongside all other llvm lit tests

常見問題

必要的 Pass

定義一個傳回 true 的靜態 isRequired() 方法的 Pass 是必要的 Pass。例如

class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {
public:
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);

  static bool isRequired() { return true; }
};

必要的 Pass 是不可略過的 Pass。必要 Pass 的一個例子是 AlwaysInlinerPass,它必須始終執行以保留 alwaysinline 語意。Pass 管理器是必要的,因為它們可能包含其他必要的 Pass。

Pass 如何被略過的一個例子是 optnone 函數屬性,它指定不應在函數上執行最佳化。必要的 Pass 仍然會在 optnone 函數上執行。

更多實作細節,請參閱 PassInstrumentation::runBeforePass()

將 Pass 註冊為外掛程式

LLVM 提供了一種機制,可在各種工具(如 clangopt)中註冊 Pass 外掛程式。Pass 外掛程式可以將 Pass 新增到預設最佳化管線,或透過 opt 等工具手動執行。更多資訊,請參閱 使用新的 Pass 管理器

在儲存庫的根目錄中與其他專案一起建立一個 CMake 專案。此專案必須包含以下最小的 CMakeLists.txt

add_llvm_pass_plugin(MyPassName source.cpp)

有關更多 CMake 細節,請參閱 add_llvm_pass_plugin 的定義。

Pass 必須為新的 Pass 管理器提供至少兩個進入點之一,一個用於靜態註冊,另一個用於動態載入的外掛程式

  • llvm::PassPluginLibraryInfo get##Name##PluginInfo();

  • extern "C" ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() LLVM_ATTRIBUTE_WEAK;

預設情況下,Pass 外掛程式會被動態編譯和連結。將 LLVM_${NAME}_LINK_INTO_TOOLS 設定為 ON 會將專案變成靜態連結的擴充功能。

樹狀結構內部的範例,請參閱 llvm/examples/Bye/

為了讓 PassBuilder 知道靜態連結的 Pass 外掛程式

// Declare plugin extension function declarations.
#define HANDLE_EXTENSION(Ext) llvm::PassPluginLibraryInfo get##Ext##PluginInfo();
#include "llvm/Support/Extension.def"

...

// Register plugin extensions in PassBuilder.
#define HANDLE_EXTENSION(Ext) get##Ext##PluginInfo().RegisterPassBuilderCallbacks(PB);
#include "llvm/Support/Extension.def"

為了讓 PassBuilder 知道動態連結的 Pass 外掛程式

// Load plugin dynamically.
auto Plugin = PassPlugin::Load(PathToPlugin);
if (!Plugin)
  report_error();
// Register plugin extensions in PassBuilder.
Plugin.registerPassBuilderCallbacks(PB);