[译]LLVM的源代码调试

原文地址: http://llvm.org/docs/SourceLevelDebugging.html#llvm-dbg-declare

引子

本文是与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优化以感知调试信息,允许它们在执行积极优化时更新调试信息。这意味着,通过努力,LLVM优化器可以像优化非调试代码那样好地优化调试代码。

· LLVM调试信息不妨碍优化(例如,内联,基本块重排/合并/清除,尾调用复制等)。

· 使用现有设施,LLVM调试信息连同程序的余下部分被自动优化。例如,重复信息被链接器自动合并,未使用的信息被自动删除。

基本上,调试信息允许你以“-O0 -g”编译程序,获得完整的调试信息,使你在使用一个调试器运行它时,能任意修改这个程序。以“-O3 -g”编译程序,给予你总是可用且对读操作准确(accurate for reading)的完整调试信息(即尽管尾调用消除以及内联,你得到准确的栈追踪),不过你会失去修改程序以及调用被优化掉或完全内联的函数的能力。

LLVM测试集
提供了一个测试优化器处理调试信息的框架。它可以像这样运行:

% cd
llvm/projects/test-suite/MultiSource/Benchmarks
# or some other level

%make TEST
=
dbgopt

这将测试调试信息对优化遍的影响。如果调试信息影响了优化遍,那么它将被报告为失败。关于LLVM测试基础设施及如何运行各种测试的更多信息,参考 LLVM测试基础设施指引

调试信息格式

LLVM调试信息被仔细设计,使优化器无需了解调试信息,就能优化程序及调试信息。特别的,元数据的使用从一开始避免了调试信息的重复,全局死代码消除遍如果决定删除一个函数,它会自动删除这个函数的调试信息。

为此,语言前端以LLVM元数据的形式插入大多数调试信息(类型、变量、函数、源文件等的描述符)。

调试信息被设计为不知道目标机器调试器及调试信息的表示(即DWARF/Stabs等)。它使用一个通用遍解码代表变量、类型、函数、名字空间等的信息:这允许使用任意源语言语义及类型系统,只要对目标机器调试器存在一个解释这个信息的模块。

为了提供基本的功能,LLVM调试器确实对要调试的源语言做了一些假定,虽然它将假定保持尽可能少。LLVM调试器假定存在且仅有的通用特征是 源文件
以及 程序对象
。这些抽象对象被调试器用来构成栈追踪,显示局部变量信息等等。

本节首先描述对所有源语言都相同的表示方面。C/C++前端特定调试信息描述了C及C++前端使用的数据布局约定。

调试信息描述符是 特化的元数据节点
,Metadata的第一类子类(first-class subclass)。

调试器固有函数

LLVM使用几个固有函数(名字以“llvm.dbg”开头)在生成代码各处提供调试信息。


llvm.dbg.declare

void
@llvm.dbg.declare
( metadata
, metadata
, metadata
)

这个固有函数提供一个局部元素(即变量)的信息。第一个实参是保存该变量alloca的元数据。第二个实参是包含该变量一个描述的 局部变量
。第三个实参是一个 复杂表达式


llvm.dbg.value

void
@llvm.dbg.value
( metadata
,
i64

, metadata
, metadata
)

当设置一个用户源变量为新的值时,这个固有函数提供信息。第一个实参是新值(封装为元数据)。第二个实参是用户源变量中写入新值的偏移。第三个实参是包含该变量一个描述的 局部变量
。第四个实参是一个 复杂表达式

对象生命期与
作用域

在许多语言里,函数中局部变量的生命期与作用域局限在函数的一个子集。例如,在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

definevoid @foo() #0 !dbg !4 {

entry:

%X = alloca i32, align 4

%Y = alloca i32, align 4

%Z = alloca i32, align 4

call void @llvm.dbg.declare(metadata i32* %X,metadata !11, metadata !13), !dbg !14

store i32 21, i32* %X, align 4, !dbg !14

call void @llvm.dbg.declare(metadata i32* %Y,metadata !15, metadata !13), !dbg !16

store i32 22, i32* %Y, align 4, !dbg !16

call void @llvm.dbg.declare(metadata i32* %Z,metadata !17, metadata !13), !dbg !19

store i32 23, i32* %Z, align 4, !dbg !19

%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

}

;Function Attrs: nounwind readnone

declarevoid @llvm.dbg.declare(metadata, metadata, metadata) #1

