DWARF和符号化

  1. DWARF 格式简介
  2. 利用DWARF符号化
    1. 函数名称
    2. 内联函数处理
    3. 非定义调试信息项处理
    4. 定位地址对应Dwarf debug info
    5. 行号信息
  3. 利用symtab符号化
  4. 优化
  5. 写在最后

DWARF 格式简介

DWARF: 它是可执行程序与源代码关系的一个紧凑的表示.

大多数现代编程语言是块结构的:每个实体(例如,一个类定义或一个函数)被包含在另一个实体中。在一个C程序里,每个文件可能包含多个数据定义、多个变量定义,及多个函数。DWARF遵循这个模型,它也是块结构的。在DWARF里基本的描述项是调试信息项(DebuggingInformation Entry——DIE)。一个DIE有一个标签,它指明了这个DIE描述什么及一个填入了细节并进一步描述该项的属性列表。一个DIE(除了最顶层的)被一个父DIE包含(或者说拥有),并可能有兄弟DIE或子DIE。属性可能包含各种值:常量(比如一个函数名),变量(比如一个函数的起始地址),或对另一个DIE的引用(比如一个函数的返回值类型)。

利用DWARF符号化

符号化地址,我们期望的结果是,获取地址对应的函数名称和调用行数等信息。DWARF调试格式中携带了大量信息,当然包括函数名称和地址。因此如果我们提供的调用地址能在函数中找到信息,优先使用DWARF中的符号信息进行符号化。

在 DWARF 中,这些信息主要由 DW_AT_subprogram 和 line table 承载。
我们只需要根据地址在 DWARF 文件中找到对应的 信息。

函数名称

DW_AT_Subprogram 中可以获取到的信息

很容易看到,Dwarf调试信息中精心设计了 函数信息的具体表示,我们可以获取名称和类型等重要的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
1$: DW_TAG_unspecified_type
DW_AT_name("void")
...
2$ DW_TAG_base_type
DW_AT_name("int")
...
3$: DW_TAG_class_type
DW_AT_name("A")
...
4$: DW_TAG_pointer_type
DW_AT_type(reference to 3$)
...
5$: DW_TAG_const_type
DW_AT_type(reference to 3$)
...
6$: DW_TAG_pointer_type
DW_AT_type(reference to 5$)
...
7$: DW_TAG_subprogram
DW_AT_name("func1")
DW_AT_type(reference to 1$)
DW_AT_object_pointer(reference to 8$)
! References a formal parameter in this member function
...
8$: DW_TAG_formal_parameter
DW_AT_artificial(true)
DW_AT_name("this")
DW_AT_type(reference to 4$)
! Makes type of 'this' as 'A*' =>
! func1 has not been marked const or volatile
DW_AT_location ...
...
9$: DW_TAG_formal_parameter
DW_AT_name(x1)
DW_AT_type(reference to 2$)
...
10$: DW_TAG_subprogram
DW_AT_name("func2")
DW_AT_type(reference to 1$)
DW_AT_object_pointer(reference to 11$)
! References a formal parameter in this member function
...
11$: DW_TAG_formal_parameter
DW_AT_artificial(true)
DW_AT_name("this")
DW_AT_type(reference to 6$)
! Makes type of 'this' as 'A const*' =>
! func2 marked as const
DW_AT_location ...
...
12$: DW_TAG_subprogram
DW_AT_name("func3")
DW_AT_type(reference to 1$)
...
! No 'this' formal parameter => func3 is static
13$: DW_TAG_formal_parameter
DW_AT_name(x3)
DW_AT_type(reference to 2$)
...

内联函数处理

对于内联函数,编译器会将其在多处展开。可以通过 DW_AT_inline 标志判断其展开的状态和判断其是否是”abstract instance entry”.
基于存储空间的考虑,把 某个内联函数构造成 “abstract instance entry” 提供名称等信息,可以避免多个具体内联函数展开的重复空间消耗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
! Abstract instance for OUTER
!
OUTER.AI.1.1:
DW_TAG_subprogram
DW_AT_name("OUTER")
DW_AT_inline(DW_INL_declared_inlined)
! No low/high PCs
OUTER.AI.1.2:
DW_TAG_formal_parameter
DW_AT_name("OUTER_FORMAL")
DW_AT_type(reference to integer)
! No location
OUTER.AI.1.3:
DW_TAG_variable
DW_AT_name("OUTER_LOCAL")
DW_AT_type(reference to integer)
! No location
!
! Abstract instance for INNER
!
INNER.AI.1.1:
DW_TAG_subprogram
DW_AT_name("INNER")
DW_AT_inline(DW_INL_declared_inlined)
! No low/high PCs
INNER.AI.1.2: DW_TAG_formal_parameter
DW_AT_name("INNER_FORMAL")
DW_AT_type(reference to integer)
! No location
INNER.AI.1.3: DW_TAG_variable
DW_AT_name("INNER_LOCAL")
DW_AT_type(reference to integer)
! No location
...
0
! No DW_TAG_inlined_subroutine (concrete instance)
! for INNER corresponding to calls of INNER
...
0
Figure 66. Inlining example #1: abstract instance

