使用 LLVM 进行源代码级调试¶
简介¶
本文档是 LLVM 中所有与调试信息相关的中心资料库。它描述了 LLVM 调试信息采用的实际格式,这对那些有兴趣创建前端或直接处理信息的人很有用。此外,本文档提供了 C/C++ 调试信息的具体示例。
LLVM 调试信息背后的理念¶
LLVM 调试信息的理念是捕获源语言抽象语法树的重要部分如何映射到 LLVM 代码。几个设计方面塑造了此处出现的解决方案。重要方面包括:
调试信息对编译器的其他部分的影响应非常小。由于调试信息,不需要修改任何转换、分析或代码生成器。
LLVM 优化应该以 定义明确且易于描述的方式 与调试信息交互。
由于 LLVM 旨在支持任意编程语言,因此 LLVM 到 LLVM 的工具不需要了解源级语言的语义。
源级语言通常彼此 **大不相同**。LLVM 不应对源语言的种类施加任何限制,并且调试信息应适用于任何语言。
在代码生成器支持下,可以使用 LLVM 编译器将程序编译为本地机器代码和标准调试格式。这允许与传统的机器代码级调试器(如 GDB 或 DBX)兼容。
LLVM 实现使用的方法是使用一小组 调试记录 来定义 LLVM 程序对象与源级对象之间的映射。源级程序的描述以 LLVM 元数据保存在 实现定义的格式 中(C/C++ 前端目前使用 DWARF 3 标准 的工作草案 7)。
当调试程序时,调试器与用户交互并将存储的调试信息转换为特定于源语言的信息。因此,调试器必须了解源语言,因此与特定的语言或语言系列相关联。
调试信息使用者¶
调试信息的目的是提供在编译过程中通常被剥离的元信息。此元信息为 LLVM 用户提供了生成代码与原始程序源代码之间的关系。
目前,调试信息有两个后端使用者:DwarfDebug 和 CodeViewDebug。DwarfDebug 生成适用于 GDB、LLDB 和其他基于 DWARF 的调试器的 DWARF。 CodeViewDebug 生成 CodeView,即 Microsoft 调试信息格式,可用于 Microsoft 调试器(如 Visual Studio 和 WinDBG)。LLVM 的调试信息格式主要源自并受 DWARF 启发,但可以转换为其他目标调试信息格式,例如 STABS。
使用调试信息来为生成的代码提供分析的分析工具或从生成的代码重建原始源代码的工具也是合理的。
调试信息和优化¶
LLVM 调试信息的一个极高优先级是使其与优化和分析良好交互。特别是,LLVM 调试信息提供了以下保证:
LLVM 调试信息 **始终提供准确读取程序源级状态的信息**,无论运行了哪些 LLVM 优化。 如何更新调试信息:LLVM Pass 作者指南 指定了如何在各种代码转换中更新调试信息以避免破坏此保证,以及如何尽可能保留有用的调试信息。请注意,某些优化可能会影响使用调试器修改程序当前状态的能力,例如设置程序变量或调用已被删除的函数。
根据需要,可以升级 LLVM 优化以了解调试信息,使它们能够在执行积极优化时更新调试信息。这意味着,通过努力,LLVM 优化器可以像优化非调试代码一样优化调试代码。
LLVM 调试信息不会阻止优化发生(例如内联、基本块重排序/合并/清理、尾部复制等)。
LLVM 调试信息会随着程序的其余部分自动优化,使用现有的工具。例如,链接器会自动合并重复的信息,并且会自动删除未使用的信息。
基本上,调试信息允许您使用“-O0 -g
”编译程序并获得完整的调试信息,允许您在调试器中任意修改程序执行过程。使用“-O3 -g
”编译程序提供了始终可用且准确的完整调试信息(例如,尽管进行了尾调用消除和内联,但您仍然可以获得准确的堆栈跟踪),但您可能会失去修改程序和调用已从程序中优化掉或完全内联的函数的能力。
LLVM 测试套件 提供了一个框架来测试优化器对调试信息的处理。可以像这样运行它:
% cd llvm/projects/test-suite/MultiSource/Benchmarks # or some other level
% make TEST=dbgopt
这将测试调试信息对优化传递的影响。如果调试信息影响优化传递,则将报告为失败。有关 LLVM 测试基础设施以及如何运行各种测试的更多信息,请参阅 LLVM 测试基础设施指南。
调试信息格式¶
LLVM 调试信息经过精心设计,使优化器能够优化程序和调试信息,而无需 unbedingt 了解调试信息。特别是,使用元数据从一开始就避免了重复的调试信息,并且全局死代码消除传递会自动删除函数的调试信息,如果它决定删除该函数。
为此,大多数调试信息(类型、变量、函数、源文件等的描述符)由语言前端以 LLVM 元数据的形式插入。
调试信息的设计独立于目标调试器和调试信息表示(例如 DWARF/Stabs 等)。它使用一个通用传递来解码表示变量、类型、函数、命名空间等的 information:这允许使用任意源语言语义和类型系统,只要为目标调试器编写了一个模块来解释信息即可。
为了提供基本功能,LLVM 调试器确实必须对正在调试的源级语言做出一些假设,尽管它将这些假设降到最低。LLVM 调试器假定的唯一共同特征是 源文件 和 程序对象。调试器使用这些抽象对象来形成堆栈跟踪、显示有关局部变量的信息等。
本节文档首先描述了任何源语言通用的表示方面。 C/C++ 前端特定调试信息 描述了 C 和 C++ 前端使用的 data layout 约定。
调试信息描述符是 专门的元数据节点,是 Metadata
的一等子类。
有两种模型用于定义程序不同状态下源变量的值并跟踪这些值在优化和代码生成过程中的变化: 调试记录(当前默认值)和 内建函数调用(不是默认值,但目前支持向后兼容性),但这两种模型绝不能在 IR 模块中混合使用。有关我们更改为新模型的原因、工作原理以及如何更新旧代码或 IR 以使用调试记录的指南,请参阅 RemoveDIs 文档。
调试记录¶
调试记录定义程序执行期间源变量具有的值;它们与指令交错出现,尽管它们本身不是指令并且对编译器生成的代码没有影响。
LLVM 使用几种类型的调试记录来定义源变量。这些记录的通用语法为:
#dbg_<kind>([<arg>, ]* <DILocation>)
; Using the intrinsic model, the above is equivalent to:
call void llvm.dbg.<kind>([metadata <arg>, ]*), !dbg <DILocation>
调试记录始终以比指令多一个缩进级别打印,并且始终以 #dbg_ 为前缀,并在括号中包含以逗号分隔的参数列表,就像 call 一样。
#dbg_declare
¶
#dbg_declare([Value|MDNode], DILocalVariable, DIExpression, DILocation)
此记录提供有关局部元素(例如,变量)的信息。第一个参数是对应于变量地址的 SSA 值,通常是函数入口块中的静态 alloca。第二个参数是包含变量描述的 局部变量。第三个参数是 复杂表达式。第四个参数是 源位置。#dbg_declare
记录描述了源变量的地址。
%i.addr = alloca i32, align 4
#dbg_declare(ptr %i.addr, !1, !DIExpression(), !2)
; ...
!1 = !DILocalVariable(name: "i", ...) ; int i
!2 = !DILocation(...)
; ...
%buffer = alloca [256 x i8], align 8
; The address of i is buffer+64.
#dbg_declare(ptr %buffer, !3, !DIExpression(DW_OP_plus, 64), !4)
; ...
!3 = !DILocalVariable(name: "i", ...) ; int i
!4 = !DILocation(...)
前端应该在源变量声明点生成恰好一个 #dbg_declare
记录。将变量完全从内存提升到 SSA 值的优化过程将用可能多个 #dbg_value`
记录替换此记录。删除存储的过程实际上是部分提升,它们将插入混合的 #dbg_value
记录以跟踪源变量值在可用时的值。优化后,可能会有多个 #dbg_declare
记录描述变量在内存中生存的程序点。同一具体源变量的所有调用必须就内存位置达成一致。
#dbg_value
¶
#dbg_value([Value|DIArgList|MDNode], DILocalVariable, DIExpression, DILocation)
此记录提供用户源变量设置为新值时的信息。第一个参数是新值。第二个参数是包含变量描述的 局部变量。第三个参数是 复杂表达式。第四个参数是 源位置。
#dbg_value
记录直接描述源变量的值,而不是其地址。请注意,此内在函数的值操作数可能是间接的(即,指向源变量的指针),前提是解释复杂表达式可以得出直接值。
#dbg_assign
¶
#dbg_assign( [Value|DIArgList|MDNode] Value,
DILocalVariable Variable,
DIExpression ValueExpression,
DIAssignID ID,
[Value|MDNode] Address,
DIExpression AddressExpression,
DILocation SourceLocation )
此记录标记 IR 中发生源赋值的位置。它对变量的值进行编码。它引用执行赋值的存储(如果有)和目标地址。
前三个参数与 #dbg_value
的参数相同。第四个参数是用于引用存储的 DIAssignID
。第五个是存储的目标,第六个是修改它的 复杂表达式,第七个是 源位置。
有关更多信息,请参见 调试信息赋值跟踪。
调试器内在函数¶
在内在模式下,LLVM 使用多个内在函数(名称以“llvm.dbg
”为前缀)来跟踪优化和代码生成过程中的源局部变量。这些内在函数中的每一个都对应于上述调试记录之一,但有一些语法差异:调试器内在函数的每个参数都必须包装为元数据,这意味着它必须以 metadata
为前缀,并且每个记录中的 DILocation
参数必须是调用指令的元数据附件,这意味着它出现在参数列表之后,前缀为 !dbg
。
llvm.dbg.declare
¶
void @llvm.dbg.declare(metadata, metadata, metadata)
此内在函数等效于 #dbg_declare
#dbg_declare(i32* %i.addr, !1, !DIExpression(), !2)
call void @llvm.dbg.declare(metadata i32* %i.addr, metadata !1,
metadata !DIExpression()), !dbg !2
llvm.dbg.value
¶
void @llvm.dbg.value(metadata, metadata, metadata)
此内在函数等效于 #dbg_value
#dbg_value(i32 %i, !1, !DIExpression(), !2)
call void @llvm.dbg.value(metadata i32 %i, metadata !1,
metadata !DIExpression()), !dbg !2
llvm.dbg.assign
¶
void @llvm.dbg.assign(metadata, metadata, metadata, metadata, metadata, metadata)
此内在函数等效于 #dbg_assign
#dbg_assign(i32 %i, !1, !DIExpression(), !2,
ptr %i.addr, !DIExpression(), !3)
call void @llvm.dbg.assign(
metadata i32 %i, metadata !1, metadata !DIExpression(), metadata !2,
metadata ptr %i.addr, metadata !DIExpression(), metadata !3), !dbg !3
对象生命周期和作用域¶
在许多语言中,函数中的局部变量的生命周期或作用域可以限制为函数的子集。例如,在 C 系列语言中,变量仅在其定义的源代码块内处于活动状态(可读和可写)。在函数式语言中,值仅在定义后才能读取。尽管这是一个非常明显的概念,但在 LLVM 中建模却并非易事,因为它在这个意义上没有作用域的概念,并且不想与语言的作用域规则绑定。
为了处理这个问题,LLVM 调试格式使用附加到 llvm 指令的元数据来编码行号和作用域信息。例如,考虑以下 C 代码片段
1. void foo() {
2. int X = 21;
3. int Y = 22;
4. {
5. int Z = 23;
6. Z = X;
7. }
8. X = Y;
9. }
编译成 LLVM 后,此函数将表示如下
; Function Attrs: nounwind ssp uwtable
define void @foo() #0 !dbg !4 {
entry:
%X = alloca i32, align 4
%Y = alloca i32, align 4
%Z = alloca i32, align 4
#dbg_declare(ptr %X, !11, !DIExpression(), !13)
store i32 21, i32* %X, align 4, !dbg !13
#dbg_declare(ptr %Y, !14, !DIExpression(), !15)
store i32 22, i32* %Y, align 4, !dbg !15
#dbg_declare(ptr %Z, !16, !DIExpression(), !18)
store i32 23, i32* %Z, align 4, !dbg !18
%0 = load i32, i32* %X, align 4, !dbg !20
store i32 %0, i32* %Z, align 4, !dbg !21
%1 = load i32, i32* %Y, align 4, !dbg !22
store i32 %1, i32* %X, align 4, !dbg !23
ret void, !dbg !24
}
attributes #0 = { nounwind ssp uwtable "less-precise-fpmad"="false" "frame-pointer"="all" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nounwind readnone }
!llvm.dbg.cu = !{!0}
!llvm.module.flags = !{!7, !8, !9}
!llvm.ident = !{!10}
!0 = !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang version 3.7.0 (trunk 231150) (llvm/trunk 231154)", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, enums: !2, retainedTypes: !2, subprograms: !3, globals: !2, imports: !2)
!1 = !DIFile(filename: "/dev/stdin", directory: "/Users/dexonsmith/data/llvm/debug-info")
!2 = !{}
!3 = !{!4}
!4 = distinct !DISubprogram(name: "foo", scope: !1, file: !1, line: 1, type: !5, isLocal: false, isDefinition: true, scopeLine: 1, isOptimized: false, retainedNodes: !2)
!5 = !DISubroutineType(types: !6)
!6 = !{null}
!7 = !{i32 2, !"Dwarf Version", i32 2}
!8 = !{i32 2, !"Debug Info Version", i32 3}
!9 = !{i32 1, !"PIC Level", i32 2}
!10 = !{!"clang version 3.7.0 (trunk 231150) (llvm/trunk 231154)"}
!11 = !DILocalVariable(name: "X", scope: !4, file: !1, line: 2, type: !12)
!12 = !DIBasicType(name: "int", size: 32, align: 32, encoding: DW_ATE_signed)
!13 = !DILocation(line: 2, column: 9, scope: !4)
!14 = !DILocalVariable(name: "Y", scope: !4, file: !1, line: 3, type: !12)
!15 = !DILocation(line: 3, column: 9, scope: !4)
!16 = !DILocalVariable(name: "Z", scope: !18, file: !1, line: 5, type: !12)
!17 = distinct !DILexicalBlock(scope: !4, file: !1, line: 4, column: 5)
!18 = !DILocation(line: 5, column: 11, scope: !17)
!29 = !DILocation(line: 6, column: 11, scope: !17)
!20 = !DILocation(line: 6, column: 9, scope: !17)
!21 = !DILocation(line: 8, column: 9, scope: !4)
!22 = !DILocation(line: 8, column: 7, scope: !4)
!23 = !DILocation(line: 9, column: 3, scope: !4)
此示例说明了有关 LLVM 调试信息的一些重要细节。特别是,它展示了如何将 #dbg_declare
记录和位置信息(附加到指令)一起应用,以允许调试器分析语句、变量定义和用于实现函数的代码之间的关系。
#dbg_declare(ptr %X, !11, !DIExpression(), !13)
; [debug line = 2:9] [debug variable = X]
第一个记录 #dbg_declare
对变量 X
编码调试信息。记录末尾的 !13
位置提供了变量 X
的作用域信息。
!13 = !DILocation(line: 2, column: 9, scope: !4)
!4 = distinct !DISubprogram(name: "foo", scope: !1, file: !1, line: 1, type: !5,
isLocal: false, isDefinition: true, scopeLine: 1,
isOptimized: false, retainedNodes: !2)
这里 !13
是提供 位置信息 的元数据。在此示例中,作用域由 !4
编码,!4
是 子程序描述符。这样,记录的位置信息参数指示变量 X
在函数 foo
中的第 2 行在函数级作用域中声明。
现在让我们来看另一个例子。
#dbg_declare(ptr %Z, !16, !DIExpression(), !18)
; [debug line = 5:11] [debug variable = Z]
第三个记录 #dbg_declare
对变量 Z
编码调试信息。记录末尾的元数据 !18
提供了变量 Z
的作用域信息。
!17 = distinct !DILexicalBlock(scope: !4, file: !1, line: 4, column: 5)
!18 = !DILocation(line: 5, column: 11, scope: !17)
这里 !18
指示 Z
在第 5 行第 11 列在词法作用域 !17
内部声明。词法作用域本身位于上面描述的子程序 !4
内部。
附加到每个指令的作用域信息提供了一种简单的方法来查找作用域覆盖的指令。
优化代码中的对象生命周期¶
在上面的示例中,每个变量赋值都唯一地对应于对堆栈上变量位置的内存存储。但是,在高度优化的代码中,LLVM 将大多数变量提升到 SSA 值,这些值最终可以放置在物理寄存器或内存位置中。为了跟踪编译过程中的 SSA 值,当对象被提升到 SSA 值时,会为每个赋值创建一个 #dbg_value
记录,记录变量的新位置。与 #dbg_declare
记录相比
一个 #dbg_value 会终止任何前面针对(指定变量的任何重叠片段)的 #dbg_values 的效果。
#dbg_value 在 IR 中的位置定义了变量值在指令流中发生变化的位置。
操作数可以是常量,表示变量被赋予一个常量值。
当优化过程更改或移动指令和块时,必须注意更新 #dbg_value
记录——开发人员可能会观察到这些变化反映在调试程序时变量的值中。对于优化程序的任何执行,调试器向开发人员呈现的变量值集不应显示优化程序执行(给定相同的输入)中从未存在过的状态。这样做会通过报告不存在的状态来误导开发人员,损害他们对优化程序的理解并削弱他们对调试器的信任。
有时无法完美地保留变量位置,通常是在优化掉冗余计算时。在这种情况下,应使用操作数为 poison
的 #dbg_value
来终止较早的变量位置,并让调试器向开发人员呈现 optimized out
。从开发人员那里隐瞒这些可能过时的变量值会减少可用的调试信息量,但会提高剩余信息的可靠性。
为了说明一些潜在的问题,请考虑以下示例
define i32 @foo(i32 %bar, i1 %cond) {
entry:
#dbg_value(i32 0, !1, !DIExpression(), !4)
br i1 %cond, label %truebr, label %falsebr
truebr:
%tval = add i32 %bar, 1
#dbg_value(i32 %tval, !1, !DIExpression(), !4)
%g1 = call i32 @gazonk()
br label %exit
falsebr:
%fval = add i32 %bar, 2
#dbg_value(i32 %fval, !1, !DIExpression(), !4)
%g2 = call i32 @gazonk()
br label %exit
exit:
%merge = phi [ %tval, %truebr ], [ %fval, %falsebr ]
%g = phi [ %g1, %truebr ], [ %g2, %falsebr ]
#dbg_value(i32 %merge, !1, !DIExpression(), !4)
#dbg_value(i32 %g, !3, !DIExpression(), !4)
%plusten = add i32 %merge, 10
%toret = add i32 %plusten, %g
#dbg_value(i32 %toret, !1, !DIExpression(), !4)
ret i32 %toret
}
在 !1
和 !3
中包含两个源代码级变量。该函数可能被优化为以下代码
define i32 @foo(i32 %bar, i1 %cond) {
entry:
%g = call i32 @gazonk()
%addoper = select i1 %cond, i32 11, i32 12
%plusten = add i32 %bar, %addoper
%toret = add i32 %plusten, %g
ret i32 %toret
}
应放置哪些 #dbg_value
记录以表示此代码中的原始变量位置?不幸的是,源函数中 !1
的第二个、第三个和第四个 #dbg_values 的操作数(%tval、%fval、%merge)已被优化掉。假设我们无法恢复它们,我们可以考虑将 #dbg_values 放置如下
define i32 @foo(i32 %bar, i1 %cond) {
entry:
#dbg_value(i32 0, !1, !DIExpression(), !4)
%g = call i32 @gazonk()
#dbg_value(i32 %g, !3, !DIExpression(), !4)
%addoper = select i1 %cond, i32 11, i32 12
%plusten = add i32 %bar, %addoper
%toret = add i32 %plusten, %g
#dbg_value(i32 %toret, !1, !DIExpression(), !4)
ret i32 %toret
}
然而,这会导致 !3
同时拥有 @gazonk()
的返回值,以及 !1
拥有常量值零——这对赋值在未优化的程序中从未出现过。为了避免这种情况,我们必须通过在 !3
的 #dbg_value 之前插入一个 poison #dbg_value 来终止 !1
拥有常量值赋值的范围。
define i32 @foo(i32 %bar, i1 %cond) {
entry:
#dbg_value(i32 0, !1, !DIExpression(), !2)
%g = call i32 @gazonk()
#dbg_value(i32 poison, !1, !DIExpression(), !2)
#dbg_value(i32 %g, !3, !DIExpression(), !2)
%addoper = select i1 %cond, i32 11, i32 12
%plusten = add i32 %bar, %addoper
%toret = add i32 %plusten, %g
#dbg_value(i32 %toret, !1, !DIExpression(), !2)
ret i32 %toret
}
还有其他一些 #dbg_value 配置意味着它终止了支配位置定义,而无需添加新的位置。完整的列表如下:
任何位置操作数为
poison
(或undef
)。任何位置操作数都是一个空的元数据元组 (
!{}
)(这在!DIArgList
中不会出现)。没有位置操作数(空的
DIArgList
),并且DIExpression
为空。
这类终止变量位置的 #dbg_value 被称为“kill #dbg_value”或“kill location”,出于遗留原因,术语“undef #dbg_value”可能在现有代码中使用。 DbgVariableIntrinsic
方法 isKillLocation
和 setKillLocation
应该尽可能地使用,而不是直接检查位置操作数来检查或设置 #dbg_value 是否为 kill location。
一般来说,如果任何 #dbg_value 的操作数被优化掉且无法恢复,则需要一个 kill #dbg_value 来终止早期的变量位置。当调试器能够观察到赋值的重新排序时,可能需要额外的 kill #dbg_value。
CodeGen 期间变量位置元数据的转换方式¶
LLVM 在中级和后端传递过程中保留调试信息,最终生成源级信息和指令范围之间的映射。对于行号信息来说,这相对比较简单,因为将指令映射到行号是一个简单的关联。但是,对于变量位置来说,情况则更加复杂。由于每个 #dbg_value
记录都表示将值赋值给源变量的源级赋值,因此调试记录实际上在 LLVM IR 中嵌入了一个小型的指令式程序。在 CodeGen 结束时,这将变成从每个变量到其机器位置(跨越指令范围)的映射。从 IR 到对象发射,影响变量位置保真度的主要转换是
指令选择
寄存器分配
块布局
下面将分别讨论这些内容。此外,指令调度可以显著改变程序的顺序,并且发生在许多不同的传递中。
一些变量位置在 CodeGen 期间不会发生转换。由 #dbg_declare
指定的栈位置在整个函数的持续时间内都是有效且不变的,并且记录在一个简单的 MachineFunction 表中。函数序言和结尾处的地址更改也会被忽略:框架设置和销毁可能需要多个指令,需要在输出二进制文件中描述过多的调试信息,并且调试器应该跳过这些指令。
指令选择和 MIR 中的变量位置¶
指令选择从 IR 函数创建 MIR 函数,就像它将 intermediate
指令转换为机器指令一样, intermediate
变量位置也必须变为机器变量位置。在 IR 中,变量位置始终由 Value 标识,但在 MIR 中,可以有不同类型的变量位置。此外,一些 IR 位置变得不可用,例如,如果多个 IR 指令的操作组合成一个机器指令(例如乘加),则中间 Value 会丢失。为了跟踪指令选择过程中的变量位置,首先将它们分离成不依赖于代码生成的位置(常量、栈位置、分配的虚拟寄存器)和依赖于代码生成的位置。对于那些依赖于代码生成的位置,调试元数据附加到 SelectionDAG 中的 SDNode 上。在指令选择完成并创建 MIR 函数后,如果与调试元数据关联的 SDNode 分配了一个虚拟寄存器,则该虚拟寄存器将用作变量位置。如果 SDNode 折叠到机器指令中或以其他方式转换为非寄存器,则变量位置将变得不可用。
不可用的位置被视为已被优化掉:在 IR 中,位置将由调试记录分配 undef
,在 MIR 中,使用等效的位置。
在为每个变量分配 MIR 位置后,将插入与每个 #dbg_value
记录相对应的机器伪指令。此类指令有两种形式。
第一种形式,DBG_VALUE
,如下所示:
DBG_VALUE %1, $noreg, !123, !DIExpression()
- 并具有以下操作数:
第一个操作数可以将变量位置记录为寄存器、帧索引、立即数或基地址寄存器(如果原始调试记录引用内存)。
$noreg
表示变量位置未定义,相当于undef
#dbg_value 操作数。第二个操作数的类型指示变量位置是由 DBG_VALUE 直接引用,还是间接引用。
$noreg
寄存器表示前者,立即数操作数 (0) 表示后者。操作数 3 是原始调试记录的 Variable 字段。
操作数 4 是原始调试记录的 Expression 字段。
第二种形式,DBG_VALUE_LIST
,如下所示:
DBG_VALUE_LIST !123, !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_arg, 1, DW_OP_plus), %1, %2
- 并具有以下操作数:
第一个操作数是原始调试记录的 Variable 字段。
第二个操作数是原始调试记录的 Expression 字段。
从第 3 个开始的任意数量的操作数记录变量位置操作数的序列,这些操作数可以采用与上面
DBG_VALUE
指令的第一个操作数相同的任何值。这些变量位置操作数插入到最终的 DWARF 表达式中,位置由 DIExpression 中的 DW_OP_LLVM_arg 操作符指示。
插入 DBG_VALUE 的位置应对应于 IR 块中其匹配的 #dbg_value
记录的位置。与优化一样,LLVM 旨在保留源程序中变量赋值发生的顺序。但是 SelectionDAG 执行一些指令调度,这可能会重新排序赋值(下面将讨论)。如果函数参数位置尚未位于开头,则将其移动到函数的开头,以确保它们在函数入口处立即可用。
为了演示指令选择期间的变量位置,请考虑以下示例:
define i32 @foo(i32* %addr) {
entry:
#dbg_value(i32 0, !3, !DIExpression(), !5)
br label %bb1, !dbg !5
bb1: ; preds = %bb1, %entry
%bar.0 = phi i32 [ 0, %entry ], [ %add, %bb1 ]
#dbg_value(i32 %bar.0, !3, !DIExpression(), !5)
%addr1 = getelementptr i32, i32 *%addr, i32 1, !dbg !5
#dbg_value(i32 *%addr1, !3, !DIExpression(), !5)
%loaded1 = load i32, i32* %addr1, !dbg !5
%addr2 = getelementptr i32, i32 *%addr, i32 %bar.0, !dbg !5
#dbg_value(i32 *%addr2, !3, !DIExpression(), !5)
%loaded2 = load i32, i32* %addr2, !dbg !5
%add = add i32 %bar.0, 1, !dbg !5
#dbg_value(i32 %add, !3, !DIExpression(), !5)
%added = add i32 %loaded1, %loaded2
%cond = icmp ult i32 %added, %bar.0, !dbg !5
br i1 %cond, label %bb1, label %bb2, !dbg !5
bb2: ; preds = %bb1
ret i32 0, !dbg !5
}
如果使用 llc -o - -start-after=codegen-prepare -stop-after=expand-isel-pseudos -mtriple=x86_64--
编译此 IR,则会生成以下 MIR:
bb.0.entry:
successors: %bb.1(0x80000000)
liveins: $rdi
%2:gr64 = COPY $rdi
%3:gr32 = MOV32r0 implicit-def dead $eflags
DBG_VALUE 0, $noreg, !3, !DIExpression(), debug-location !5
bb.1.bb1:
successors: %bb.1(0x7c000000), %bb.2(0x04000000)
%0:gr32 = PHI %3, %bb.0, %1, %bb.1
DBG_VALUE %0, $noreg, !3, !DIExpression(), debug-location !5
DBG_VALUE %2, $noreg, !3, !DIExpression(DW_OP_plus_uconst, 4, DW_OP_stack_value), debug-location !5
%4:gr32 = MOV32rm %2, 1, $noreg, 4, $noreg, debug-location !5 :: (load 4 from %ir.addr1)
%5:gr64_nosp = MOVSX64rr32 %0, debug-location !5
DBG_VALUE $noreg, $noreg, !3, !DIExpression(), debug-location !5
%1:gr32 = INC32r %0, implicit-def dead $eflags, debug-location !5
DBG_VALUE %1, $noreg, !3, !DIExpression(), debug-location !5
%6:gr32 = ADD32rm %4, %2, 4, killed %5, 0, $noreg, implicit-def dead $eflags :: (load 4 from %ir.addr2)
%7:gr32 = SUB32rr %6, %0, implicit-def $eflags, debug-location !5
JB_1 %bb.1, implicit $eflags, debug-location !5
JMP_1 %bb.2, debug-location !5
bb.2.bb2:
%8:gr32 = MOV32r0 implicit-def dead $eflags
$eax = COPY %8, debug-location !5
RET 0, $eax, debug-location !5
首先观察到,源 IR 中每个 #dbg_value
记录都有一个 DBG_VALUE 指令,确保不会丢失任何源级赋值。然后考虑记录变量位置的不同方式:
对于第一个 #dbg_value,使用立即数操作数来记录零值。
PHI 指令的 #dbg_value 导致虚拟寄存器
%0
的 DBG_VALUE。第一个 GEP 的效果折叠到第一个加载指令中(作为 4 字节偏移量),但变量位置通过将 GEP 的效果折叠到 DIExpression 中而得以保留。
第二个 GEP 也折叠到相应的加载中。但是,它不够简单,无法保存,并作为
$noreg
DBG_VALUE 发射,表示变量采用未定义的位置。最终的 #dbg_value 将其 Value 放置在虚拟寄存器
%1
中。
指令调度¶
许多传递可以重新调度指令,特别是指令选择和 RA 前后机器调度器。指令调度可以显著改变程序的本质——在(极不可能的)最坏情况下,指令序列可能会完全反转。在这种情况下,LLVM 遵循应用于优化的原则,即调试器不显示任何状态比显示误导性状态更好。因此,每当指令按照执行顺序提前时,任何相应的 DBG_VALUE 都将保留在其原始位置,如果指令被延迟,则变量在延迟期间将被赋予未定义的位置。为了说明这一点,请考虑此伪 MIR:
%1:gr32 = MOV32rm %0, 1, $noreg, 4, $noreg, debug-location !5 :: (load 4 from %ir.addr1)
DBG_VALUE %1, $noreg, !1, !2
%4:gr32 = ADD32rr %3, %2, implicit-def dead $eflags
DBG_VALUE %4, $noreg, !3, !4
%7:gr32 = SUB32rr %6, %5, implicit-def dead $eflags
DBG_VALUE %7, $noreg, !5, !6
假设 SUB32rr 被向前移动,得到以下 MIR:
%7:gr32 = SUB32rr %6, %5, implicit-def dead $eflags
%1:gr32 = MOV32rm %0, 1, $noreg, 4, $noreg, debug-location !5 :: (load 4 from %ir.addr1)
DBG_VALUE %1, $noreg, !1, !2
%4:gr32 = ADD32rr %3, %2, implicit-def dead $eflags
DBG_VALUE %4, $noreg, !3, !4
DBG_VALUE %7, $noreg, !5, !6
在这种情况下,LLVM 会保留如上所示的 MIR。如果我们将虚拟寄存器 %7 的 DBG_VALUE 与 SUB32rr 一起向上移动,我们将重新排序赋值并引入程序的新状态。而使用上述解决方案,调试器将看到较少的变量值组合,因为 !3
和 !5
将同时改变值。这优于歪曲原始程序。
相比之下,如果下沉 MOV32rm,LLVM 将生成以下内容:
DBG_VALUE $noreg, $noreg, !1, !2
%4:gr32 = ADD32rr %3, %2, implicit-def dead $eflags
DBG_VALUE %4, $noreg, !3, !4
%7:gr32 = SUB32rr %6, %5, implicit-def dead $eflags
DBG_VALUE %7, $noreg, !5, !6
%1:gr32 = MOV32rm %0, 1, $noreg, 4, $noreg, debug-location !5 :: (load 4 from %ir.addr1)
DBG_VALUE %1, $noreg, !1, !2
在这里,为了避免呈现 !1
的第一次赋值消失的状态,块顶部的 DBG_VALUE 将变量分配给未定义的位置,直到其值在块末尾可用,并在那里添加额外的 DBG_VALUE。如果在 MOV32rm 下沉过去的指令中出现任何其他 !1
的 DBG_VALUE,则 %1
的 DBG_VALUE 将被删除,并且调试器将永远不会在变量中观察到它。这准确地反映了该值在原始程序的对应部分不可用。
寄存器分配期间的变量位置¶
为了避免调试指令干扰寄存器分配器,LiveDebugVariables 遍布会从 MIR 函数中提取变量位置并删除相应的 DBG_VALUE 指令。在块内执行一些局部复制传播。寄存器分配后,VirtRegRewriter 遍布会将其原始位置重新插入 DBG_VALUE 指令,并将虚拟寄存器引用转换为其物理机器位置。为了避免编码不正确的变量位置,在此遍布中,任何非活动虚拟寄存器的 DBG_VALUE 都将替换为未定义的位置。LiveDebugVariables 可能会由于虚拟寄存器重写而插入冗余的 DBG_VALUE。这些将在随后由 RemoveRedundantDebugValues 遍布删除。
LiveDebugValues 变量位置扩展¶
在所有优化运行完毕且即将发出代码之前,LiveDebugValues 遍布会运行以实现两个目标
通过复制和寄存器溢出传播变量的位置,
对于每个块,记录该块中每个有效的变量位置。
在此遍布之后,DBG_VALUE 指令的含义发生了变化:它不再对应于变量可能更改值的源代码级别赋值,而是断言变量在块中的位置,并且在块外部失去作用。通过复制和溢出传播变量位置非常简单:确定每个基本块中的变量位置需要考虑控制流。考虑以下 IR,它呈现了一些困难
define dso_local i32 @foo(i1 %cond, i32 %input) !dbg !12 {
entry:
br i1 %cond, label %truebr, label %falsebr
bb1:
%value = phi i32 [ %value1, %truebr ], [ %value2, %falsebr ]
br label %exit, !dbg !26
truebr:
#dbg_value(i32 %input, !30, !DIExpression(), !24)
#dbg_value(i32 1, !23, !DIExpression(), !24)
%value1 = add i32 %input, 1
br label %bb1
falsebr:
#dbg_value(i32 %input, !30, !DIExpression(), !24)
#dbg_value(i32 2, !23, !DIExpression(), !24)
%value2 = add i32 %input, 2
br label %bb1
exit:
ret i32 %value, !dbg !30
}
这里的困难在于
控制流大致与基本块顺序相反
!23
变量的值合并到%bb1
中,但没有 PHI 节点
如上所述,#dbg_value
记录本质上形成了嵌入在 IR 中的命令式程序,每个记录定义一个变量位置。这可以通过 mem2reg 转换为 SSA 形式,就像它使用 use-def 链来识别控制流合并并为 IR 值插入 phi 节点一样。但是,由于调试变量位置是为每个机器指令定义的,因此实际上每个 IR 指令都使用每个变量位置,这将导致生成大量调试记录。
检查上面的示例,变量 !30
在函数的两个条件路径上都分配了 %input
,而 !23
在每条路径上都分配了不同的常数值。在 %bb1
中控制流合并的地方,我们希望 !30
保持其位置(%input
),但 !23
变成未定义,因为我们无法在运行时确定它在 %bb1 中应该是什么值,而不插入 PHI 节点。mem2reg 不会插入 PHI 节点以避免在启用调试时更改代码生成,也不会插入其他 #dbg_value 以避免添加大量记录。
相反,LiveDebugValues 在控制流合并时确定变量位置。使用数据流分析在块之间传播位置:当控制流合并时,如果变量在所有前驱体中具有相同的位置,则该位置将传播到后继中。如果前驱体位置不一致,则该位置将变为未定义。
LiveDebugValues 运行完成后,每个块都应该在其块内由 DBG_VALUE 指令描述的所有有效变量位置。支持类(例如 DbgEntityHistoryCalculator)然后只需要很少的努力就可以构建每个指令到每个有效变量位置的映射,而无需考虑控制流。从上面的示例中,否则很难确定变量 !30
的位置应该“向上”流入块 %bb1
,但变量 !23
的位置不应该“向下”流入 %exit
块。
C/C++ 前端特定调试信息¶
C 和 C++ 前端以一种与DWARF在信息内容方面实际上相同的格式表示有关程序的信息。这允许代码生成器通过生成标准的矮人信息来轻松支持本机调试器,并且包含足够的信息供非矮人目标根据需要进行转换。
本节描述用于表示 C 和 C++ 程序的形式。其他语言可以以此为模式(它本身经过调整以与 DWARF 相同的方式表示程序),或者如果它们不适合 DWARF 模型,则可以选择提供完全不同的形式。随着各种 LLVM 源语言前端对调试信息的支持不断增加,此处应记录使用的信息。
以下部分提供了几个 C/C++ 结构的示例以及最能描述这些结构的调试信息。规范参考是在 include/llvm/IR/DebugInfoMetadata.h
中定义的 DINode
类以及 lib/IR/DIBuilder.cpp
中辅助函数的实现。
C/C++ 源文件信息¶
llvm::Instruction
提供对附加到指令的元数据的轻松访问。可以使用 Instruction::getDebugLoc()
和 DILocation::getLine()
提取 LLVM IR 中编码的行号信息。
if (DILocation *Loc = I->getDebugLoc()) { // Here I is an LLVM instruction
unsigned Line = Loc->getLine();
StringRef File = Loc->getFilename();
StringRef Dir = Loc->getDirectory();
bool ImplicitCode = Loc->isImplicitCode();
}
当标志 ImplicitCode 为真时,表示该指令是由前端添加的,但不对应于用户编写的源代码。例如
if (MyBoolean) {
MyObject MO;
...
}
在作用域结束时,会调用 MyObject 的析构函数,但它没有显式编写。此信息有助于在进行代码覆盖时避免对括号进行计数。
C/C++ 全局变量信息¶
给定如下声明的整数全局变量
_Alignas(8) int MyGlobal = 100;
C/C++ 前端将生成以下描述符
;;
;; Define the global itself.
;;
@MyGlobal = global i32 100, align 8, !dbg !0
;;
;; List of debug info of globals
;;
!llvm.dbg.cu = !{!1}
;; Some unrelated metadata.
!llvm.module.flags = !{!6, !7}
!llvm.ident = !{!8}
;; Define the global variable itself
!0 = distinct !DIGlobalVariable(name: "MyGlobal", scope: !1, file: !2, line: 1, type: !5, isLocal: false, isDefinition: true, align: 64)
;; Define the compile unit.
!1 = distinct !DICompileUnit(language: DW_LANG_C99, file: !2,
producer: "clang version 4.0.0",
isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug,
enums: !3, globals: !4)
;;
;; Define the file
;;
!2 = !DIFile(filename: "/dev/stdin",
directory: "/Users/dexonsmith/data/llvm/debug-info")
;; An empty array.
!3 = !{}
;; The Array of Global Variables
!4 = !{!0}
;;
;; Define the type
;;
!5 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
;; Dwarf version to output.
!6 = !{i32 2, !"Dwarf Version", i32 4}
;; Debug info schema version.
!7 = !{i32 2, !"Debug Info Version", i32 3}
;; Compiler identification
!8 = !{!"clang version 4.0.0"}
DIGlobalVariable 描述中的 align 值指定变量对齐方式(如果由 C11 _Alignas()、C++11 alignas() 关键字或编译器属性 __attribute__((aligned ())) 强制)。在其他情况下(当此字段缺失时),对齐方式被视为默认值。这用于为 DW_AT_alignment 值生成 DWARF 输出。
C/C++ 函数信息¶
给定如下声明的函数
int main(int argc, char *argv[]) {
return 0;
}
C/C++ 前端将生成以下描述符
;;
;; Define the anchor for subprograms.
;;
!4 = !DISubprogram(name: "main", scope: !1, file: !1, line: 1, type: !5,
isLocal: false, isDefinition: true, scopeLine: 1,
flags: DIFlagPrototyped, isOptimized: false,
retainedNodes: !2)
;;
;; Define the subprogram itself.
;;
define i32 @main(i32 %argc, i8** %argv) !dbg !4 {
...
}
C++ 特定调试信息¶
C++ 特殊成员函数信息¶
DWARF v5 引入了定义以增强 C++ 程序调试信息的属性。LLVM 可以生成(或省略)这些适当的 DWARF 属性。在 C++ 中,特殊成员函数构造函数、析构函数、复制/移动构造函数、赋值运算符可以用 C++11 关键字 deleted 声明。这在 LLVM 中使用 spFlags 值 DISPFlagDeleted 表示。
给定一个声明为 deleted 的复制构造函数的类声明
class foo {
public:
foo(const foo&) = deleted;
};
C++ 前端将生成以下内容
!17 = !DISubprogram(name: "foo", scope: !11, file: !1, line: 5, type: !18, scopeLine: 5, flags: DIFlagPublic | DIFlagPrototyped, spFlags: DISPFlagDeleted)
这将生成一个额外的 DWARF 属性,如下所示
DW_TAG_subprogram [7] *
DW_AT_name [DW_FORM_strx1] (indexed (00000006) string = "foo")
DW_AT_decl_line [DW_FORM_data1] (5)
...
DW_AT_deleted [DW_FORM_flag_present] (true)
Fortran 特定调试信息¶
Fortran 函数信息¶
定义了一些 DWARF 属性来支持客户端对 Fortran 程序的调试。LLVM 可以为 ELEMENTAL、PURE、IMPURE、RECURSIVE 和 NON_RECURSIVE 的前缀规范生成(或省略)适当的 DWARF 属性。这是通过使用 spFlags 值完成的:DISPFlagElemental、DISPFlagPure 和 DISPFlagRecursive。
elemental function elem_func(a)
Fortran 前端将生成以下描述符
!11 = distinct !DISubprogram(name: "subroutine2", scope: !1, file: !1,
line: 5, type: !8, scopeLine: 6,
spFlags: DISPFlagDefinition | DISPFlagElemental, unit: !0,
retainedNodes: !2)
这将具体化为一个额外的 DWARF 属性,如下所示
DW_TAG_subprogram [3]
DW_AT_low_pc [DW_FORM_addr] (0x0000000000000010 ".text")
DW_AT_high_pc [DW_FORM_data4] (0x00000001)
...
DW_AT_elemental [DW_FORM_flag_present] (true)
定义了一些 DWARF 标记来表示 Fortran 特定结构,即 DW_TAG_string_type 用于表示 Fortran character(n)。在 LLVM 中,这表示为 DIStringType。
character(len=*), intent(in) :: string
Fortran 前端将生成以下描述符
!DILocalVariable(name: "string", arg: 1, scope: !10, file: !3, line: 4, type: !15)
!DIStringType(name: "character(*)!2", stringLength: !16, stringLengthExpression: !DIExpression(), size: 32)
Fortran 延迟长度字符还可以包含字符的原始存储信息以及字符串的长度。此信息编码在 stringLocationExpression 字段中。根据此信息,在 DW_TAG_string_type 调试信息中发出 DW_AT_data_location 属性。
!DIStringType(name: “character(*)!2”, stringLengthExpression: !DIExpression(), stringLocationExpression: !DIExpression(DW_OP_push_object_address, DW_OP_deref), size: 32)
这将具体化为 DWARF 标记,如下所示
DW_TAG_string_type
DW_AT_name ("character(*)!2")
DW_AT_string_length (0x00000064)
0x00000064: DW_TAG_variable
DW_AT_location (DW_OP_fbreg +16)
DW_AT_type (0x00000083 "integer*8")
DW_AT_data_location (DW_OP_push_object_address, DW_OP_deref)
...
DW_AT_artificial (true)
Fortran 前端可能需要生成一个trampoline 函数来调用在不同编译单元中定义的函数。在这种情况下,前端可以为 trampoline 函数发出以下描述符
!DISubprogram(name: "sub1_.t0p", linkageName: "sub1_.t0p", scope: !4, file: !4, type: !5, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition, unit: !7, retainedNodes: !24, targetFuncName: "sub1_")
targetFuncName 字段是 trampoline 调用的函数的名称。此描述符导致以下 DWARF 标记
DW_TAG_subprogram
...
DW_AT_linkage_name ("sub1_.t0p")
DW_AT_name ("sub1_.t0p")
DW_AT_trampoline ("sub1_")
调试信息格式¶
Objective C 属性的调试信息扩展¶
简介¶
Objective C 提供了一种更简单的方法来使用声明的属性声明和定义访问器方法。该语言提供了声明属性并让编译器合成访问器方法的功能。
调试器允许开发人员检查 Objective C 接口及其实例变量和类变量。但是,调试器对在 Objective C 接口中定义的属性一无所知。调试器使用编译器以 DWARF 格式生成的信息。该格式不支持 Objective C 属性的编码。本提案描述了用于编码 Objective C 属性的 DWARF 扩展,调试器可以使用这些扩展让开发人员检查 Objective C 属性。
提案¶
Objective C 属性独立于类成员存在。属性只能由“setter”和“getter”选择器定义,并且可以在每次访问时重新计算。或者属性可以只是对某个声明的 ivar 的直接访问。最后,它可以由编译器为其“自动合成”一个 ivar,在这种情况下,可以使用标准 C 解引用语法以及属性“点”语法在用户代码中直接引用该属性,但在 @interface
声明中没有与该 ivar 对应的条目。
为了便于调试,这些属性我们将一个新的 DWARF TAG 添加到类的 DW_TAG_structure_type
定义中以保存给定属性的描述,以及提供该描述的一组 DWARF 属性。属性标记还将包含属性的名称和声明类型。
如果存在相关的实例变量(ivar),那么在该实例变量的 DW_TAG_member
DIE 中也会放置一个 DWARF 属性,该属性会引用回该属性的属性 TAG。并且,在编译器直接合成实例变量的情况下,编译器预计会为该实例变量生成一个 DW_TAG_member
(其中 DW_AT_artificial
设置为 1),其名称将是在代码中直接访问此实例变量所使用的名称,并且该属性将指向其所支持的属性。
以下示例将作为我们讨论的说明。
@interface I1 {
int n2;
}
@property int p1;
@property int p2;
@end
@implementation I1
@synthesize p1;
@synthesize p2 = n2;
@end
这会生成以下 DWARF(这是“伪 dwarfdump”输出)。
0x00000100: TAG_structure_type [7] *
AT_APPLE_runtime_class( 0x10 )
AT_name( "I1" )
AT_decl_file( "Objc_Property.m" )
AT_decl_line( 3 )
0x00000110 TAG_APPLE_property
AT_name ( "p1" )
AT_type ( {0x00000150} ( int ) )
0x00000120: TAG_APPLE_property
AT_name ( "p2" )
AT_type ( {0x00000150} ( int ) )
0x00000130: TAG_member [8]
AT_name( "_p1" )
AT_APPLE_property ( {0x00000110} "p1" )
AT_type( {0x00000150} ( int ) )
AT_artificial ( 0x1 )
0x00000140: TAG_member [8]
AT_name( "n2" )
AT_APPLE_property ( {0x00000120} "p2" )
AT_type( {0x00000150} ( int ) )
0x00000150: AT_type( ( int ) )
请注意,当前约定是,自动合成的属性的实例变量的名称是从其派生的属性的名称,并在前面加上下划线,如示例所示。但实际上我们不需要知道此约定,因为我们直接获得了实例变量的名称。
此外,在 ObjC 中,通常的做法是在 @interface 和 @implementation 中使用不同的属性声明 - 例如,在接口中提供只读属性,在实现中提供读写接口。在这种情况下,编译器应发出当前翻译单元中有效的任何属性声明。
开发人员可以使用属性装饰属性,这些属性使用 DW_AT_APPLE_property_attribute
编码。
@property (readonly, nonatomic) int pr;
TAG_APPLE_property [8]
AT_name( "pr" )
AT_type ( {0x00000147} (int) )
AT_APPLE_property_attribute (DW_APPLE_PROPERTY_readonly, DW_APPLE_PROPERTY_nonatomic)
setter 和 getter 方法名称使用 DW_AT_APPLE_property_setter
和 DW_AT_APPLE_property_getter
属性附加到属性。
@interface I1
@property (setter=myOwnP3Setter:) int p3;
-(void)myOwnP3Setter:(int)a;
@end
@implementation I1
@synthesize p3;
-(void)myOwnP3Setter:(int)a{ }
@end
此处的 DWARF 将是
0x000003bd: TAG_structure_type [7] *
AT_APPLE_runtime_class( 0x10 )
AT_name( "I1" )
AT_decl_file( "Objc_Property.m" )
AT_decl_line( 3 )
0x000003cd TAG_APPLE_property
AT_name ( "p3" )
AT_APPLE_property_setter ( "myOwnP3Setter:" )
AT_type( {0x00000147} ( int ) )
0x000003f3: TAG_member [8]
AT_name( "_p3" )
AT_type ( {0x00000147} ( int ) )
AT_APPLE_property ( {0x000003cd} )
AT_artificial ( 0x1 )
新的 DWARF 属性¶
属性 |
值 |
类 |
---|---|---|
DW_AT_APPLE_property |
0x3fed |
参考 |
DW_AT_APPLE_property_getter |
0x3fe9 |
字符串 |
DW_AT_APPLE_property_setter |
0x3fea |
字符串 |
DW_AT_APPLE_property_attribute |
0x3feb |
常量 |
新的 DWARF 常量¶
名称 |
值 |
---|---|
DW_APPLE_PROPERTY_readonly |
0x01 |
DW_APPLE_PROPERTY_getter |
0x02 |
DW_APPLE_PROPERTY_assign |
0x04 |
DW_APPLE_PROPERTY_readwrite |
0x08 |
DW_APPLE_PROPERTY_retain |
0x10 |
DW_APPLE_PROPERTY_copy |
0x20 |
DW_APPLE_PROPERTY_nonatomic |
0x40 |
DW_APPLE_PROPERTY_setter |
0x80 |
DW_APPLE_PROPERTY_atomic |
0x100 |
DW_APPLE_PROPERTY_weak |
0x200 |
DW_APPLE_PROPERTY_strong |
0x400 |
DW_APPLE_PROPERTY_unsafe_unretained |
0x800 |
DW_APPLE_PROPERTY_nullability |
0x1000 |
DW_APPLE_PROPERTY_null_resettable |
0x2000 |
DW_APPLE_PROPERTY_class |
0x4000 |
名称加速表¶
简介¶
“.debug_pubnames” 和 “.debug_pubtypes” 格式并非调试器所需。节名称中的“pub”表示表中的条目仅为公开可见的名称。这意味着“。debug_pubnames”中不会显示任何静态或隐藏函数。 “。debug_pubtypes”中没有静态变量或私有类变量。许多编译器向这些表中添加不同的内容,因此我们不能依赖于 gcc、icc 或 clang 之间的内容。
用户给出的典型查询往往与这些表的内容不匹配。例如,DWARF 规范指出:“在 C++ 结构、类或联合的函数成员或静态数据成员的名称的情况下,“。debug_pubnames”节中显示的名称不是引用的调试信息条目的 DW_AT_name attribute
给出的简单名称,而是数据或函数成员的完全限定名称。”因此,对于复杂的 C++ 条目,这些表中唯一的名称是完全限定名称。调试器用户往往不会将他们的搜索字符串输入为“a::b::c(int,const Foo&) const
”,而是输入为“c
”、“b::c
” 或“a::b::c
”。因此,必须对名称表中输入的名称进行反混淆,以便适当地将其分割,并且必须手动将其他名称输入到表中,以使其成为调试器可用于名称查找的有效表。
所有调试器目前都忽略“。debug_pubnames”表,因为其不一致且无用的仅公开名称内容使其成为对象文件中浪费空间的内容。这些表在写入磁盘时不会以任何方式排序,导致每个调试器都必须执行自己的解析和排序。这些表还包含表本身中字符串值的内联副本,这使得这些表在磁盘上的大小比必要的大得多,尤其是在大型 C++ 程序中。
我们能否通过向此表中添加所有需要的名称来修复这些部分?不可以,因为这不是这些表的定义包含的内容,我们也不知道旧的错误表和新的正确表之间的区别。充其量,我们可以创建自己的重命名部分,其中包含我们所需的所有数据。
这些表对于像 LLDB 这样的调试器所需的功能也不够。LLDB 使用 clang 进行表达式解析,其中 LLDB 充当 PCH。然后,经常会要求 LLDB 查找类型“foo
”或命名空间“bar
”,或列出命名空间“baz
”中的项目。命名空间不包含在 pubnames 或 pubtypes 表中。由于 clang 在解析表达式时会提出很多问题,因此我们需要在查找名称时非常快,因为这种情况经常发生。拥有针对非常快速的查找进行优化的新的加速表将极大地有利于这种类型的调试体验。
我们希望生成可以从磁盘映射到内存并按原样使用,并且几乎无需预先解析的名称查找表。我们还能够控制这些不同表的精确内容,以便它们只包含我们所需的内容。名称加速表旨在解决这些问题。为了解决这些问题,我们需要
具有可以从磁盘映射到内存并按原样使用的格式。
查找应该非常快。
可扩展的表格式,以便许多生产者可以创建这些表。
包含开箱即用进行典型查找所需的所有名称。
表的严格内容规则。
表的大小很重要,并且加速表格式应允许重用来自公共字符串表的字符串,以便不会重复名称的字符串。我们还希望确保只需将表映射到内存并进行最少的标头解析即可按原样使用该表。
名称查找需要快速,并且针对调试器倾向于执行的查找类型进行优化。理想情况下,我们希望在执行名称查找时尽可能少地访问映射表的某些部分,并且能够快速找到我们正在查找的名称条目,或者发现没有匹配项。在调试器的情况下,我们优化了大多数情况下失败的查找。
每个定义的表都应具有关于加速表中确切内容的严格规则,并进行记录,以便客户端可以依赖其内容。
哈希表¶
标准哈希表¶
典型的哈希表具有标头、桶,每个桶都指向桶内容。
.------------.
| HEADER |
|------------|
| BUCKETS |
|------------|
| DATA |
`------------'
桶是每个哈希数据的偏移量数组。
.------------.
| 0x00001000 | BUCKETS[0]
| 0x00002000 | BUCKETS[1]
| 0x00002200 | BUCKETS[2]
| 0x000034f0 | BUCKETS[3]
| | ...
| 0xXXXXXXXX | BUCKETS[n_buckets]
'------------'
因此,对于上面示例中的 bucket[3]
,我们有一个指向表的偏移量 0x000034f0,该偏移量指向桶的条目链。每个桶都必须包含一个下一个指针、完整的 32 位哈希值、字符串本身以及当前字符串值的 数据。
.------------.
0x000034f0: | 0x00003500 | next pointer
| 0x12345678 | 32 bit hash
| "erase" | string value
| data[n] | HashData for this bucket
|------------|
0x00003500: | 0x00003550 | next pointer
| 0x29273623 | 32 bit hash
| "dump" | string value
| data[n] | HashData for this bucket
|------------|
0x00003550: | 0x00000000 | next pointer
| 0x82638293 | 32 bit hash
| "main" | string value
| data[n] | HashData for this bucket
`------------'
对于调试器而言,此布局的问题在于我们需要优化负查找情况,在这种情况下,我们正在搜索的符号不存在。因此,如果我们要在上面的表中查找“printf
”,我们将为“printf
”创建一个 32 位哈希值,它可能与 bucket[3]
匹配。我们需要转到偏移量 0x000034f0 并开始查找我们的 32 位哈希值是否匹配。为此,我们需要读取下一个指针,然后读取哈希值,进行比较,并跳到下一个桶。每次我们都在内存中跳过许多字节并接触新的页面,只是为了对完整的 32 位哈希值进行比较。所有这些访问然后告诉我们我们没有匹配项。
名称哈希表¶
为了解决上面提到的问题,我们对哈希表的结构进行了稍微不同的调整:标头、桶、所有唯一 32 位哈希值的数组,然后是每个哈希值一个的哈希值数据偏移量数组,然后是所有哈希值的数据。
.-------------.
| HEADER |
|-------------|
| BUCKETS |
|-------------|
| HASHES |
|-------------|
| OFFSETS |
|-------------|
| DATA |
`-------------'
名称表中的 BUCKETS
是 HASHES
数组的索引。通过使所有 32 位哈希值在内存中连续,我们允许自己在接触尽可能少的内存的情况下有效地检查匹配项。最常见的是,检查 32 位哈希值就是查找的范围。如果确实匹配,则通常是匹配,没有冲突。因此,对于具有“n_buckets
”个桶和“n_hashes
”个唯一 32 位哈希值的表,我们可以阐明 BUCKETS
、HASHES
和 OFFSETS
的内容如下:
.-------------------------.
| HEADER.magic | uint32_t
| HEADER.version | uint16_t
| HEADER.hash_function | uint16_t
| HEADER.bucket_count | uint32_t
| HEADER.hashes_count | uint32_t
| HEADER.header_data_len | uint32_t
| HEADER_DATA | HeaderData
|-------------------------|
| BUCKETS | uint32_t[n_buckets] // 32 bit hash indexes
|-------------------------|
| HASHES | uint32_t[n_hashes] // 32 bit hash values
|-------------------------|
| OFFSETS | uint32_t[n_hashes] // 32 bit offsets to hash value data
|-------------------------|
| ALL HASH DATA |
`-------------------------'
因此,使用上面标准哈希示例中的完全相同的数据,我们最终得到
.------------.
| HEADER |
|------------|
| 0 | BUCKETS[0]
| 2 | BUCKETS[1]
| 5 | BUCKETS[2]
| 6 | BUCKETS[3]
| | ...
| ... | BUCKETS[n_buckets]
|------------|
| 0x........ | HASHES[0]
| 0x........ | HASHES[1]
| 0x........ | HASHES[2]
| 0x........ | HASHES[3]
| 0x........ | HASHES[4]
| 0x........ | HASHES[5]
| 0x12345678 | HASHES[6] hash for BUCKETS[3]
| 0x29273623 | HASHES[7] hash for BUCKETS[3]
| 0x82638293 | HASHES[8] hash for BUCKETS[3]
| 0x........ | HASHES[9]
| 0x........ | HASHES[10]
| 0x........ | HASHES[11]
| 0x........ | HASHES[12]
| 0x........ | HASHES[13]
| 0x........ | HASHES[n_hashes]
|------------|
| 0x........ | OFFSETS[0]
| 0x........ | OFFSETS[1]
| 0x........ | OFFSETS[2]
| 0x........ | OFFSETS[3]
| 0x........ | OFFSETS[4]
| 0x........ | OFFSETS[5]
| 0x000034f0 | OFFSETS[6] offset for BUCKETS[3]
| 0x00003500 | OFFSETS[7] offset for BUCKETS[3]
| 0x00003550 | OFFSETS[8] offset for BUCKETS[3]
| 0x........ | OFFSETS[9]
| 0x........ | OFFSETS[10]
| 0x........ | OFFSETS[11]
| 0x........ | OFFSETS[12]
| 0x........ | OFFSETS[13]
| 0x........ | OFFSETS[n_hashes]
|------------|
| |
| |
| |
| |
| |
|------------|
0x000034f0: | 0x00001203 | .debug_str ("erase")
| 0x00000004 | A 32 bit array count - number of HashData with name "erase"
| 0x........ | HashData[0]
| 0x........ | HashData[1]
| 0x........ | HashData[2]
| 0x........ | HashData[3]
| 0x00000000 | String offset into .debug_str (terminate data for hash)
|------------|
0x00003500: | 0x00001203 | String offset into .debug_str ("collision")
| 0x00000002 | A 32 bit array count - number of HashData with name "collision"
| 0x........ | HashData[0]
| 0x........ | HashData[1]
| 0x00001203 | String offset into .debug_str ("dump")
| 0x00000003 | A 32 bit array count - number of HashData with name "dump"
| 0x........ | HashData[0]
| 0x........ | HashData[1]
| 0x........ | HashData[2]
| 0x00000000 | String offset into .debug_str (terminate data for hash)
|------------|
0x00003550: | 0x00001203 | String offset into .debug_str ("main")
| 0x00000009 | A 32 bit array count - number of HashData with name "main"
| 0x........ | HashData[0]
| 0x........ | HashData[1]
| 0x........ | HashData[2]
| 0x........ | HashData[3]
| 0x........ | HashData[4]
| 0x........ | HashData[5]
| 0x........ | HashData[6]
| 0x........ | HashData[7]
| 0x........ | HashData[8]
| 0x00000000 | String offset into .debug_str (terminate data for hash)
`------------'
所以我们仍然拥有所有相同的数据,只是为了调试器查找更有效地组织了它们。如果我们重复上面相同的“printf
”查找,我们将对“printf
”进行哈希,并发现它通过获取 32 位哈希值并将其模 n_buckets
匹配 BUCKETS[3]
。 BUCKETS[3]
包含“6”,它是 HASHES
表中的索引。然后,只要哈希位于 BUCKETS[3]
中,我们就会比较 HASHES
数组中任何连续的 32 位哈希值。我们通过验证每个后续哈希值模 n_buckets
是否仍然为 3 来做到这一点。在查找失败的情况下,我们将访问 BUCKETS[3]
的内存,然后比较几个连续的 32 位哈希值,然后再确定我们没有匹配项。我们最终不会遍历多个内存字,并且确实将被访问的处理器数据缓存行数量保持在尽可能小的范围内。
用于这些查找表的字符串哈希是 Daniel J. Bernstein 哈希,它也用于 ELF GNU_HASH
部分。对于程序中各种名称,它是一个非常好的哈希,哈希冲突很少。
空桶通过使用无效的哈希索引 UINT32_MAX
来指定。
详情¶
这些名称哈希表旨在通用,其中表的专门化可以定义添加到标题中的其他数据(“HeaderData
”)、如何存储字符串值(“KeyType
”)以及每个哈希值的数据内容。
标题布局¶
标题具有固定部分和专门化部分。标题的确切格式为
struct Header
{
uint32_t magic; // 'HASH' magic value to allow endian detection
uint16_t version; // Version number
uint16_t hash_function; // The hash function enumeration that was used
uint32_t bucket_count; // The number of buckets in this hash table
uint32_t hashes_count; // The total number of unique hash values and hash data offsets in this table
uint32_t header_data_len; // The bytes to skip to get to the hash indexes (buckets) for correct alignment
// Specifically the length of the following HeaderData field - this does not
// include the size of the preceding fields
HeaderData header_data; // Implementation specific header data
};
标题以 32 位“magic
”值开头,该值必须为 'HASH'
,编码为 ASCII 整数。这允许检测哈希表的开始,并允许确定表的字节顺序,以便可以正确提取表。“magic
”值后面是一个 16 位 version
号,允许将来修改和修改表。当前版本号为 1。hash_function
是一个 uint16_t
枚举,指定用于生成此表的哈希函数。哈希函数枚举的当前值包括
enum HashFunctionType
{
eHashFunctionDJB = 0u, // Daniel J Bernstein hash function
};
bucket_count
是一个 32 位无符号整数,表示 BUCKETS
数组中有多少个桶。hashes_count
是 HASHES
数组中包含的唯一 32 位哈希值的个数,并且与 OFFSETS
数组中包含的偏移量个数相同。header_data_len
指定由此表的专门版本填充的 HeaderData
的大小(以字节为单位)。
固定查找¶
标题后面是桶、哈希、偏移量和哈希值数据。
struct FixedTable
{
uint32_t buckets[Header.bucket_count]; // An array of hash indexes into the "hashes[]" array below
uint32_t hashes [Header.hashes_count]; // Every unique 32 bit hash for the entire table is in this table
uint32_t offsets[Header.hashes_count]; // An offset that corresponds to each item in the "hashes[]" array above
};
buckets
是一个 32 位索引数组,指向 hashes
数组。 hashes
数组包含哈希表中所有名称的所有 32 位哈希值。 hashes
表中的每个哈希在 offsets
数组中都有一个偏移量,该偏移量指向哈希值的数据。
此表设置使得可以非常轻松地重新利用这些表以包含不同的数据,同时保持所有表的查找机制相同。此布局还使将表保存到磁盘并在以后映射它并进行非常高效的名称查找成为可能,而无需或几乎无需解析。
DWARF 查找表可以用多种方式实现,并且可以为每个名称存储大量信息。我们希望使 DWARF 表可扩展并能够有效地存储数据,因此我们使用了一些 DWARF 功能来实现高效的数据存储,以准确定义我们为每个名称存储的数据类型。
HeaderData
包含每个 HashData 块内容的定义。我们可能希望将所有调试信息条目 (DIE) 的偏移量存储到每个名称中。为了保持可扩展性,我们创建了一个包含每个名称数据中包含的项目或原子的列表。首先是每个原子中数据的类型
enum AtomType
{
eAtomTypeNULL = 0u,
eAtomTypeDIEOffset = 1u, // DIE offset, check form for encoding
eAtomTypeCUOffset = 2u, // DIE offset of the compiler unit header that contains the item in question
eAtomTypeTag = 3u, // DW_TAG_xxx value, should be encoded as DW_FORM_data1 (if no tags exceed 255) or DW_FORM_data2
eAtomTypeNameFlags = 4u, // Flags from enum NameFlags
eAtomTypeTypeFlags = 5u, // Flags from enum TypeFlags
};
枚举值及其含义为
eAtomTypeNULL - a termination atom that specifies the end of the atom list
eAtomTypeDIEOffset - an offset into the .debug_info section for the DWARF DIE for this name
eAtomTypeCUOffset - an offset into the .debug_info section for the CU that contains the DIE
eAtomTypeDIETag - The DW_TAG_XXX enumeration value so you don't have to parse the DWARF to see what it is
eAtomTypeNameFlags - Flags for functions and global variables (isFunction, isInlined, isExternal...)
eAtomTypeTypeFlags - Flags for types (isCXXClass, isObjCClass, ...)
然后我们允许每个原子类型定义原子类型以及如何对每个原子类型数据进行编码
struct Atom
{
uint16_t type; // AtomType enum value
uint16_t form; // DWARF DW_FORM_XXX defines
};
上面的 form
类型来自 DWARF 规范,并定义了原子类型的数据的精确编码。有关 DW_FORM_
定义,请参阅 DWARF 规范。
struct HeaderData
{
uint32_t die_offset_base;
uint32_t atom_count;
Atoms atoms[atom_count0];
};
HeaderData
定义了应添加到使用 DW_FORM_ref1
、DW_FORM_ref2
、DW_FORM_ref4
、DW_FORM_ref8
或 DW_FORM_ref_udata
编码的任何原子的基本 DIE 偏移量。它还定义了每个 HashData
对象中包含的内容 - Atom.form
告诉我们每个字段在 HashData
中的大小,而 Atom.type
告诉我们如何解释此数据。
对于“.apple_names
”(所有函数 + 全局变量),“.apple_types
”(所有定义类型的名称)和“.apple_namespaces
”(所有命名空间)的当前实现,我们目前将 Atom
数组设置为
HeaderData.atom_count = 1;
HeaderData.atoms[0].type = eAtomTypeDIEOffset;
HeaderData.atoms[0].form = DW_FORM_data4;
这将内容定义为作为 32 位值(DW_FORM_data4)编码的 DIE 偏移量 (eAtomTypeDIEOffset)。这允许单个名称在一个文件中具有多个匹配的 DIE,例如内联函数可能会出现这种情况。未来的表可能会包含有关 DIE 的更多信息,例如指示 DIE 是否为函数、方法、块或内联的标志。
DWARF 表的 KeyType 是指向“.debug_str”表的 32 位字符串表偏移量。“.debug_str”是 DWARF 的字符串表,其中可能已经包含所有字符串的副本。这有助于确保在编译器的帮助下,我们在所有 DWARF 部分之间重用字符串,并使哈希表的大小保持较小。编译器将所有字符串作为 DW_FORM_strp 生成到调试信息中的另一个好处是,可以使 DWARF 解析速度更快。
进行查找后,我们得到一个指向哈希数据的偏移量。哈希数据需要能够处理 32 位哈希冲突,因此哈希数据中偏移量处的数据块由三元组组成
uint32_t str_offset
uint32_t hash_data_count
HashData[hash_data_count]
如果“str_offset”为零,则桶内容已完成。99.9% 的哈希数据块包含单个项目(没有 32 位哈希冲突)
.------------.
| 0x00001023 | uint32_t KeyType (.debug_str[0x0001023] => "main")
| 0x00000004 | uint32_t HashData count
| 0x........ | uint32_t HashData[0] DIE offset
| 0x........ | uint32_t HashData[1] DIE offset
| 0x........ | uint32_t HashData[2] DIE offset
| 0x........ | uint32_t HashData[3] DIE offset
| 0x00000000 | uint32_t KeyType (end of hash chain)
`------------'
如果存在冲突,您将拥有多个有效的字符串偏移量
.------------.
| 0x00001023 | uint32_t KeyType (.debug_str[0x0001023] => "main")
| 0x00000004 | uint32_t HashData count
| 0x........ | uint32_t HashData[0] DIE offset
| 0x........ | uint32_t HashData[1] DIE offset
| 0x........ | uint32_t HashData[2] DIE offset
| 0x........ | uint32_t HashData[3] DIE offset
| 0x00002023 | uint32_t KeyType (.debug_str[0x0002023] => "print")
| 0x00000002 | uint32_t HashData count
| 0x........ | uint32_t HashData[0] DIE offset
| 0x........ | uint32_t HashData[1] DIE offset
| 0x00000000 | uint32_t KeyType (end of hash chain)
`------------'
使用真实世界 C++ 二进制文件的当前测试表明,每 100,000 个名称条目大约有 1 个 32 位哈希冲突。
内容¶
正如我们所说,我们希望严格定义不同表中包含的内容。对于 DWARF,我们有 3 个表:“.apple_names
”、“.apple_types
”和“.apple_namespaces
”。
“.apple_names
”部分应为每个 DWARF DIE 包含一个条目,其 DW_TAG
为 DW_TAG_label
、DW_TAG_inlined_subroutine
或 DW_TAG_subprogram
,并且具有地址属性:DW_AT_low_pc
、DW_AT_high_pc
、DW_AT_ranges
或 DW_AT_entry_pc
。它还包含在位置(全局变量和静态变量)中具有 DW_OP_addr
的 DW_TAG_variable
DIE。应包含所有全局变量和静态变量,包括函数和类中作用域内的变量。例如,使用以下代码
static int var = 0;
void f ()
{
static int var = 0;
}
两个静态 var
变量都将包含在表中。所有函数都应发出其完整名称和基本名称。对于 C 或 C++,完整名称是已损坏的名称(如果可用),通常位于 DW_AT_MIPS_linkage_name
属性中,而 DW_AT_name
包含函数基本名称。如果全局变量或静态变量在 DW_AT_MIPS_linkage_name
属性中具有已损坏的名称,则应将其与在 DW_AT_name
属性中找到的简单名称一起发出。
“.apple_types
”部分应为每个 DWARF DIE 包含一个条目,其标记为以下之一:
DW_TAG_array_type
DW_TAG_class_type
DW_TAG_enumeration_type
DW_TAG_pointer_type
DW_TAG_reference_type
DW_TAG_string_type
DW_TAG_structure_type
DW_TAG_subroutine_type
DW_TAG_typedef
DW_TAG_union_type
DW_TAG_ptr_to_member_type
DW_TAG_set_type
DW_TAG_subrange_type
DW_TAG_base_type
DW_TAG_const_type
DW_TAG_immutable_type
DW_TAG_file_type
DW_TAG_namelist
DW_TAG_packed_type
DW_TAG_volatile_type
DW_TAG_restrict_type
DW_TAG_atomic_type
DW_TAG_interface_type
DW_TAG_unspecified_type
DW_TAG_shared_type
仅包含具有DW_AT_name
属性的条目,并且条目不能是前向声明(DW_AT_declaration
属性的值不为零)。例如,使用以下代码
int main ()
{
int *b = 0;
return *b;
}
我们得到一些类型 DIE
0x00000067: TAG_base_type [5]
AT_encoding( DW_ATE_signed )
AT_name( "int" )
AT_byte_size( 0x04 )
0x0000006e: TAG_pointer_type [6]
AT_type( {0x00000067} ( int ) )
AT_byte_size( 0x08 )
DW_TAG_pointer_type 未包含在内,因为它没有 DW_AT_name
。
“.apple_namespaces
” 部分应包含所有 DW_TAG_namespace
DIE。如果我们遇到一个没有名称的命名空间,则这是一个匿名命名空间,名称应输出为“(anonymous namespace)
”(不带引号)。为什么?这与标准 C++ 库中用于反解 mangled 名称的 abi::cxa_demangle()
的输出相匹配。
语言扩展和文件格式更改¶
Objective-C 扩展¶
“.apple_objc
” 部分应包含 Objective-C 类的所有 DW_TAG_subprogram
DIE。哈希表中使用的名称是 Objective-C 类本身的名称。如果 Objective-C 类具有类别,则会为不带类别的类名和带类别的类名创建条目。因此,如果我们在偏移量 0x1234 处有一个 DIE,其方法名为“-[NSString(my_additions) stringWithSpecialString:]
”,我们将为“NSString
” 添加一个指向 DIE 0x1234 的条目,并为“NSString(my_additions)
” 添加一个指向 0x1234 的条目。这使我们能够在执行表达式时快速跟踪 Objective-C 类的所有 Objective-C 方法。这是因为 Objective-C 的动态特性,任何人都可以向类添加方法。Objective-C 方法的 DWARF 也与 C++ 类不同地发出,其中方法通常不包含在类定义中,它们分散在一个或多个编译单元中。类别也可以在不同的共享库中定义。因此,我们需要能够根据 Objective-C 类名快速找到所有方法和类函数,或者快速找到类 + 类别名称的所有方法和类函数。此表不包含任何选择器名称,它只是将 Objective-C 类名(或类名 + 类别)映射到所有方法和类函数。“.debug_names
” 部分中添加了选择器作为函数基本名称。
在 Objective-C 函数的“.apple_names
” 部分中,完整名称是带括号的整个函数名称(“-[NSString stringWithCString:]
”),基本名称仅为选择器(“stringWithCString:
”)。
Mach-O 更改¶
apple 哈希表的节名称适用于非 mach-o 文件。对于 mach-o 文件,节应包含在 __DWARF
段中,名称如下
“
.apple_names
” -> “__apple_names
”“
.apple_types
” -> “__apple_types
”“
.apple_namespaces
” -> “__apple_namespac
”(16 个字符限制)“
.apple_objc
” -> “__apple_objc
”
CodeView 调试信息格式¶
LLVM 支持发出 CodeView(Microsoft 调试信息格式),本节描述了该支持的设计和实现。
格式背景¶
CodeView 作为一种格式显然面向 C++ 调试,在 C++ 中,大多数调试信息往往是类型信息。因此,CodeView 的首要设计约束是将类型信息与其他“符号”信息分离,以便能够跨翻译单元有效地合并类型信息。类型信息和符号信息通常都存储为一系列记录,其中每个记录以 16 位记录大小和 16 位记录种类开头。
类型信息通常存储在目标文件的 .debug$T
部分中。所有其他调试信息,例如行信息、字符串表、符号信息和内联信息,都存储在一个或多个 .debug$S
部分中。每个目标文件可能只有一个 .debug$T
部分,因为所有其他调试信息都引用它。如果在编译期间使用了 PDB(通过 /Zi
MSVC 选项启用),则 .debug$T
部分将仅包含一个指向 PDB 的 LF_TYPESERVER2
记录。当使用 PDB 时,符号信息似乎保留在目标文件的 .debug$S
部分中。
类型记录通过其索引引用,该索引是在给定记录之前的流中的记录数加上 0x1000
。许多常见的基本类型,例如基本整型类型和指向它们的未限定指针,使用小于 0x1000
的类型索引表示。此类基本类型内置于 CodeView 使用者中,不需要类型记录。
每个类型记录只能包含小于其自身类型索引的类型索引。这确保类型流引用的图形是无环的。虽然源级类型图形可能包含通过指针类型的循环(考虑一个链接列表结构),但这些循环通过始终引用用户定义记录类型的转发声明记录从类型流中删除。只有 .debug$S
流中的“符号”记录才能引用完整(非转发声明)类型记录。
使用 CodeView¶
这些是为致力于改进 LLVM 的 CodeView 支持的开发人员提供的一些常见任务的说明。其中大部分都围绕着使用嵌入在 llvm-readobj
中的 CodeView 转储器。
测试 MSVC 的输出
$ cl -c -Z7 foo.cpp # Use /Z7 to keep types in the object file $ llvm-readobj --codeview foo.obj
从 Clang 获取 LLVM IR 调试信息
$ clang -g -gcodeview --target=x86_64-windows-msvc foo.cpp -S -emit-llvm
使用它为 LLVM 测试用例生成 LLVM IR。
从 LLVM IR 元数据生成并转储 CodeView
$ llc foo.ll -filetype=obj -o foo.obj $ llvm-readobj --codeview foo.obj > foo.txt
在 lit 测试用例中使用此模式,并对 llvm-readobj 的输出进行 FileCheck。
改进 LLVM 的 CodeView 支持是一个查找有趣的类型记录、构建使 MSVC 发出这些记录的 C++ 测试用例、转储记录、理解它们,然后在 LLVM 后端生成等效记录的过程。