attributes#0 = { nounwind ssp uwtable “less-precise-fpmad”=”false””no-frame-pointer-elim”=”true””no-frame-pointer-elim-non-leaf””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 version3.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,variables: !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= !DIExpression()

!14= !DILocation(line: 2, column: 9, scope: !4)

!15= !DILocalVariable(name: “Y”, scope: !4, file: !1, line: 3, type:!12)

!16= !DILocation(line: 3, column: 9, scope: !4)

!17= !DILocalVariable(name: “Z”, scope: !18, file: !1, line: 5, type:!12)

!18= distinct !DILexicalBlock(scope: !4, file: !1, line: 4, column: 5)

!19= !DILocation(line: 5, column: 11, scope: !18)

!20= !DILocation(line: 6, column: 11, scope: !18)

!21= !DILocation(line: 6, column: 9, scope: !18)

!22= !DILocation(line: 8, column: 9, scope: !4)

!23= !DILocation(line: 8, column: 7, scope: !4)

!24= !DILocation(line: 9, column: 3, scope: !4)

这个例子展示了LLVM调试信息的几个重要细节。特别的,它展示了如何应用附加在一条指令的固有函数llvm.dbg.declare及位置信息,使调试器能分析语句、变量定义以及函数实现代码之间的关系。

call
void
@llvm.dbg.declare
( metadata

i32

* %X
, metadata
!11
, metadata
!13
), !dbg
!14

;[debug line = 2:7] [debug variable = X]

第一个固有函数%llvm.dbg.declare编码了变量x的调试信息。附加到该固有函数的元数据!dbg !14提供了变量x的作用域信息。

!14= !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,variables: !2)

这里!14是提供 位置信息
的元数据。在这个例子中,!4编码了作用域, 一个子程序描述符
。因此附加在固有函数的位置信息表示,变量x在函数foo函数作用域的行号2处声明。

现在让我们看另一个例子。

call
void
@llvm.dbg.declare
( metadata

i32

* %Z
, metadata
!17
, metadata
!13
), !dbg
!19

;[debug line = 5:9] [debug variable = Z]

第三个固有函数%llvm.dbg.declare为变量z编码调试信息。附加在固有函数的元数据!dbg !19提供了变量z的作用域信息。

!18= distinct !DILexicalBlock(scope: !4, file: !1, line: 4, column: 5)

!19= !DILocation(line: 5, column: 11, scope: !18)

这里!19表示z声明在词法域(lexical scope)!18里的行号5,列号0处。词法域本身位于上面描述的子程序!4内。

附加在每条指令的作用域信息提供了一个直截了当的方式来查找被一个作用域覆盖的指令。


C/C++
前端特定的调试信息

C与C++前端以一个就信息内容而言,实际等同于 DWARF3.0
的格式来表示程序的信息。这使代码生成器通过生成标准dwarf信息,轻而易举地支持本机调试器;同时对非dwarf目标机器,包含足够的信息,在需要时翻译之。

本节描述用于表示C与C++程序的形式。其他语言可以效仿之(它本身被调整为以与DWARF3相同的方式表示程序),或者如果它们不合适DWARF模型,可以选择提供完全不一样的形式。因为对调试信息的支持被添加到各个LLVM源语言前端,使用的数据应该记录在这里。

以下各节提供了几个C/C++构造以及最合适描述这些构造的调试信息的例子。权威参考是定义在include/llvm/IR/DebugInfo.h中的DIDescriptor类,以及在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();

}


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 (http://llvm.org/git/clang.gitae4deadbea242e8ea517eef662c30443f75bd086) (http://llvm.org/git/llvm.git818b4c1539df3e51dc7e62c89ead4abfd348827d)”,

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 (http://llvm.org/git/clang.gitae4deadbea242e8ea517eef662c30443f75bd086) (http://llvm.org/git/llvm.git818b4c1539df3e51dc7e62c89ead4abfd348827d)”}

在DIGlobalVariable描述里的align值说明由C11 _Alignas(),C++11alignas()关键字或编译器属性__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,

variables: !2)

;;

;;Define the subprogram itself.

;;

definei32 @main(i32 %argc, i8** %argv) !dbg !4 {

}

调试信息格式

Objective-C属性的调试信息扩展


介绍

使用被声明的属性,Objective-C提供了一个更简单的方式来声明及定义访问器(accessor)方法。该语言提供声明一个属性并让编译器合成访问器方法的特性。

调试器让开发者检查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 attribute。属性标记也将包含该属性的名字与声明类型。

如果有相关的ivar,还将有一个放置在DW_TAG_member DIE的DWARF属性attribute,被该ivar用于援引回该属性的属性TAG。在编译器直接合成ivar的情形里,编译器预期为该ivar生成一个DW_AT_member(DW_AT_artficial设置为1),其名字将是代码里用于直接访问这个ivar的名字,属性attribute指回它支持的属性。

下面的例子用于展示我们的讨论:

@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 ) )

