除錯資訊的指令參考

本文說明 LLVM 如何在編譯的程式碼生成階段使用值追蹤或指令參考來確定除錯資訊的變數位置。這些內容面向於從事程式碼生成目標和最佳化過程的開發人員。對於任何對底層除錯資訊處理感興趣的人也可能會有幫助。

問題陳述

在編譯結束時,LLVM 必須產生一個 DWARF 位置清單(或類似資訊),描述在該變數的詞彙範圍內,每個指令中可以找到變數的暫存器或堆疊位置。我們可以在編譯過程中追蹤變數所在的虛擬暫存器,但是這在暫存器配置期間容易受到暫存器最佳化和指令移動的影響。

解決方案:指令參考

在指令參考模式中,LLVM 並非識別變數值所在的虛擬暫存器,而是參考定義該值的機器指令和運算元位置。請考慮 LLVM IR 參考指令值的方式

%2 = add i32 %0, %1
  #dbg_value(metadata i32 %2,

在 LLVM IR 中,IR 值與計算該值的指令同義,以至於在記憶體中,值是指向計算指令的指標。指令參考在指令選擇之後,在 LLVM 的程式碼生成後端實現了這種關係。請考慮以下 X86 組合語言和指令參考除錯資訊,它們對應於先前的 LLVM IR

%2:gr32 = ADD32rr %0, %1, implicit-def $eflags, debug-instr-number 1
DBG_INSTR_REF 1, 0, !123, !456, debug-location !789

雖然函數保持 SSA 形式,但虛擬暫存器 %2 足以識別由指令計算的值 - 然而,函數最終會離開 SSA 形式,並且暫存器最佳化會掩蓋所需值所在的暫存器。相反,一種更一致的識別指令值的方法是參考定義值的 MachineOperand:與 MachineOperand 定義的暫存器無關。在上面的程式碼中,DBG_INSTR_REF 指令參考指令編號 1,運算元 0,而 ADD32rr 附加了一個 debug-instr-number 屬性,表示它是指令編號 1。

將變數位置與暫存器解耦避免了涉及暫存器配置和最佳化的困難,但在最佳化指令時需要額外的工具。用計算相同值的最佳化版本替換指令的最佳化必須保留指令編號,或者記錄從舊指令/運算元編號對到新指令/運算元編號對的替換 - 請參閱 MachineFunction::substituteDebugValuesForInst。如果未執行除錯資訊維護,或者指令作為無效程式碼被刪除,則變數位置會被安全地刪除並標記為“已最佳化”。例外情況是經過修改而不是替換的指令,它們始終需要維護除錯資訊。

暫存器配置器考量

當暫存器配置器執行時,偵錯指令不會直接參照任何虛擬暫存器,因此在 regalloc 期間不需要進行耗費資源的位置維護(例如 LiveDebugVariables)。偵錯指令會先從函式中取消連結,然後在暫存器配置完成後再連結回來。

PHI 指令是個例外:在 regalloc 完成後,這些指令會在控制流程合併時成為隱式定義,並且附加到 PHI 指令的任何偵錯序號都會遺失。為了避免這種情況,會在暫存器配置開始時(phi-node-elimination)記錄 PHI 的偵錯序號,然後在 regalloc 完成後插入 DBG_PHI 指令。這需要在 regalloc 期間維護變數所在的暫存器,但僅限於單一位置(區塊進入點),而不是指令範圍。

範例,在 regalloc 之前

bb.2:
  %2 = PHI %1, %bb.0, %2, %bb.1, debug-instr-number 1

之後

bb.2:
  DBG_PHI $rax, 1

LiveDebugValues

在最佳化和程式碼佈局完成後,必須將有關變數值的資訊轉換為變數位置,即暫存器和堆疊位置。這是在 [LiveDebugValues 階段][LiveDebugValues] 中執行的,其中偵錯指令和機器碼會分隔成兩個獨立的函式

  • 一個將值賦予變數名稱,

  • 一個將值賦予機器暫存器和堆疊位置。

