偵錯資訊遷移:從內建函式到記錄

我們正計劃從 LLVM 中移除偵錯資訊內建函式,因為它們速度慢、笨重,並且如果最佳化過程沒有預料到它們,就會造成混淆。過去指令序列看起來像這樣

    %add = add i32 %foo, %bar
    call void @llvm.dbg.value(metadata %add, ...
    %sub = sub i32 %add, %tosub
    call void @llvm.dbg.value(metadata %sub, ...
    call void @a_normal_function()

其中 dbg.value 內建函式代表偵錯資訊記錄,現在改為列印成

    %add = add i32 %foo, %bar
      #dbg_value(%add, ...
    %sub = sub i32 %add, %tosub
      #dbg_value(%sub, ...
    call void @a_normal_function()

偵錯記錄不是指令,不會出現在指令列表中,也不會出現在您的最佳化過程中,除非您刻意去挖掘它們。

太棒了,我需要做些什麼?

很少 - 我們已經對所有 LLVM 進行了檢測,以處理這些新記錄(”DbgRecords”),並且其行為與過去的 LLVM 行為完全相同。這目前預設為開啟,因此 DbgRecords 將在記憶體、IR 和位元碼中預設使用。

API 變更

有兩個需要注意的重大變化。首先,我們要將一個與偵錯相關的資料位元新增到 BasicBlock::iterator 類別中(這樣我們就可以判斷範圍是否打算在區塊開頭包含偵錯資訊)。這意味著在編寫插入 LLVM IR 指令的過程時,您需要使用 BasicBlock::iterator 而不是僅僅使用裸露的 Instruction * 來識別位置。大多數情況下,這意味著在確定要插入內容的位置後,您還必須在指令位置上呼叫 getIterator - 但是,在區塊開頭插入時,您 *必須* 使用 getFirstInsertionPtgetFirstNonPHIItbegin 並使用該迭代器進行插入,而不是僅僅取得第一個指令的指標。

第二個問題是,如果您手動將指令序列從一個地方傳輸到另一個地方,例如重複使用 moveBefore 而您可能使用了 splice,那麼您應該改用 moveBeforePreserving 方法。moveBeforePreserving 將傳輸偵錯資訊記錄及其附加到的指令。這是今天自動發生的事情 - 如果您對指令序列中的每個元素都使用 moveBefore,那麼偵錯內建函式將在您的程式碼正常執行過程中被移動,但是我們在非指令偵錯資訊中失去了這種行為。

如需有關如何更新現有程式碼以支援偵錯記錄的更深入說明,請參閱以下指南

文字 IR 變更

隨著我們從使用偵錯內嵌函式轉向使用偵錯記錄,任何依賴解析 LLVM 生成的 IR 的工具都需要處理新的格式。在大多數情況下,列印形式的偵錯內嵌函式呼叫和偵錯記錄之間的差異很小

  1. 新增兩個額外的縮排空格。

  2. 文字 (tail|notail|musttail)? call void @llvm.dbg.<type> 會替換為 #dbg_<type>

  3. 會從內嵌函式的每個參數中移除前導 metadata

  4. DILocation 從格式為 !dbg !<Num> 的指令附件變更為作為最後一個參數傳遞給偵錯記錄的普通參數,即 !<Num>

遵循這些規則,我們有這個偵錯內嵌函式和等效偵錯記錄的範例

; Debug Intrinsic:
  call void @llvm.dbg.value(metadata i32 %add, metadata !10, metadata !DIExpression()), !dbg !20
; Debug Record:
    #dbg_value(i32 %add, !10, !DIExpression(), !20)

測試更新

由於使用記錄的變更,主 LLVM 儲存庫下游測試 LLVM IR 輸出的任何測試都可能會中斷。考慮到上述更新規則,將個別測試更新為預期記錄而不是內嵌函式應該很簡單。然而,更新許多測試可能會很繁重;若要更新主儲存庫中的 lit 測試,請使用下列步驟

  1. 將失敗的 lit 測試清單收集到一個檔案 failing-tests.txt 中,以換行符號分隔(並以換行符號結尾)。

  2. 使用以下行將失敗的測試分成使用 update_test_checks 的測試和不使用的測試

    $ while IFS= read -r f; do grep -q "Assertions have been autogenerated by" "$f" && echo "$f" >> update-checks-tests.txt || echo "$f" >> manual-tests.txt; done < failing-tests.txt
    
  3. 對於使用 update_test_checks 的測試,請執行適當的 update_test_checks 腳本 - 對於主 LLVM 儲存庫,這是透過以下方式實現的

    $ xargs ./llvm/utils/update_test_checks.py --opt-binary ./build/bin/opt < update-checks-tests.txt
    $ xargs ./llvm/utils/update_cc_test_checks.py --llvm-bin ./build/bin/ < update-checks-tests.txt
    
  4. 其餘的測試可以手動更新,但如果測試數量很多,則以下腳本可能會有所幫助;首先,用於從檔案中提取檢查行首碼的腳本

    $ cat ./get-checks.sh
    #!/bin/bash
    
    # Always add CHECK, since it's more effort than it's worth to filter files where
    # every RUN line uses other check prefixes.
    # Then detect every instance of "check-prefix(es)=..." and add the
    # comma-separated arguments as extra checks.
    for filename in "$@"
    do
        echo "$filename,CHECK"
        allchecks=$(grep -Eo 'check-prefix(es)?[ =][A-Z0-9_,-]+' $filename | sed -E 's/.+[= ]([A-Z0-9_,-]+).*/\1/g; s/,/\n/g')
        for check in $allchecks; do
            echo "$filename,$check"
        done
    done
    

    然後是第二個腳本,用於執行實際更新每個失敗測試中的檢查行的操作,並使用一系列簡單的替換模式

    $ cat ./substitute-checks.sh
    #!/bin/bash
    
    file="$1"
    check="$2"
    
    # Any test that explicitly tests debug intrinsic output is not suitable to
    # update by this script.
    if grep -q "write-experimental-debuginfo=false" "$file"; then
        exit 0
    fi
    
    sed -i -E -e "
    /(#|;|\/\/).*$check[A-Z0-9_\-]*:/!b
    /DIGlobalVariableExpression/b
    /!llvm.dbg./bpostcall
    s/((((((no|must)?tail )?call.*)?void )?@)?llvm.)?dbg\.([a-z]+)/#dbg_\7/
    :postcall
    /declare #dbg_/d
    s/metadata //g
    s/metadata\{/{/g
    s/DIExpression\(([^)]*)\)\)(,( !dbg)?)?/DIExpression(\1),/
    /#dbg_/!b
    s/((\))?(,) )?!dbg (![0-9]+)/\3\4\2/
    s/((\))?(, ))?!dbg/\3/
    " "$file"
    

    這兩個腳本可以結合使用在 manual-tests.txt 中的清單上,如下所示

    $ cat manual-tests.txt | xargs ./get-checks.sh | sort | uniq | awk -F ',' '{ system("./substitute-checks.sh " $1 " " $2) }'
    

    這些腳本成功處理了 clang/testllvm/test 中的絕大多數檢查。

  5. 驗證產生的測試是否通過,並檢測任何失敗的測試

    $ xargs ./build/bin/llvm-lit -q < failing-tests.txt
    ********************
    Failed Tests (5):
    LLVM :: DebugInfo/Generic/dbg-value-lower-linenos.ll
    LLVM :: Transforms/HotColdSplit/transfer-debug-info.ll
    LLVM :: Transforms/ObjCARC/basic.ll
    LLVM :: Transforms/ObjCARC/ensure-that-exception-unwind-path-is-visited.ll
    LLVM :: Transforms/SafeStack/X86/debug-loc2.ll
    
    
    Total Discovered Tests: 295
    Failed: 5 (1.69%)
    
  6. 某些測試可能已失敗 - 更新腳本過於簡化,並且不會跨行保留任何上下文,因此在某些情況下它們將無法處理;其餘情況必須手動更新(或由其他腳本處理)。

C-API 變更

新增的一些函式是暫時的,將來會被棄用。其目的是在過渡期間幫助下游項目適應。

Deleted functions
-----------------
LLVMDIBuilderInsertDeclareBefore   # Insert a debug record (new debug info format) instead of a debug intrinsic (old debug info format).
LLVMDIBuilderInsertDeclareAtEnd    # Same as above.
LLVMDIBuilderInsertDbgValueBefore  # Same as above.
LLVMDIBuilderInsertDbgValueAtEnd   # Same as above.

New functions (to be deprecated)
--------------------------------
LLVMIsNewDbgInfoFormat     # Returns true if the module is in the new non-instruction mode.
LLVMSetIsNewDbgInfoFormat  # Convert to the requested debug info format.

New functions (no plans to deprecate)
-------------------------------------
LLVMDIBuilderInsertDeclareRecordBefore   # Insert a debug record (new debug info format).
LLVMDIBuilderInsertDeclareRecordAtEnd    # Same as above. See info below.
LLVMDIBuilderInsertDbgValueRecordBefore  # Same as above. See info below.
LLVMDIBuilderInsertDbgValueRecordAtEnd   # Same as above. See info below.

LLVMPositionBuilderBeforeDbgRecords          # See info below.
LLVMPositionBuilderBeforeInstrAndDbgRecords  # See info below.

LLVMDIBuilderInsertDeclareRecordBeforeLLVMDIBuilderInsertDeclareRecordAtEndLLVMDIBuilderInsertDbgValueRecordBeforeLLVMDIBuilderInsertDbgValueRecordAtEnd 正在取代已刪除的 LLVMDIBuilderInsertDeclareBefore 類型函數。

除了插入位置設置在目標指令之前的偵錯記錄之前外,LLVMPositionBuilderBeforeDbgRecordsLLVMPositionBuilderBeforeInstrAndDbgRecords 的行為與 LLVMPositionBuilderLLVMPositionBuilderBefore 相同。請注意,這並不意味著選定指令之前的偵錯內建函式會被跳過,只有偵錯記錄(與偵錯內建函式不同,它們本身不是指令)才會被跳過。

如果您不知道要呼叫哪個函式,請遵循以下規則:如果您嘗試在區塊的開頭插入,或者出於任何其他原因特意跳過偵錯內建函式來確定插入點,則呼叫新的函式。

LLVMPositionBuilderLLVMPositionBuilderBefore 保持不變。它們插入在指示的指令之前,但在任何附加的偵錯記錄之後。

新的「偵錯記錄」模型

以下是取代偵錯內建函式的新的表示法的簡要概述;有關更新舊程式碼的說明性指南,請參閱這裡

您究竟用什麼取代了偵錯內建函式?

我們正在使用一個專用的 C++ 類別,稱為 DbgRecord 來儲存偵錯資訊,在任何 LLVM IR 程式中,每個偵錯內建函式的實例與每個 DbgRecord 物件之間都存在一對一的關係;這些 DbgRecord 在 IR 中表示為非指令偵錯記錄,如 [原始碼級偵錯](project:SourceLevelDebugging.rst#Debug Records) 文件中所述。這個類別有一組子類別,它們儲存與偵錯內建函式中儲存的資訊完全相同的資訊。每個子類別也幾乎具有完全相同的方法集,它們的行為方式也相同

https://llvm.dev.org.tw/docs/doxygen/classllvm_1_1DbgRecord.html https://llvm.dev.org.tw/docs/doxygen/classllvm_1_1DbgVariableRecord.html https://llvm.dev.org.tw/docs/doxygen/classllvm_1_1DbgLabelRecord.html

這允許您將 DbgVariableRecord 視為 dbg.value/dbg.declare/dbg.assign 內建函式,例如在泛型(自動參數)lambda 中,以及 DbgLabelRecorddbg.label 也是如此。

這些 DbgRecords 如何融入指令流?

像這樣

                 +---------------+          +---------------+
---------------->|  Instruction  +--------->|  Instruction  |
                 +-------+-------+          +---------------+
                         |
                         |
                         |
                         |
                         v
                  +-------------+
          <-------+  DbgMarker  |<-------
         /        +-------------+        \
        /                                 \
       /                                   \
      v                                     ^
 +-------------+    +-------------+   +-------------+
 |  DbgRecord  +--->|  DbgRecord  +-->|  DbgRecord  |
 +-------------+    +-------------+   +-------------+

每個指令都有一個指向 DbgMarker 的指標(這將變成可選的),其中包含一個 DbgRecord 物件列表。指令列表中根本沒有出現任何偵錯記錄。DbgRecord 有一個指向其所屬 DbgMarker 的父指標,而每個 DbgMarker 都有一個指回其所屬指令的指標。

圖中沒有顯示的是從 DbgRecord 到 Value/Metadata 階層其他部分的連結:DbgRecord 子類別具有指向它們使用的 DIMetadata 的追蹤指標,而 DbgVariableRecord 具有對儲存在 DebugValueUser 基類中的 Value 的參考。這是指透過 TrackingMetadata 機制,參考 ValueValueAsMetadata 物件。

各種偵錯內建函式(值、宣告、賦值、標籤)都儲存在 DbgRecord 子類別中,其中「RecordKind」欄位用於區分 DbgLabelRecordDbgVariableRecord,而 DbgVariableRecord 類別中的 LocationType 欄位進一步區分它可以表示的各種偵錯變數內建函式。

如何更新現有程式碼

任何以某種方式與偵錯內建函式互動的現有程式碼都需要更新,以便以相同的方式與偵錯記錄互動。更新程式碼時,請牢記以下幾條快速規則

  • 迭代指令時不會看到偵錯記錄;若要尋找緊接在指令之前的偵錯記錄,您需要迭代 Instruction::getDbgRecordRange()

  • 偵錯記錄具有與偵錯內建函式相同的介面,這表示任何對偵錯內建函式進行操作的程式碼都可以輕鬆地應用於偵錯記錄。例外情況是不適用於偵錯記錄的 InstructionCallInst 方法,以及 isa/cast/dyn_cast 方法,它們由 DbgRecord 類別本身的方法取代。

  • 偵錯記錄不能出現在也包含偵錯內建函式的模組中;這兩者互斥。由於偵錯記錄是未來的格式,因此在新程式碼中應優先考慮正確處理記錄。

  • 在不再支援內建函式之前,對於僅處理偵錯內建函式且更新起來不輕鬆的程式碼,一個有效的熱修補程式是使用 Module::setIsNewDbgInfoFormat 將模組轉換為內建函式格式,然後再轉換回來。

    • 這也可以使用 ScopedDbgInfoFormatSetter 類別在模組或個別函式的詞彙範圍內執行

    void handleModule(Module &M) {
      {
        ScopedDbgInfoFormatSetter FormatSetter(M, false);
        handleModuleWithDebugIntrinsics(M);
      }
      // Module returns to previous debug info format after exiting the above block.
    }
    

以下是如何將目前支援偵錯內建函式的現有程式碼更新為支援偵錯記錄的粗略指南。

建立偵錯記錄

啟用新格式後,DIBuilder 類別將自動建立偵錯記錄。與指令一樣,也可以呼叫 DbgRecord::clone 來建立現有記錄的未附加副本。

跳過偵錯記錄,忽略 Values 的偵錯使用,穩定地計算指令等等。

這一切都會透明地發生,不需要您費心!

for (Instruction &I : BB) {
  // Old: Skips debug intrinsics
  if (isa<DbgInfoIntrinsic>(&I))
    continue;
  // New: No extra code needed, debug records are skipped by default.
  ...
}

尋找偵錯記錄

findDbgUsers 等工具現在都有一個可選的參數,它將返回引用 ValueDbgVariableRecord 記錄集。您應該能夠將它們視為內建函數一樣處理。

// Old:
  SmallVector<DbgVariableIntrinsic *> DbgUsers;
  findDbgUsers(DbgUsers, V);
  for (auto *DVI : DbgUsers) {
    if (DVI->getParent() != BB)
      DVI->replaceVariableLocationOp(V, New);
  }
// New:
  SmallVector<DbgVariableIntrinsic *> DbgUsers;
  SmallVector<DbgVariableRecord *> DVRUsers;
  findDbgUsers(DbgUsers, V, &DVRUsers);
  for (auto *DVI : DbgUsers)
    if (DVI->getParent() != BB)
      DVI->replaceVariableLocationOp(V, New);
  for (auto *DVR : DVRUsers)
    if (DVR->getParent() != BB)
      DVR->replaceVariableLocationOp(V, New);

檢查特定位置的偵錯記錄

呼叫 Instruction::getDbgRecordRange() 以取得附加到指令的 DbgRecord 物件的範圍。

for (Instruction &I : BB) {
  // Old: Uses a data member of a debug intrinsic, and then skips to the next
  // instruction.
  if (DbgInfoIntrinsic *DII = dyn_cast<DbgInfoIntrinsic>(&I)) {
    recordDebugLocation(DII->getDebugLoc());
    continue;
  }
  // New: Iterates over the debug records that appear before `I`, and treats
  // them identically to the intrinsic block above.
  // NB: This should always appear at the top of the for-loop, so that we
  // process the debug records preceding `I` before `I` itself.
  for (DbgRecord &DR = I.getDbgRecordRange()) {
    recordDebugLocation(DR.getDebugLoc());
  }
  processInstruction(I);
}

這也可以通過 filterDbgVars 函數傳遞,以專門迭代更常用的 DbgVariableRecords。

for (Instruction &I : BB) {
  // Old: If `I` is a DbgVariableIntrinsic we record the variable, and apply
  // extra logic if it is an `llvm.dbg.declare`.
  if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
    recordVariable(DVI->getVariable());
    if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
      recordDeclareAddress(DDI->getAddress());
    continue;
  }
  // New: `filterDbgVars` is used to iterate over only DbgVariableRecords.
  for (DbgVariableRecord &DVR = filterDbgVars(I.getDbgRecordRange())) {
    recordVariable(DVR.getVariable());
    // Debug variable records are not cast to subclasses; simply call the
    // appropriate `isDbgX()` check, and use the methods as normal.
    if (DVR.isDbgDeclare())
      recordDeclareAddress(DVR.getAddress());
  }
  // ...
}

處理個別的偵錯記錄

在大多數情況下,任何對偵錯內建函數進行操作的程式碼都可以提取到一個模板函數或自動 lambda(如果它還不在其中)中,該函數可以應用於偵錯內建函數和偵錯記錄 - 但請記住主要例外是 isa/cast/dyn_cast 不適用於 DbgVariableRecord 類型。

// Old: Function that operates on debug variable intrinsics in a BasicBlock, and
// collects llvm.dbg.declares.
void processDbgInfoInBlock(BasicBlock &BB,
                           SmallVectorImpl<DbgDeclareInst*> &DeclareIntrinsics) {
  for (Instruction &I : BB) {
    if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
      processVariableValue(DebugVariable(DVI), DVI->getValue());
      if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
        Declares.push_back(DDI);
      else if (!isa<Constant>(DVI->getValue()))
        DVI->setKillLocation();
    }
  }
}