注意,当前约定是用于一个自动合成属性的ivar名字是该属性名前接一个下划线,正如例子所示。但我们实际上不需要知道这个约定,因为ivar的名字直接向我们给出。

同样,在ObjC里在@interface与@implementation中具有不同属性说明是常见做法——即在接口里提供一个只读属性,在实现中提供一个读写接口。在这个情形里,编译器应该发布在当前编译单元生效的属性声明。

开发者可使用以DW_AT_APPLE_property_attribute编码的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)

DW_AT_APPLE_property_setter及DW_AT_APPLE_property_getterattribute将setter与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
标签

TAG

Value

DW_TAG_APPLE_property

0x4200


DWARF
属性(
attribute


Attribute

Value

Classes

DW_AT_APPLE_property

0x3fed

Reference

DW_AT_APPLE_property_getter

0x3fe9

String

DW_AT_APPLE_property_setter

0x3fea

String

DW_AT_APPLE_property_attribute

0x3feb

Constant



DWARF
常量

Name

Value

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

名字加速器表(Name Accelerator Tables)


介绍

.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++程序(These tables also include an inlined copy of the string values in thetable itself making the tables much larger than they need to be on disk)。

通过向这个表添加所有我们需要的名字能修复这些节吗?不,因为这些表不是设计来包含这些的,并且我们不知道坏的旧表与好的新表间的差异。充其量,我们可以做出我们自己重命名的、包含所有我们所需数据的节。

这些表对像LLDB这样的调试器也是不足够的。LLDB使用clang来解析表达式,它表现得像一个PCH。然后LLDB通常被询问查找类型foo或名字空间bar,或者列出名字空间baz中的项。名字空间没有包含在pubnames或pubtypes表里。因为clang在解析一个表达式时,询问许多问题,在查找名字时,我们需要非常快,因为它频繁发生。对非常快查找优化的新加速器表将显著施惠于这种类型的调试经历。

我们希望生成可以从硬盘映射到内存的名字查找表,很少或无需预先解析就直接使用。我们还应该能控制这些不同表的实际内容,使它们包含我们的实际所需。名字加速器表被设计来修复这些问题。为了解决这些问题,我们需要:

· 具有一个可以从硬盘映射到内存,并直接使用的格式

· 查找应该非常快

· 可扩展的表格式,因此这些表可以由许多生产者制作

· 包含典型查找所需的所有名字

· 表内容有严格的规则

表大小是重要的,加速器表格式应该允许重用来自公共字符串表的字符串,使名字字符串没有重复。我们还希望确保该表随时可直接使用——以尽量少的表头解析,简单地将表映射入内存。

名字查找表需要快,并对调试器倾向使用的各种查找进行优化。理想地,我们希望触及映射表尽可能少的部分,并能够快速找到我们查找的名字项,或者发现没有匹配。至于调试器,我们优化大多数时间失败的查找。

定义的每张表应该对在加速器表中实际有什么有严格的规则与文档记录,使用户可以信赖这些内容。


哈希表


标准哈希表

典型的哈希表有一个头,多个bucket,每个bucket指向储存的内容:

.————.

| HEADER |

|————|

| BUCKETS |

|————|

| DATA |

`————‘

对每个哈希值,BUCKETS是到DATA的一个偏移数组:

.————.

|0x00001000 | BUCKETS[0]

|0x00002000 | BUCKETS[1]

|0x00002200 | BUCKETS[2]

|0x000034f0 | BUCKETS[3]

| | …

|0xXXXXXXXX | BUCKETS[n_buckets]

‘————‘

因此在上面的例子中,对bucket[3],我们有表的一个偏移0x000034f0,指向该bucket的一连串项。每个bucket必须包含一个next指针、完整的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位哈希值是否匹配。为此,我们需要读next指针,然后读哈希值,比较之,跳到下一个bucket。每次我们在内存里跳过许多内存字节并触及新页面,只是为了执行完整的32位哈希值比较。然后所有这些访问告诉我们,没有匹配。


名字哈希表

为了解决上面提到的问题,我们以一个略微不同的方式构建哈希表:一个头,多个bucket,所有唯一32位哈希值的一个数组,后跟一个哈希值数据偏移数组、每个哈希值一个,然后是所有哈希值的数据:

.————-.

| HEADER |

|————-|

| BUCKETS |

|————-|

| HASHES |

|————-|

| OFFSETS |

|————-|

| DATA |

`————-‘

名字表中的BUCKETS是HASHES数组的索引。通过使所有完整32位哈希值在内存中连续,我们允许自己高效地查找一个匹配,同时尽可能少地触及内存。使32位哈希值检查与查找一样快最常见。如果它确实匹配,通常是一个没有冲突的匹配。因此对一个有n_buckets个bucket及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 bithash indexes

|————————-|

| HASHES | uint32_t[n_hashes] // 32 bithash values

|————————-|

| OFFSETS | uint32_t[n_hashes] // 32 bitoffsets 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]包含HASHES表的索引6。然后我们比较HASHES数组中任何连续的32位哈希值,只要这些哈希值在BUCKETS[3]里。要这样做,我们验证后续每个哈希值对n_buckets取模仍然是3。在一个查找失败时,我们将访问BUCKETS[3]的内存,然后在我们知道没有匹配前,比较几个连续32位哈希值。我们最终没有使用许多内存字,我们确实尽可能少地访问处理器数据缓存行。

用于这些查找表的字符串哈希是Daniel J. Bernstein哈希,它也用在ELF GNU_HASH节中。对只有很少哈希冲突的程序里所有类型的名字,它是一个非常好的哈希。

通过使用一个无效的哈希值UINT32_MAX,标示空的bucket。


细节

这些名字哈希表被设计为通用,其中表的特殊性着手定义进到头部的额外数据(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) forcorrect 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

};

