符号化标记格式¶
概述¶
本文档定义了一种用于日志消息的文本格式,该格式可以由符号化过滤器处理。基本思想是,日志记录代码发出包含原始地址值等的文本,而日志记录代码本身无需执行任何实际工作来将这些值转换为人类可读的格式。相反,日志记录文本使用此处定义的标记格式来识别应在事后转换为人类可读格式的信息片段。与其他标记格式一样,预期大多数文本将按原样显示,而标记元素将被替换为扩展文本或转换为主动 UI 元素,以以符号形式呈现更多详细信息。
这意味着无需在运行时直接访问符号表、DWARF 调试部分或类似信息。在运行时,也不需要任何用于计算信息的人类可读表示的逻辑,例如 C++ 符号反混淆。相反,日志记录必须包含标记元素,这些元素提供理解原始数据所需的环境信息,例如内存布局详细信息。
此格式使用既简单又独特的语法来识别标记元素。它足够简单,可以使用简单的代码进行匹配和解析。它足够独特,以至于看起来像标记元素的开始或结束的字符序列很少(如果有的话)会偶然出现在日志文本中。它专门旨在不需要清理纯文本,例如 HTML/XML 要求用 <
替换 <
等。
llvm-symbolizer 通过其 --filter-markup
选项包含一个符号化过滤器。此外,当设置 LLVM_ENABLE_SYMBOLIZER_MARKUP
环境变量时,LLVM 实用程序会将堆栈跟踪作为标记发出。
范围和假设¶
符号化过滤器实现将独立于生成日志的目标操作系统和机器架构,以及过滤器运行的主机操作系统和机器架构。
此格式假设符号化过滤器处理完整的完整行。如果长行可能在日志记录管道的某些阶段被拆分,则必须在将行馈送到符号化过滤器之前重新组装它们以恢复原始换行符。大多数标记元素必须完全出现在一行上(通常在标记元素之前和/或之后还有其他文本)。有一些标记元素被指定为跨行,中间有换行符。即使在这些情况下,过滤器也不需要处理标记元素内部任意位置的换行符,而只需要处理某些字段内部的换行符。
此格式假设符号化过滤器处理来自单个进程地址空间上下文的连贯的日志行流。如果日志流交织了来自多个进程的日志行,则必须将其整理成单独的每个进程日志流,并由符号化过滤器的单独实例处理每个流。由于在大多数操作系统中内核和用户进程使用不相交的地址区域,因此如果需要,可以将单个用户进程地址空间加上内核地址空间视为单个地址空间以进行符号化。
对构建 ID 的依赖¶
符号化标记方案依赖于有关运行时内存地址布局的环境信息,以使将标记元素转换为有用的符号形式成为可能。这依赖于对每个地址加载了哪个二进制文件有一个明确的标识。
ELF 构建 ID 是名称为 "GNU"
且类型为 NT_GNU_BUILD_ID
的 ELF 注记的有效负载,这是一个唯一字节序列,用于标识特定的二进制文件(可执行文件、共享库、可加载模块或驱动程序模块)。链接器会根据包含完整符号表和调试信息的哈希自动生成此信息,即使此信息稍后从二进制文件中剥离。
此规范使用 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
后跟偶数个十六进制数字(使用小写或大写表示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
后缀标识。传统的做法通常是将回溯收集为简单的地址列表,从而丢失返回地址代码位置和精确代码位置之间的区别。某些此类代码在报告地址值之前对地址值应用上述“减一”调整,并且并不总是清楚或一致地知道是否应用了此调整。这些模棱两可的情况由
bt
和pc
形式(没有:ra
或: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,其他上下文元素使用它来引用此模块。第一个%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}}}
脚注