符號標記格式

概覽

本文件定義了一種用於日誌訊息的文字格式,該格式可以由符號化篩選器處理。基本概念是,日誌記錄程式碼會發出包含原始地址值等的文字,而日誌記錄程式碼不會執行任何實際工作將這些值轉換為人類可讀的形式。相反的,日誌記錄文字使用此處定義的標記格式來識別應該在事後轉換為人類可讀形式的資訊片段。與其他標記格式一樣,預期大多數文字將按原樣顯示,而標記元素將替換為擴展文字,或轉換為以符號形式呈現更多細節的活動 UI 元素。

這意味著在執行時不需要直接存取符號表、DWARF 除錯區段或類似資訊。在執行時也不需要任何用於計算人類可讀資訊呈現的邏輯,例如 C++ 符號還原。相反的,日誌記錄必須包含標記元素,這些元素提供理解原始資料所需的上下文資訊,例如記憶體佈局細節。

此格式使用簡單且獨特的語法來識別標記元素。它非常簡單,可以使用簡單的程式碼進行匹配和解析。它非常獨特,以至於看起來像標記元素的開始或結束的字元序列很少會偶然出現在日誌記錄文字中。它專門設計為不需要清理純文字,例如 HTML/XML 要求將 < 替換為 &lt; 等。

llvm-symbolizer 通過其 --filter-markup 選項包含一個符號化篩選器。此外,當設定了 LLVM_ENABLE_SYMBOLIZER_MARKUP 環境變數時,LLVM 公用程式會將堆疊追蹤作為標記發出。

範圍和假設

符號化篩選器實現將獨立於產生日誌的目標作業系統和機器架構,以及執行篩選器的主機作業系統和機器架構。

此格式假設符號化過濾器處理的是完整的整行。如果長行在記錄管線的某個階段可能被拆分,則必須在將行送入符號化過濾器之前重新組合它們以恢復原始的換行符。大多數標記元素必須完全出現在單行上(通常在標記元素之前和/或之後還有其他文字)。有些標記元素被指定為跨越多行,元素中間有換行符。即使在這些情況下,也不期望過濾器處理標記元素內部任意位置的換行符,而只處理某些欄位內的換行符。

此格式假設符號化過濾器處理來自單一行程式地址空間上下文的一致日誌行流。如果日誌流混合了來自多個行程的日誌行,則必須將這些日誌行整理到每個行程的獨立日誌流中,並且每個流由符號化過濾器的獨立實例處理。由於核心和使用者行程在大多数操作系統中使用不相交的地址區域,因此如果需要,單一使用者行程地址空間加上核心地址空間可以被視為單一地址空間以進行符號化。

對建置 ID 的依賴性

符號化器標記方案依賴於有關運行時內存地址佈局的上下文資訊,以便將標記元素轉換為有用的符號形式。這依賴於明確識別哪個二進制文件加載在每個地址。

ELF 建置 ID 是名稱為 "GNU" 且類型為 NT_GNU_BUILD_ID 的 ELF 備註的有效負載,它是一個唯一字節序列,用於識別特定的二進制文件(可執行文件、共享庫、可加載模組或驅動程序模組)。鏈接器會根據包含完整符號表和調試信息的哈希值自動生成此 ID,即使這些信息稍後會從二進制文件中刪除。

本规范使用 ELF 建置 ID 作為識別二進制文件的唯一方法。與日誌相關的每個二進制文件都必須鏈接到唯一的建置 ID。符號化過濾器必須具有一些方法,可以將建置 ID 映射回原始 ELF 二進制文件(完整的未剝離二進制文件,或與單獨的調試文件配對的剝離二進制文件)。

顏色

標記格式支持 ANSI X3.64 SGR(選擇圖形再现)控制序列的受限子集。這些與其他標記元素不同

  • 它們指定的是呈現細節(粗體或顏色),而不是語義信息。語義含義與顏色(例如,紅色表示錯誤)的關聯是由執行日誌記錄的代碼選擇的,而不是由符號化過濾器的 UI 呈現選擇的。這是對現有代碼(例如,LLVM 消毒器運行時)的讓步,這些代碼使用特定的顏色,並且需要進行大量更改才能生成語義標記。

  • 單個控制序列會更改“狀態”,而不是圍繞受影響文本的層次結構。

過濾器只會在單行內處理 ANSI SGR 控制序列。如果遇到進入粗體或顏色狀態的控制序列,則預計在該行結束之前會遇到重置為默認狀態的控制序列。如果在一行的末尾留下“懸掛”狀態,則過濾器可能會為下一行重置為默認狀態。