头部以一个编码为ASCII整数的32位“HASH”魔数开始。这使检测哈希表起始,以及确定表的字节序使表被正确获取,成为可能。魔数后跟一个16位版本值,以允许在未来修订及修改这个表。当前版本号是1。Hash_function是一个指定使用哪个哈希函数来产生这个表的uint16_t枚举值。哈希函数的当前枚举值包括:

enum
HashFunctionType

{

eHashFunctionDJB =
0u
,
// Daniel J Bernstein hash function

};

Bucket_count是表示BUCKETS数组中有多少bucket的32位无符号整数。Hashes_count是HASHES数组中唯一32位哈希值的数量,在OFFSETS数组中包含相同数量的偏移值。Header_data_lens说明被这个指定版本的表所填充的HeaderData字节大小。


确定的查找

(fixed lookup)

头后跟bucket,哈希,偏移及哈希值数据。

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 entiretable is in this table

uint32_t
offsets[Header.hashes_count];
// An offset that corresponds to each item inthe “hashes[]” array above

};

Buckets是一个hashes数组的32位索引数组。数组hashes包含哈希表里所有名字的所有32位哈希值。Hashes表中每个哈希在offsets数组中都有一个偏移,指向该哈希值的数据。

这个表设置使改变这些表的用途,包含别的数据十分容易,同时对所有的表保持查找机制不变。这个布局也使将表保存到硬盘,稍后将它映射进来,以很少或无需解析,进行非常高效的名字查找,成为可能。

DWARF查找表可以各种方式实现,可对每个名字保存大量的信息。我们希望DWARF表可扩展,能够高效保存数据,因此我们使用支持高效数据存储的某些DWARF特性,来确切定义对每个名字保存什么类型的数据。

HeaderData包含每个HashData块内容的一个定义。对每个名字我们希望保存一个到所有调试信息项(DIE)的偏移。为了保持可扩展性,我们创建了一组项或原子(atom),包含在每个名字的数据中。首先是每个原子的数据类型:

enum
AtomType

{

eAtomTypeNULL =
0u
,

eAtomTypeDIEOffset =
1u
,
// DIE offset, check form for encoding

eAtomTypeCUOffset =
2u
,
// DIE offset of the compiler unit header that contains the itemin question

eAtomTypeTag =
3u
,
// DW_TAG_xxx value, should be encoded as DW_FORM_data1 (if notags exceed 255) or DW_FORM_data2

eAtomTypeNameFlags =
4u
,
// Flags from enum NameFlags

eAtomTypeTypeFlags =
5u
,
// Flags from enum TypeFlags

};

这些枚举值以及它们的含义是:

eAtomTypeNULL – a termination atom that specifies theend of the atom list

eAtomTypeDIEOffset – an offset into the .debug_info section forthe DWARF DIE for this name

