赞
踩
系列文章:
GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop)
GDB 源码分析系列文章二:gdb 主流程 Event Loop 事件处理逻辑详解
GDB 源码分析系列文章三:调试信息的处理、符号表的创建和使用
GDB 源码分析系列文章四:gdb 事件处理异步模式分析 ---- 以 ctrl-c 信号为例
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
,也就是说 dynsym
是 symtab
的子集。那为什么还要保存重复的 dynsym
呢?当对目标程序进行 strip 时 strip xx.out
,symtab
将会被去除,但是 dynsym 还在,可以保证动态加载可以正常工作。因此,gdb 在解析符号表时候,只需要读取 symtab
即可。
gdb 读取调试信息和符号表,在 gdb 使用符号表来记录, gdb 内部存在 3 种符号表。
最小符号表是直接读取 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,进入完整符号表的构建。这里也可以看到,只在需要的时候才会建立完整的符号表。在完整符号表建立后,上述查找符号表的过程中就一定能够找到所需的符号了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。