// New: Template function is used to deduplicate handling of intrinsics and
// records.
// An overloaded function is also used to handle isa/cast/dyn_cast operations
// for intrinsics and records, since those functions cannot be directly applied
// to DbgRecords.
DbgDeclareInst *DynCastToDeclare(DbgVariableIntrinsic *DVI) {
  return dyn_cast<DbgDeclareInst>(DVI);
}
DbgVariableRecord *DynCastToDeclare(DbgVariableRecord *DVR) {
  return DVR->isDbgDeclare() ? DVR : nullptr;
}

template<typename DbgVarTy, DbgDeclTy>
void processDbgVariable(DbgVarTy *DbgVar,
                       SmallVectorImpl<DbgDeclTy*> &Declares) {
    processVariableValue(DebugVariable(DbgVar), DbgVar->getValue());
    if (DbgDeclTy *DbgDeclare = DynCastToDeclare(DbgVar))
      Declares.push_back(DbgDeclare);
    else if (!isa<Constant>(DbgVar->getValue()))
      DbgVar->setKillLocation();
};

void processDbgInfoInBlock(BasicBlock &BB,
                           SmallVectorImpl<DbgDeclareInst*> &DeclareIntrinsics,
                           SmallVectorImpl<DbgVariableRecord*> &DeclareRecords) {
  for (Instruction &I : BB) {
    if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I))
      processDbgVariable(DVI, DeclareIntrinsics);
    for (DbgVariableRecord *DVR : filterDbgVars(I.getDbgRecordRange()))
      processDbgVariable(DVR, DeclareRecords);
  }
}