concrete instance 表示编译时,某处内联函数的展开。它最主要的信息就是 DW_AT_low_pc,DW_AT_high_pc 通过pc地址信息,我们可以将函数调用地址定位到具体的DWARF debug entry。
由于存在 “abstract instance”,”concrete instance” 的某些属性会被省略,因此我们需要通过 DW_AT_abstract_origin 属性去找到对应的 abstract instance entry,获取被省略掉的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
! Concrete instance for call "OUTER(7)"
!
OUTER.CI.1.1:
DW_TAG_inlined_subroutine
! No name
DW_AT_abstract_origin(reference to OUTER.AI.1.1)
DW_AT_low_pc(...)
DW_AT_high_pc(...)
OUTER.CI.1.2:
DW_TAG_formal_parameter
! No name
DW_AT_abstract_origin(reference to OUTER.AI.1.2)
DW_AT_const_value(7)
OUTER.CI.1.3:
DW_TAG_variable
! No name
DW_AT_abstract_origin(reference to OUTER.AI.1.3)
DW_AT_location(...)
!
! No DW_TAG_subprogram (abstract instance) for INNER
!
! Concrete instance for call INNER(OUTER_LOCAL)
!
INNER.CI.1.1:
DW_TAG_inlined_subroutine
! No name
DW_AT_abstract_origin(reference to INNER.AI.1.1)
DW_AT_low_pc(...)
DW_AT_high_pc(...)
DW_AT_static_link(...)
INNER.CI.1.2: DW_TAG_formal_parameter
! No name
DW_AT_abstract_origin(reference to INNER.AI.1.2)
DW_AT_location(...)
INNER.CI.1.3: DW_TAG_variable
! No name
DW_AT_abstract_origin(reference to INNER.AI.1.3)
DW_AT_location(...)
...
0
! Another concrete instance of INNER within OUTER
! for the call "INNER(31)"
...
0

非定义调试信息项处理

当函数定义并不在声明区域时,subprogram DIE 就会有 DW_AT_specification 属性,指向相关的函数定义DIE.
对于带有 DW_AT_specification 的调试信息项,我们需要进行特殊处理。

定位地址对应Dwarf debug info

backtrace() 我们可以拿到 image 的 loadaddress 和 address调用地址。

进而得到文件虚拟地址
fileVmAddress = address - (loadaddress - imageFileVmTextAddress)

通过文件虚拟地址后通过 .debug_aranges 查找到指定的 DWARF compile unit,进而查找到具体的DIE. 从属性中获取我们需要的信息。

.debug_aranges 是DWARF 中包含 debug info entry offset的 Section。
DWARF compile unit 是编译单元,在这里我们可以理解为文件或文件中的片段。也是DIE

行号信息

对于一个文件,我们将其编译后,其产出汇编代码类似于这种形式,在原始文件中的行对应着多条汇编指令。
对于行号的查找我们只需要 将符号的运行时地址转换为文件的虚拟地址,在该文件的行号信息中进行范围查找即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1: int
2: main()
0x239: push pb
0x23a: mov bp,sp
3: {
4: printf(“Omit needless words\n”);
0x23c: mov ax,0xaa
0x23f: push ax
0x240: call _printf
0x243: pop cx
5: exit(0);
0x244: xor ax,ax
0x246: push ax
0x247: call _exit
0x24a: pop cx
6: }
0x24b: pop bp
0x24c: ret
7:
0x24d:

对应的编码类似于: SPECIAL(n,m) 制定了行号增加和地址增加

1
2
3
4
5
6
7
8
9
Opcode Operand Byte Stream
--------------------------------------------------------------------------------
DW_LNS_advance_pc LEB128(0x239) 0x2, 0xb9, 0x04
SPECIAL(2, 0) 0xb
SPECIAL(2, 3) 0x38
SPECIAL(1, 8) 0x82
SPECIAL(1, 7) 0x73
DW_LNS_advance_pc LEB128(2) 0x2, 0x2
DW_LNE_end_sequence 0x0, 0x1, 0x1

利用symtab符号化

当我们代码编译的时候,可以选择产生调试信息。也可以不产生调试信息,作为降级方案,我们仍可采用symbtab所包含的信息进行符号化。
symtab段我们可以提取到具体符号的地址,对应字符串表中的偏移地址,很容易就取到对应的符号名称。

具体可以参考各种实现,这里贴一段fishhook里的代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
Dl_info info;
if (dladdr(header, &info) == 0) {
return;
}
printf("dli_fname:%s \ndli_sname:%s", info.dli_fname, info.dli_sname);
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
// Find base symbol/string table addresses
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
section_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
}

优化

符号化工具提供了 将函数地址 转化为 具体名称等信息的能力。但是提供大规模的符号化能力,需要在 网络I/O,缓存,Server端进一步的优化,这些优化我会写在内网Wiki上 hhhhh…

写在最后

符号化仅是DWARF提供能力的冰山一角,作为提供给 lldb, gdb等调试器的文件格式,蕴含着大量的信息,也就是说,上传了DWARF文件,也相当于应用裸奔了…

script>