SGR 控制序列不會在任何其他標記元素內部解釋。但是,其他標記元素可能會出現在 SGR 控制序列之間,並且顏色/粗體狀態預計會應用於在過濾器輸出中替換標記元素的符號輸出。

可接受的 SGR 控制序列都採用 "\033[%um" 的形式(此處使用 C 字符串語法表示),其中 %u 是以下之一

代碼

效果

備註

0

重設為預設格式。

1

粗體文字

與顏色狀態結合,不會重設它們。

30

黑色前景

31

紅色前景

32

綠色前景

33

黃色前景

34

藍色前景

35

洋紅色前景

36

青色前景

37

白色前景

常見標記元素語法

所有標記元素都共用一個通用的語法結構,以便於簡單的匹配和解析程式碼。每個元素的形式為

{{{tag:fields}}}

tag 識別以下描述的元素類型之一,並且始終是一個必須使用小寫的簡短字母字串。元素的其餘部分由一個或多個欄位組成。欄位以 : 分隔,並且不能包含任何 :} 字元。每個元素類型都指定了必須或可以出現多少個欄位以及它們包含的內容。

在欄位內容中不應解釋任何標記元素或 ANSI SGR 控制序列。

實作必須忽略預期欄位之後的標記欄位;這允許添加新欄位以向後相容地擴展元素。實作不需要靜默地忽略它們,但元素應該表現得好像欄位已被移除一樣。

在每種元素類型的描述中,printf 風格的佔位符表示欄位內容

%s

一個可列印字元字串,不包括 :}

%p

一個由 0x 後跟偶數個十六進制數字(使用小寫或大寫的 AF)表示的地址值。如果所有數字都是 0,則可以省略 0x 前綴。預計單個值中最多出現 16 個十六進制數字(64 位)。

%u

一個非負十進制整數。

%i

一個非負整數。如果以 0x 為前綴,則數字為十六進制;如果以 0 為前綴,則為八進制;否則為十進制。

%x

一個由偶數個十六進制數字(使用小寫或大寫的 AF)組成的序列,沒有 0x 前綴。這表示一個任意的字節序列,例如 ELF 建置 ID。

呈現元素

這些元素傳達要以人類可讀的符號形式顯示的特定程式實體。

{{{symbol:%s}}}

這裡的 %s 是符號或類型的連結名稱。它可能需要根據語言 ABI 規則進行名稱還原。即使對於未經名稱還原的名稱,也建議使用此標記元素來識別符號名稱,以便可以以獨特的方式呈現它。

範例

{{{symbol:_ZN7Mangled4NameEv}}}
{{{symbol:foobar}}}

{{{pc:%p}}}, {{{pc:%p:ra}}}, {{{pc:%p:pc}}}

這裡的 %p 是程式碼位置的記憶體位址。它可能會以函式名稱和原始碼位置的形式呈現。後兩種形式區分了程式碼位置的種類,如下面針對 bt 元素的詳細說明。

範例

{{{pc:0x12345678}}}
{{{pc:0xffffffff9abcdef0}}}

{{{data:%p}}}

這裡的 %p 是資料位置的記憶體位址。它可能會以該位置的全域變數名稱呈現。

範例

{{{data:0x12345678}}}
{{{data:0xffffffff9abcdef0}}}

{{{bt:%u:%p}}}{{{bt:%u:%p:ra}}}{{{bt:%u:%p:pc}}}

這表示回溯中的單一框架。它通常單獨出現在一行上(僅由空格包圍),並以一系列遞增的框架編號排列。因此,人類可讀的輸出可能會格式化為假設這樣,以便它在每行只有一個 bt 元素且每行縮排一致的情況下看起來很好。但它可以出現在任何地方,因此篩選器不應刪除元素周圍的任何非空格文字。

這裡的 %u 是框架編號,它從零開始,表示要識別的錯誤的位置,遞增到一表示框架零的呼叫框架的呼叫者,遞增到二表示框架一的呼叫者,依此類推。%p 是程式碼位置的記憶體位址。

回溯中的程式碼位置來自兩個不同的來源。大多數回溯框架描述了返回地址程式碼位置,即呼叫指令之後的指令。這是尚未執行的程式碼的位置,因為在那裡呼叫的函式尚未返回。因此,實際感興趣的程式碼位置通常是呼叫站點本身,而不是返回地址,即前一條指令。在呈現返回地址框架的原始碼位置時,符號化篩選器將從呼叫站點的實際返回地址中減去一個位元組或一個指令長度,其目的是記錄的地址可以直接轉換為呼叫站點的原始碼位置,而不是之後的明顯返回站點(這可能會造成混淆)。當涉及內聯函式時,呼叫站點和返回站點可能會出現在完全不相關的原始碼位置的不同函式中,而不仅仅是一行之隔,這使得顯示返回站點而不是呼叫站點的混淆變得非常嚴重。

