系列文章:
GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop) GDB 源码分析系列文章二:gdb 主流程 Event Loop 事件处理逻辑详解 GDB 源码分析系列文章三:调试信息的处理、符号表的创建和使用
GDB 源码分析系列文章三:调试信息的处理、符号表的创建和使用
gdb 之所以能够进行源码级调试,本质上是编译的过程保存了源码到目标程序之间的映射关系,包括行号、地址等映射关系。gdb 在调试程序时,会对调试信息和符号表进行加工处理,得到 gdb 内部的符号表,gdb 依靠符号表完成一些列调试任务。
调试信息和符号表简介
这里说的调试信息指的是 debug_* 相关信息,符号表是指 symtab 和 dynsym 。gdb 对这两种信息进行解析和加工,得到的信息在 gdb 内部统称为符号表。
调试信息
调试信息需要带上 -g 编译选项才能产生。关于调试信息在我之前的博客中有一篇引导性的介绍 调试信息(debugging information)——解析DWARF文件 这里只简单列举下各个 section。
使用 readelf -S xx.out 得到可执行文件各个 section 的列表:
There are 37 section headers, starting at offset 0x99d8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000000338 00000338
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.gnu.build-i NOTE 0000000000000358 00000358
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000037c 0000037c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0
0000000000000024 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003c8 000003c8
00000000000000a8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000470 00000470
0000000000000084 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000000004f4 000004f4
000000000000000e 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000508 00000508
0000000000000020 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000528 00000528
00000000000000c0 0000000000000018 A 6 0 8
[11] .rela.plt RELA 00000000000005e8 000005e8
0000000000000018 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020
0000000000000020 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 0000000000001040 00001040
0000000000000010 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 0000000000001050 00001050
0000000000000010 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000001060 00001060
00000000000001d5 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 0000000000001238 00001238
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000
000000000000000c 0000000000000000 A 0 0 4
[19] .eh_frame_hdr PROGBITS 000000000000200c 0000200c
000000000000004c 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 0000000000002058 00002058
0000000000000128 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000003db8 00002db8
0000000000000008 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003dc0 00002dc0
0000000000000008 0000000000000008 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003dc8 00002dc8
00000000000001f0 0000000000000010 WA 7 0 8
[24] .got PROGBITS 0000000000003fb8 00002fb8
0000000000000048 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000004000 00003000
0000000000000014 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004014 00003014
0000000000000004 0000000000000000 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 00003014
000000000000002b 0000000000000001 MS 0 0 1
[28] .debug_aranges PROGBITS 0000000000000000 0000303f
0000000000000030 0000000000000000 0 0 1
[29] .debug_info PROGBITS 0000000000000000 0000306f
000000000000037f 0000000000000000 0 0 1
[30] .debug_abbrev PROGBITS 0000000000000000 000033ee
0000000000000122 0000000000000000 0 0 1
[31] .debug_line PROGBITS 0000000000000000 00003510
0000000000000266 0000000000000000 0 0 1
[32] .debug_str PROGBITS 0000000000000000 00003776
00000000000047a7 0000000000000001 MS 0 0 1
[33] .debug_macro PROGBITS 0000000000000000 00007f1d
000000000000106a 0000000000000000 0 0 1
[34] .symtab SYMTAB 0000000000000000 00008f88
00000000000006d8 0000000000000018 35 52 8
[35] .strtab STRTAB 0000000000000000 00009660
000000000000020c 0000000000000000 0 0 1
[36] .shstrtab STRTAB 0000000000000000 0000986c
0000000000000167 0000000000000000
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
其中 .debug 打头的就是调试信息了。
section | description |
---|
.debug_aranges | 范围表。每个编译单元对应一个范围表,记录一些 entry 的范围,方便多个编译单元间快速查询。 | .debug_info | 主要的调试信息。 | .debug_abbrev | 调试信息缩写表。每个编译单元对应一个缩写表,缩写表是该编译单元的一系列缩写。 | .debug_line | 调试行信息。源码的行对应到的目标程序的pc。 | .debug_str | .debug_info中使用到的字符串表 | .debug_macinfo | 宏信息。-g3 编译才会产生。上面的编译程序没有 |
符号表
其实符号表包括 symtab 和 dynsym 两种。在没有 -g 编译也会产生,在重定位过程中需要处理。
- symtab 包括两种类型符号:全局符号和本地静态符号。
- dynsym 仅仅包加载动态库所需要的符号。
其实,symtab 是包含了 dynsym ,也就是说 dynsym 是 symtab 的子集。那为什么还要保存重复的 dynsym 呢?当对目标程序进行 strip 时 strip xx.out ,symtab 将会被去除,但是 dynsym 还在,可以保证动态加载可以正常工作。因此,gdb 在解析符号表时候,只需要读取 symtab 即可。
调试信息的处理和符号表的创建
gdb 读取调试信息和符号表,在 gdb 使用符号表来记录, gdb 内部存在 3 种符号表。
- minimal symbol table:最小符号表。直接读取 ELF 文件中 .symtab 的 section。也即链接过程中使用到的符号。最小符号表 “better than nothing” 。最小符号表至少可以满足你通过函数名打断点。
- partial symbol table:部分符号表。在 minimal symbol table 基础上初步分析调试信息(.debug_info section),得到部分符号的部分信息,可以满足一定的调试要求。并且部分符号表记录了读取完整符号表的函数指针,可以在部分符号表的基础上读取完整符号表。
- full symbol table:完整符号表。记录了完整的调试信息,占用内存大。gdb 首先是建立部分符号表,只在必要的时候才会建立完整的符号表。
最小符号表
最小符号表是直接读取 ELF 文件的 .symtab section 得到。
最小符号表数据结构简介
在看 minimal_symbol 之前,先看下 general_symbol_info 。general_symbol_info 是在所有 3 种符号(minimal_symbol ,partial_symbol 和 (full) symbol )都共有的成员。
general_symbol_info 主要成员如下(详见 gdb/symtab.h):
成员 | 说明 |
---|
name | 符号的名字。对于 c++ 而言,mangled name 和 demangled name 是不一样的,这里是 mangled name。 | value | 符号的值。是一个枚举类型,它的含义取决于符号的类型(SYMBOL_CLASS)。比如一个函数符号,这里就对应一个 block,block中包含 pc 范围。 | language_specific | 特定语言使用,是一个枚举,比如 c++,这里就是它的 demangled name | section | 指明这个符号属于哪个 section,是一个下标索引 |
minimal_symbol 主要成员如下(详见 gdb/symtab.h):
成员 | 说明 |
---|
general_symbol_info mginfo | 即 general_symbol_info | unsigned long size | 符号的 size | filename | 符号归属的源文件 | type | minimal symbol 类型。比如属于 bss 段、text 段等 | struct minimal_symbol *hash_next | 拥有相同 hash key 的 minmal symbol 通过 hash_next 链接在一起 |
minimal_symbol 最后存储在 objfile 的 per-bfd 的存储空间,形成 minimal symbol table 。
最小符号表创建流程
read_symbols ? elf_symfile_read ? ? elf_read_minimal_symbols ? ? ? elf_symtab_read ? ? ? install_minimal_symbols
elf_symfile_read 调用 elf_read_minimal_symbols 读取和建立最小符号表。
elf_read_minimal_symbols 首先通过 bfd 库得到 .symtab section 的大小和符号个数。然后调用 elf_symtab_read 函数读取符号表。最后调用 install_minimal_symbols 安装符号表。
install_minimal_symbols 会对符号表进行排序、去重、压缩后存放在 objfile->per_bfd->msymbols 中,再给 minimal symblos 建立 hash table,用于对 minimal symbols 进行索引。
此外,符号表除了 dwarf 格式,还有其他格式,例如 stab 格式。符号表的建立流程中还有很多兼容性处理,这里不再赘述。
部分符号表
部分符号表是在最小符号表的基础上,尝试读取 dwarf 的调试信息,形成相对比较完整的符号表。
部分符号表数据结构简介
partial_symbol 成员如下(详见 gdb/psympriv.h):
成员 | 说明 |
---|
ginfo | 即 general_symbol_info | domain | 符号的类型。变量、函数、label、type等 | aclass | adress class。符号的地址类型,是寄存器、局部变量、typedef、args等 |
partial_symbol 以一定的规则组合形成 partial_symtab ,一个源文件对应一个 partial_symtab ,一个 objfile 中所有 partial_symtab 形成一个链表。
partial_symtab 主要成员如下(详见 gdb/psympriv.h):
成员 | 说明 |
---|
struct partial_symtab *next | partial_symtab 链表 | filename、 fullname、dirname | 文件名、完整路径文件 、编译目录 | CORE_ADDR textlow 、CORE_ADDR textlow | 文件地址范围 | void (*read_symtab) (struct partial_symtab *, struct objfile *) | 用于读取完整符号表的函数指针 | struct partial_symtab *user | 非空则代表本符号表是一个共享的 prtial_symtab,被 user 共享 | struct partial_symtab **dependencies | 指向本符号表依赖的符号表,被依赖的符号表需要先读入。似乎是给 stabs 格式专用 | unsigned char readin | 标识符号表是否已经读入 | int globals_offset、n_global_syms | 本文件对应的全局符号在 objfile->global_psymbols 中的偏移和个数 | statics_offset、n_static_syms | 本文件对应的静态符号在 objfile->static_psymbols 中的偏移和个数 | struct compunit_symtab *compunit_symtab | 本文件最终编译单元的符号表 |
每个源文件还没有完全读入,会先形成部分符号表。其中包含有关特定文件的调试符号在可执行文件中的位置的信息,以及位于该文件中的全局符号的名称列表。它们链接形成部分符号列表,即使完整符号表读入后,它们依然保留。
部分符号表创建流程
read_symbols ? require_partial_symbols ? ? read_psyms ? ? ? dwarf2_build_psymtabs ? ? ? ? dwarf2_build_psymtabs_hard
建立最小符号表后,随即调用到 dwarf2_build_psymtabs_hard ,在 dwarf2_build_psymtabs_hard 中处理 .debug_info 和 .debug_abbrev section 建立部分符号表 。
dwarf2_build_psymtabs_hard ? dwarf2_read_section ? create_all_comp_units ? process_psymtab_comp_unit ? ? init_cutu_and_read_dies ? ? process_psymtab_comp_unit_reader ? ? ? create_partial_symtab ? ? ? load_partial_dies ? ? ? scan_partial_symbols ? ? ? dwarf2_build_include_psymtabs
dwarf2_build_psymtabs_hard 首先调用 dwarf2_read_section 将 .debug_info section 读入。然后调用 create_all_comp_units 找到每个 compile unit (cu),记录 cu 个数,然后依次读取 cu 的 offset、length 等信息。最后调用 process_psymtab_comp_unit 处理每个 cu 的信息。
init_cutu_and_read_dies 首先读取 .debug_info section 的 Abbrev offset ,该 offset 是本 cu 在 .debug_abbrev 中的偏移。然后读取该 cu 的 .debug_abbrev section。接着根据 abbrev 读取 comiple unit 的 DIE。
在读取 cu 信息后,调用process_psymtab_comp_unit_reader 处理该 cu 的符号信息。由于一个 cu 对应一个 partial symtab(pst),根据文件名、路径、完整符号表调用函数等信息通过 create_partial_symtab 函数建立符号表,并添加到 objfile->psymtabs 中,并设置 pst 的 globals_offset 和 static_offset 等信息。
建立 pst 后,调用 load_partial_dies 将感兴趣的 dies 读入,这里只读入本文件全局或者静态变量相关的 die。比如 DW_TAG_subprogram 是代表一个函数的 die,DW_TAG_variable 代表变量的 die。 的 scan_partial_symbols 解析这些 dies,不同 die 有不同的处理函数,得到 global symbols 和 static symbols,并存放到 objfile->global_psymbols 和 objfile->static_psymbols 中,得到该文件的全局符号和静态符号信息,但这里不包括局部符号。这里顺便说下,对于函数符号,psym->ginfo.value.address 存放的是函数首地址,对于全局或者静态变量,存放的是变量地址。
最后,还需要调用 dwarf2_build_include_psymtabs 处理本文件包含的头文件的 pst。这里只会为本文件实际用到的头文件建立 pst(例如,你可能 include 了一个不需要的头文件,那则没必要为它建立 pst)。这就需要分析 .debug_line 的信息了。.debug_line 中的行信息也是以 cu 为单位存放的。逐行解析.debug_line 中的 opcode,有的 opcode 会改变 file 寄存器,并且与当前文件 file 寄存器值不一样,则需要为该 include file 创建 pst, 并加入到 objfile->psymtabs 中,并记录 include file 的 dependencies 为本文件 pst。
至此,psts 建立完成。
完整符号表
如果 gdb 调试过程中需要的符号信息,超出了 pst 的范围,则需要根据 pst 的函数指针 read_symtab 读取完整符号表。
完整符号表数据结构简介
完整符号表的符号定义在 gdb/symtab.h 中。struct symbol 主要成员如下:
成员 | 说明 |
---|
ginfo | 即 general_symbol_info | type | data type of value | union owner | 该符号归属的符号表指针 | domain | 符号的类型。变量、函数、label、type等 | aclass_index | adress class | unsigned is_argument | 是否为一个 argument | unsigned is_inlined | 是否为一个内连函数 | unsigned is_cplus_template_function | 是否为 c++ 木板函数 | unsigned short line | 符号被定义的行数 | struct symbol *hash_next | next hash |
struct symtab 主要成员如下:
成员 | 说明 |
---|
struct symtab *next | symtab 链表 | struct compunit_symtab *compunit_symtab | cu symtab 链表 | struct linetable *linetable | 本文件的 core address 和 line number 的 map | const char *filename; | 源文件名 | int nlines | 源文件总行数 | enum language language | 源文件语言 | char *fullname | 完整文件名 |
另外,完整符号表定义了 struct pending *file_symbols 、pending *global_symbols 和 pending *local_symbols 等信息分别记录 static、global 和 local 符号信息。详见 gdb/buildsym.h 。
完整符号表创建流程
dwarf2_read_symtab ? psymtab_to_symtab_1 ?? dw2_do_instantiate_symtab ??? load_cu ??? process_queue ???? process_full_comp_unit ???? ?process_die ???? ?? read_file_scope ???? ?? read_func_scope ???? ?? process_structure_scope ???? ?? new_symbol ???? ?end_symtab_get_static_block ???? ?end_symtab_from_static_block ???? ?? end_symtab_with_blockvector
dwarf2_read_symtab 首先调用 psymtab_to_symtab_1 包含依赖文件在内的所有文件的 符号表。 psymtab_to_symtab_1 再调用 load_cu 读取 cu,加入到 queue 中,然后调用 process_queue ->process_full_comp_unit ->process_die 处理每个 die,建立完整符号表。
process_die 根据不同 die 进入不同的分发函数进行处理。比如 read_func_scope 函数:将局部变量放到 local_symbols 、file_static 变量符号放到 static_symbols 、global 符号放入到 global_symbols 。函数创建 block ,block 的 startaddr 和 endaddr 对应函数的 pc 地址范围,将局部符号添加到 block->dict 中,并将函数的 symbol 和 block 绑定,再将该block 放入pending_blocks 中。
最后,end_symtab_with_blockvector 构建 static_blocks ,将 file_symbols 添加到它的 dict 中。构建 global_blocks ,将 global_symbols 添加到它的 dict 中。最后将 static_blocks 、globla_blocks 和 pending_blocks 组成 blockvector 。最后放到 symtab 的 compunit_symtab 中。
符号表的使用
使用符号表的接口为 lookup_symbol 。根据当前 pc,找到当前 block,在该 block 的 dict 中查找所需的符号(根据符号名),若找到,则返回。否则在 static_block 中 dict 查找,若找到,则返回。若还找不到,说明不在当前 cu 中,接着调用 lookup_global_symbol 。在 lookup_global_symbol ->…->lookup_symbol_in_objfile 中,先调用 lookup_symbol_in_objfile_symtabs ,若找到,则返回。否则,这个时候需要遍历 psts,在 obifile->global_symbols 查找,找到对应的 pst,进入完整符号表的构建。这里也可以看到,只在需要的时候才会建立完整的符号表。在完整符号表建立后,上述查找符号表的过程中就一定能够找到所需的符号了。
|