除錯資訊的指令參照

本文檔說明 LLVM 如何使用數值追蹤或指令參照,來決定編譯程式碼產生階段中除錯資訊的變數位置。本文內容旨在為從事程式碼產生目標和最佳化Pass的開發者而寫。對於任何對底層除錯資訊處理感到好奇的人,本文也可能引起他們的興趣。

問題陳述

在編譯結束時,LLVM 必須產生 DWARF 位置列表(或類似的列表),針對變數語法作用域中的每個指令,描述變數可以在哪個暫存器或堆疊位置找到。我們可以追蹤變數在編譯過程中駐留的虛擬暫存器,然而,這很容易受到暫存器配置期間的暫存器最佳化和指令移動的影響。

解決方案:指令參照

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

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

在 LLVM IR 中,IR 值與計算該值的指令是同義的,在記憶體中,Value 是一個指向計算指令的指標。指令參照在 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 指令參照指令編號一,運算元零,而 ADD32rr 具有附加的 debug-instr-number 屬性,指示它是指令編號一。

將變數位置與暫存器解耦可以避免涉及暫存器配置和最佳化的困難,但當指令被最佳化時,需要額外的檢測。將指令替換為計算相同值的最佳化版本的最佳化,必須保留指令編號,或記錄從舊指令/運算元對到新指令/運算元對的替換 – 請參閱 MachineFunction::substituteDebugValuesForInst。如果沒有執行除錯資訊維護,或者指令被消除為無效程式碼,則變數位置會安全地被捨棄並標記為「已最佳化」。例外情況是突變而不是替換的指令,這些指令始終需要除錯資訊維護。

暫存器配置器的考量

當暫存器配置器執行時,除錯指令不會直接參照任何虛擬暫存器,因此在暫存器配置期間無需昂貴的位置維護(即 LiveDebugVariables)。除錯指令與函數解除連結,然後在暫存器配置完成後重新連結。

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

範例,在暫存器配置之前

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

之後

bb.2:
  DBG_PHI $rax, 1

LiveDebugValues

在最佳化和程式碼佈局完成後,關於變數值的信息必須轉換為變數位置,即暫存器和堆疊槽。這在 [LiveDebugValues pass][LiveDebugValues] 中執行,其中除錯指令和機器碼被分離成兩個獨立的函數

  • 一個將值分配給變數名稱,

  • 一個將值分配給機器暫存器和堆疊槽。

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

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

所需目標支援和轉換指南

指令參照將適用於任何目標,但覆蓋率可能很差。良好地支援指令參照需要

  • 實作目標Hook,以允許 LiveDebugValues 追蹤機器中的值,

  • 檢測特定目標的最佳化,以保留指令編號。

目標 Hook

必須實作 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 最佳化Pass一樣,請勿嘗試保留指令編號或記錄任何替換。MachineFunction::CloneMachineInstr 應捨棄任何複製指令的指令編號,以避免重複編號出現在 LiveDebugValues 中。處理重複指令是指令參照的自然擴展,目前尚未實作。

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