移動和刪除偵錯記錄

您可以使用 DbgRecord::removeFromParent 從其標記中取消連結 DbgRecord,然後使用 BasicBlock::insertDbgRecordBeforeBasicBlock::insertDbgRecordAfterDbgRecord 重新插入到其他位置。您不能在 DbgRecord 列表中的任意位置插入 DbgRecord(如果您使用 llvm.dbg.value 執行此操作,則不太可能是正確的)。

通過呼叫 eraseFromParent 刪除 DbgRecord

// Old: Move a debug intrinsic to the start of the block, and delete all other intrinsics for the same variable in the block.
void moveDbgIntrinsicToStart(DbgVariableIntrinsic *DVI) {
  BasicBlock *ParentBB = DVI->getParent();
  DVI->removeFromParent();
  for (Instruction &I : ParentBB) {
    if (auto *BlockDVI = dyn_cast<DbgVariableIntrinsic>(&I))
      if (BlockDVI->getVariable() == DVI->getVariable())
        BlockDVI->eraseFromParent();
  }
  DVI->insertBefore(ParentBB->getFirstInsertionPt());
}

// New: Perform the same operation, but for a debug record.
void moveDbgRecordToStart(DbgVariableRecord *DVR) {
  BasicBlock *ParentBB = DVR->getParent();
  DVR->removeFromParent();
  for (Instruction &I : ParentBB) {
    for (auto &BlockDVR : filterDbgVars(I.getDbgRecordRange()))
      if (BlockDVR->getVariable() == DVR->getVariable())
        BlockDVR->eraseFromParent();
  }
  DVR->insertBefore(ParentBB->getFirstInsertionPt());
}