通常,回溯中的第一個框架(“框架零”)標識錯誤、陷阱或非同步中斷的精確程式碼位置,而不是返回地址。在其他時候,即使是第一個框架實際上也是一個返回地址(例如,在物件分配時收集並在使用或誤用已分配的物件時報告的回溯)。當系統支援執行緒內陷阱處理時,第一個框架之後也可能有框架表示精確的中斷程式碼位置而不是返回地址,表示為陷阱處理程式函式(例如,POSIX 系統中的訊號處理程式)的“呼叫者”。

返回地址框架由 :ra 後綴標識。精確的程式碼位置框架由 :pc 後綴標識。

傳統做法通常是將回溯收集為簡單的地址列表,而失去了返回地址代碼位置和精確代碼位置之間的區別。一些這樣的代碼在報告地址值之前會對其應用上述的“減一”調整,並且並不總是清楚或一致地說明是否應用了此調整。這些模棱兩可的情況由沒有 :ra:pc 後綴的 btpc 形式支援,這表示不清楚這是哪種類型的代碼位置。但是,強烈建議所有發射器都使用帶後綴的形式,並傳遞未經調整的地址值。當傳統做法不明確時,大多數情況似乎是列印了返回地址代碼位置的地址,並且在列印時沒有進行調整。因此,符號化過濾器通常會將“減去一字節”調整應用於沒有消除歧義後綴的地址。假設在所有支援的機器上,調用指令都長於一字節,那麼再次應用“減去一字節”調整仍然會產生調用指令中的某個地址,因此這裡的一點疏忽通常不會造成什麼危害。

範例

{{{bt:0:0x12345678:pc}}}
{{{bt:1:0xffffffff9abcdef0:ra}}}

{{{hexdict:...}}} [1]

此元素可以跨越多行。這裡的 ... 是一個鍵值對序列,其中單個 : 將每個鍵与其值分隔開,並且任意空白將這些鍵值對分隔開。每對的值(右側)要么是一個或多個 0 數字,要么是 0x 後跟十六進制數字。每個值都可能是内存地址,也可能是其他整數(包括看起來像可能的内存地址但實際上具有不相關用途的整數)。當有關内存佈局的上下文資訊表明給定值可能是代碼位置或全局變數數據地址時,它可能會以源代碼位置或變數名稱的形式呈現,或者使用可以選擇性地顯示此類解釋的活動 UI。

預期用途是用於寄存器轉儲之類的操作,其中發射器不知道哪些值可能具有符號解釋,但是可以提供合理的符號解釋的呈現方式對於閱讀日誌的人可能非常有用。同時,純文字呈現通常應避免過多干擾轉儲的原始內容和格式。例如,它可以使用帶有源代碼位置的腳註來表示看起來像代碼位置的值。活動 UI 呈現方式可能會按原樣顯示轉儲文字,但會突出顯示具有可用符號資訊的值,並在選擇值時彈出符號詳細資訊的呈現。

範例

{{{hexdict:
    CS:                   0 RIP:     0x6ee17076fb80 EFL:            0x10246 CR2:                  0
    RAX:      0xc53d0acbcf0 RBX:     0x1e659ea7e0d0 RCX:                  0 RDX:     0x6ee1708300cc
    RSI:                  0 RDI:     0x6ee170830040 RBP:     0x3b13734898e0 RSP:     0x3b13734898d8
    R8:      0x3b1373489860 R9:          0x2776ff4f R10:     0x2749d3e9a940 R11:              0x246
    R12:     0x1e659ea7e0f0 R13: 0xd7231230fd6ff2e7 R14:     0x1e659ea7e108 R15:      0xc53d0acbcf0
  }}}

觸發元素

這些元素會導致外部動作,並將以人類可讀的形式呈現給使用者。通常,它們會觸發導致可連結頁面的外部動作。然後,可以將連結或有關外部動作的其他一些資訊性資訊呈現給使用者。

{{{dumpfile:%s:%s}}} [1]

這裡第一個 %s 是用來識別傾印類型的標識符,而第二個 %s 則是用來識別剛才發佈的特定傾印的標識符。傾印的類型、「發佈」的確切含義,以及標識符的性質,都不在標記格式本身的範圍內。一般來說,它可能對應於以該名稱寫入檔案或類似操作。