LLVM 現有的 SSA 工具會用於為每個函式放置 PHI,位於變數值與機器位置中包含的值之間,並透過值傳播來消除任何不必要的 PHI。然後可以將兩者結合起來,為函式中的每個指令將變數映射到值,然後將值映射到位置。

此過程的關鍵是能夠識別值在暫存器和堆疊位置之間的移動,以便在值駐留在機器中的整個時間內保留其位置。

必要的目標支援和轉換指南

指令參照適用於任何目標,但可能涵蓋範圍不佳。要良好地支援指令參照,需要

  • 實作目標鉤子,讓 LiveDebugValues 能夠追蹤機器中的值,

  • 對目標特定的最佳化進行檢測,以保留指令序號。

目標鉤子

必須實作 TargetInstrInfo::isCopyInstrImpl 來識別任何類似複製的指令,LiveDebugValues 使用它來識別值在暫存器之間移動的時間。

需要使用 TargetInstrInfo::isLoadFromStackSlotPostFETargetInstrInfo::isStoreToStackSlotPostFE 來識別溢出和還原指令。每個指令應分別返回目的或來源暫存器。LiveDebugValues 將追蹤值從堆疊位置移動或移動到堆疊位置的過程。此外,任何寫入堆疊溢出位置的指令都應該附加一個 MachineMemoryOperand,以便 LiveDebugValues 可以辨識該位置已被覆蓋。

目標特定優化檢測

優化分為兩種:一種是變動 MachineInstr 使其執行不同的操作,另一種是建立新的指令來取代舊指令的操作。

前者*必須*進行檢測 - 相關的問題是,任何運算元中的任何暫存器定義是否會因為變動而產生不同的值。如果答案是肯定的,那麼就有風險,即引用該運算元的 DBG_INSTR_REF 指令最終會將不同的值賦予變數,從而使除錯開發人員看到意外的變數值。在這種情況下,請在變動的指令上呼叫 MachineInstr::dropDebugNumber() 以清除其指令編號。任何引用它的 DBG_INSTR_REF 都將產生一個空的變數位置,在除錯器中顯示為「已優化」。

對於後一種優化,為了提高覆蓋率,您應該記錄指令編號替換:從舊指令編號/運算元對到新指令編號/運算元對的映射。考慮一下,如果我們用二地址加法指令替換三地址加法指令

%2:gr32 = ADD32rr %0, %1, debug-instr-number 1

變成

%2:gr32 = ADD32rr %0(tied-def 0), %1, debug-instr-number 2

並在 MachineFunction 中記錄從「指令編號 1 運算元 0」到「指令編號 2 運算元 0」的替換。在 LiveDebugValues 中,DBG_INSTR_REF 將通過替換表進行映射,以找到其引用的值的最新指令編號/運算元編號。

使用 MachineFunction::substituteDebugValuesForInst 自動產生舊指令和新指令之間的替換。它假設舊指令中定義的任何運算元在新指令中都定義在相同的運算元位置。這在大多數情況下都有效,例如在上面的例子中。

如果舊指令和新指令之間的運算元編號不一致,請使用 MachineInstr::getDebugInstrNum 獲取新指令的指令編號,並使用 MachineFunction::makeDebugValueSubstitution 記錄舊指令和新指令中暫存器定義之間的映射。如果舊指令計算的某些值在新指令中不再計算,則不記錄替換 - LiveDebugValues 將安全地丟棄現在不可用的變數值。

如果您的目標複製指令,就像 TailDuplicator 優化遍歷一樣,請勿嘗試保留指令編號或記錄任何替換。MachineFunction::CloneMachineInstr 應該丟棄任何複製指令的指令編號,以避免在 LiveDebugValues 中出現重複的編號。處理重複指令是指令引用的自然擴展,目前尚未實現。

[LiveDebugValues]:project:SourceLevelDebugging.rst#LiveDebugValues 變數位置的擴展