懸空的偵錯記錄怎麼辦?

如果您有一個像這樣的區塊

    foo:
      %bar = add i32 %baz...
      dbg.value(metadata i32 %bar,...
      br label %xyzzy

您的優化過程可能希望刪除終止符,然後對區塊執行某些操作。當偵錯信息保存在指令中時,這很容易做到,但是使用 DbgRecord 時,一旦終止符被刪除,在上面的區塊中就沒有尾隨指令可以附加變量信息。對於這種退化的區塊,DbgRecord 會臨時存儲在 LLVMContext 的映射中,並在終止符重新插入區塊或在 end() 處插入其他指令時重新插入。

在極少數情況下,當優化過程刪除終止符然後決定刪除整個區塊時,這在技術上可能會導致問題。(我們建議不要這樣做)。

還有其他嗎?

上述指南並未涵蓋所有可能適用於除錯內建函式的模式;如同在指南開頭所述,您可以暫時將目標模組從除錯記錄轉換為內建函式作為權宜之計。大多數可在除錯內建函式上執行的操作,對於除錯記錄都有完全相同的對應操作,但如果您遇到任何例外情況,請閱讀類別文件(連結在此)可能會有所幫助,現有程式碼庫中可能會有範例,您也可以隨時在論壇上尋求幫助。