此元素可能會觸發符號化標記之外的額外後處理工作。它表示已發佈某種類型的傾印檔案。與符號化過濾器相關聯的一些邏輯可能會理解某些類型的傾印檔案,並在遇到此元素時觸發對傾印檔案的額外後處理(例如,生成視覺化、符號化)。預期是,從日誌流中的上下文元素(如下所述)收集的信息,可能是解碼傾印內容所必需的。因此,如果符號化過濾器觸發了其他處理,它可能需要將某種提取形式的上下文信息饋送到這些處理中。

類型標識符的一個例子是 sancov,用於來自 LLVM SanitizerCoverage 的傾印。

範例

{{{dumpfile:sancov:sancov.8675}}}

上下文元素

這些元素提供將呈現元素轉換為符號形式所需的信息。與呈現元素不同,它們與周圍的文字沒有直接關係。上下文元素應該單獨出現在沒有其他非空白文字的行上,以便符號化過濾器可以從其輸出中省略整行,而不會隱藏任何其他日誌文字。

上下文元素本身不一定需要以人類可讀的輸出呈現。然而,即使在符號化之後,它們所傳達的信息對於理解日誌文字也可能是必不可少的。因此,建議在由於任何原因可能無法再輕鬆訪問帶有標記的原始原始日誌時,以某種形式保留這些信息。

上下文元素應該在需要它們之前出現在日誌流中。也就是說,如果某些上下文信息可能會影響符號化過濾器如何解釋或呈現後面的呈現元素,則必要的上下文元素應該已經出現在日誌流中較早的位置。符號化過濾器應該始終可以實現為對原始日誌流的單遍掃描,在掃描過程中累積上下文並處理文字。

{{{reset}}}

這應該在任何其他上下文元素之前輸出。需要此上下文元素是為了支援處理來自多個進程的日誌的實現。這樣的實現可能不知道新進程何時開始或結束。由於某些識別信息(如進程 ID)在新進程和舊進程之間可能相同,因此需要一種方法來區分兩個具有相同識別信息的進程。此元素通知此類實現重置過濾器的狀態,以便不會將先前進程的上下文元素中的信息假設為恰好具有相同識別信息的新進程。

{{{module:%i:%s:%s:...}}}

此元素表示所謂的「模組」。「模組」是單個鏈接的二進制檔案,例如已加載的 ELF 檔案。通常,每個模組佔用一個連續的記憶體範圍。

這裡的 %i 是模組 ID,其他上下文元素可以使用它來引用這個模組。第一個 %s 是模組的人類可讀識別碼,例如 ELF DT_SONAME 字串或檔名;但它也可能是空的。它僅供參考使用。在其他上下文元素中,只有模組 ID 會被用來引用這個模組,而不會使用 %s 字串。定義模組 ID 的 module 元素必須始終在任何其他引用該模組 ID 的元素之前發出,以便過濾器永遠不需要追蹤懸空引用。第二個 %s 是模組類型,它決定了剩餘欄位是什麼。支援的模組類型如下:

  • elf:%x

這裡的 %x 編碼了 ELF 建置 ID。建置 ID 應該引用單個連結的二進制檔案。建置 ID 字串是識別載入此模組的二進制檔案的唯一方法。

範例

{{{module:1:libc.so:elf:83238ab56ba10497}}}

{{{mmap:%p:%i:...}}}

這個上下文元素用於提供有關特定記憶體區域的信息。%p 是起始地址,%i 以十六進制表示記憶體區域的大小。... 部分可以採用不同的形式來提供有關指定記憶體區域的不同信息。允許的形式如下:

  • load:%i:%s:%p

此子元素通知過濾器從模組載入了一個區段。模組由其模組 ID %i 識別。%s 是字母 'r'、'w' 和 'x' 中的一個或多個(按此順序,大小寫不限),表示此記憶體區段是可讀的、可寫的和/或可執行的。符號化過濾器可以使用此信息來猜測地址在給定模組中是程式碼地址還是數據地址。剩餘的 %p 提供模組相對地址。對於 ELF 檔案,模組相對地址將是關聯程序標題的 p_vaddr。例如,如果您的模組的可執行區段具有 p_vaddr=0x1000p_memsz=0x1234,並且載入在 0x7acba69d5000,則您需要從 0x7acba69d50000x7acba69d6234 之間的任何地址中減去 0x7acba69d4000 才能獲得模組相對地址。起始地址通常會向下舍入到活動頁面大小,而大小會向上舍入。

範例

{{{mmap:0x7acba69d5000:0x5a000:load:1:rx:0x1000}}}

註腳