符号化器标记格式¶
概述¶
本文档定义了一种文本格式,用于可以被符号化过滤器处理的日志消息。基本思想是,日志代码发出包含原始地址值等的文本,而日志代码不进行任何实际工作来将这些值转换为人类可读的形式。相反,日志文本使用此处定义的标记格式来标识应在事后转换为人类可读形式的信息片段。与其他标记格式一样,期望是大多数文本将按原样显示,而标记元素将被替换为扩展文本,或转换为活动 UI 元素,以符号形式呈现更多详细信息。
这意味着在运行时无需直接访问符号表、DWARF 调试节或类似信息。运行时也不需要任何旨在计算信息的易于理解的表示形式的逻辑,例如 C++ 符号反解。相反,日志记录必须包含标记元素,这些元素提供必要的上下文信息以理解原始数据,例如内存布局详细信息。
此格式使用既简单又独特的语法来标识标记元素。它足够简单,可以使用简单的代码进行匹配和解析。它足够独特,以至于看起来像标记元素开始或结束的字符序列很少甚至不会偶然出现在日志文本中。它的特定目的是不需要清理纯文本,例如 HTML/XML 要求将 <
替换为 <
等。
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 sanitizer 运行时)的一种让步,这些代码使用特定颜色,并且需要进行大量更改才能生成语义标记。
单个控制序列更改“状态”,而不是围绕受影响文本的层次结构。
过滤器仅在单行内处理 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
开头,后跟偶数个十六进制数字(使用小写或大写A
–F
)。如果数字全部为0
,则可以省略0x
前缀。单个值中预计不会出现超过 16 个十六进制数字(64 位)。%u
非负十进制整数。
%i
非负整数。如果以
0x
为前缀,则数字为十六进制;如果以0
为前缀,则为八进制;否则为十进制。%x
偶数个十六进制数字的序列(使用小写或大写
A
–F
),没有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
后缀的bt
和pc
形式支持,这表明不清楚这是哪种代码位置。但是,强烈建议所有发射器都使用带后缀的形式并传递未应用任何调整的地址值。当传统做法模棱两可时,大多数情况似乎都是打印作为返回地址代码位置的地址并打印它们而不进行调整。因此,符号化过滤器通常会将“减一字节”调整应用于没有歧义后缀打印的地址。假设在所有受支持的机器上,调用指令都长于一个字节,则第二次应用“减一字节”调整仍然会导致地址位于调用指令中的某个位置,因此这里的一点点马虎通常几乎没有或没有危害。示例
{{{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,其他上下文元素使用该 ID 来引用此模块。第一个%s
是模块的人类可读标识符,例如 ELFDT_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=0x1000
、p_memsz=0x1234
,并且加载在0x7acba69d5000
,那么您需要从0x7acba69d5000
和0x7acba69d6234
之间的任何地址中减去0x7acba69d4000
,以获得模块相对地址。起始地址通常已向下舍入到活动页面大小,并且大小已向上舍入。示例
{{{mmap:0x7acba69d5000:0x5a000:load:1:rx:0x1000}}}
脚注