eAtomTypeCUOffset – an offset into the .debug_info section forthe CU that contains the DIE

eAtomTypeDIETag – The DW_TAG_XXX enumeration value so youdon’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规范,定义Atom类型数据的确切编码,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是零,那么bucket内容找完了。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个名字项有一次32位哈希冲突。

内容

正如我们所说,我们希望严格地定义在不同的表里包括什么。对DWARF,我们有3个表:.apple_names,.apple_types及.apple_namespaces。

对每个DW_TAG是具有地址属性:DW_AT_low_pc,DW_AT_high_pc,DW_AT_ranges或DW_AT_entry_pc的DW_TAG_label,DW_TAG_inlined_subroutine或DW_TAG_subprogram的DWARF DIE,.apple_names节应该包含一个项。它还包含在该位置(全局及静态变量)具有一个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属性中找到的简单名一同发布。

对标签是以下之一的每个DWARF DIE,.apple_types节应该包含一个项:

· 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_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++库的abi::cxa_demangle()的输出。

语言扩展与文件格式变化


Objective-C
扩展

对一个Objective-C类,.apple_objc节应该包含所有DW_TAG_subprogram DIE。在哈希表中的名字是该Objective-C类本身的名字。如果这个Objective-C类有一个分类(category),对没有分类的类名及有分类的类名都构造一项。因此,如果我们在偏移0x1234处有一个带有方法名”-[NSString(my_additions)stringWithSpecialString:] “的DIE,我们将对”NNString”添加一项,指向DIE 0x1234,对”NSString(my_additions)”添加一项,指向0x1234。这使我们在执行表达式时,能快速追踪一个Objective-C类的所有Objective-C方法。因为Objective-C动态的本质,任何人都可以向一个类添加方法,这是需要的。发布用于Objective-C方法的DWARF也不同于C++类,C++类的方法通常不包含在类定义里,它们分散在一个或多个编译单元中。分类也可以定义在不同的共享库中。因此,给定一个Objective-C类名,或者对一个类+分类名,我们需要能快速找出所有的方法与类函数。这个表不包含任何选择符名,它只是将Objective-C类名(或类名+分类)映射到所有的方法与类函数。选择符作为函数基本名添加在.debug_names节。

在Objective-C函数的.apple_names节里,完整名是带有括号(“-[NSStringstringWithCString:]”)的整个函数名,基本名是选择符(“stringWithCString:”)。


Mach-O
的改变

Apple哈希表的节名用于non-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(由/ZiMSVC选项激活),.debug$T节将仅包含一个指向该PDB的LF_TYPESERVER2记录。在使用多个PDB时,符号信息看起来仍然在目标文件的.debug$S节里。

通过索引来援引类型记录,索引是流里一个给定记录前的记录数加上0x1000。许多公共基本类型,比如基本整数类型及它们的非受限指针,使用小于0x1000的类型索引来表示。这样的基本类型构建在CodeView使用者中,不要求类型记录。

每个类型记录仅可能包含小于它自身类型索引的类型索引。这确保了类型流引用的图是无环的。尽管源代码类型图通过指针类型可能包含环(考虑一个链表结构),通过总是援引用户定义记录类型的前向声明记录,这些环被从类型流里删除。.debug$S流中仅symbol记录会援引完整的、非前向声明的类型记录。

使用
CodeView

这是对致力于改进LLVM对CodeView支持的开发者某些共同任务的指引。他们大多数人围绕在使用llvm-readobj中的CodeView倾印器。

· 测试MSVC输出:

· $ cl -c -Z7 foo.cpp #Use /Z7 to keep types in the object file

· $ llvm-readobj -codeviewfoo.obj

· 从Clang拿出LLVM IR调试信息:

· $ clang -g -gcodeview–target=x86_64-windows-msvc foo.cpp -S -emit-llvm

使用这为LLVM测试用例生成LLVM IR。

· 从LLVMIR元数据产生并倾印CodeView:

· $ llc foo.ll-filetype=obj -o foo.obj

· $ llvm-readobj -codeviewfoo.obj > foo.txt

在lit测试用例中使用这个模式,并FileCheck llvm-readobj的输出。

改进LLVM对CodeView支持是一个找出感兴趣类型记录,构造一个C++测试用例、使MSVC发布这些记录,倾印记录,理解它们,然后在LLVM后端里生成等效记录的过程。

稿源:wuhui_gdnt的专栏 (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 综合技术 » [译]LLVM的源代码调试

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录