GDB 源码分析系列文章三:调试信息的处理、符号表的创建和使用

系列文章:

GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop)
GDB 源码分析系列文章二:gdb 主流程 Event Loop 事件处理逻辑详解
GDB 源码分析系列文章三:调试信息的处理、符号表的创建和使用
GDB 源码分析系列文章四:gdb 事件处理异步模式分析 ---- 以 ctrl-c 信号为例
GDB 源码分析系列文章五:动态库延迟断点实现机制

gdb 之所以能够进行源码级调试,本质上是编译的过程保存了源码到目标程序之间的映射关系,包括行号、地址等映射关系。gdb 在调试程序时,会对调试信息和符号表进行加工处理,得到 gdb 内部的符号表,gdb 依靠符号表完成一些列调试任务。

调试信息和符号表简介

这里说的调试信息指的是 debug_* 相关信息,符号表是指 symtabdynsym。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 打头的就是调试信息了。

sectiondescription
.debug_aranges范围表。每个编译单元对应一个范围表,记录一些 entry 的范围,方便多个编译单元间快速查询。
.debug_info主要的调试信息。
.debug_abbrev调试信息缩写表。每个编译单元对应一个缩写表,缩写表是该编译单元的一系列缩写。
.debug_line调试行信息。源码的行对应到的目标程序的pc。
.debug_str.debug_info中使用到的字符串表
.debug_macinfo宏信息。-g3 编译才会产生。上面的编译程序没有

符号表

其实符号表包括 symtabdynsym 两种。在没有 -g 编译也会产生,在重定位过程中需要处理。

  • symtab 包括两种类型符号:全局符号和本地静态符号。
  • dynsym 仅仅包加载动态库所需要的符号。

其实,symtab 是包含了 dynsym,也就是说 dynsymsymtab 的子集。那为什么还要保存重复的 dynsym 呢?当对目标程序进行 strip 时 strip xx.outsymtab 将会被去除,但是 dynsym 还在,可以保证动态加载可以正常工作。因此,gdb 在解析符号表时候,只需要读取 symtab 即可。

调试信息的处理和符号表的创建

gdb 读取调试信息和符号表,在 gdb 使用符号表来记录, gdb 内部存在 3 种符号表。

  1. minimal symbol table:最小符号表。直接读取 ELF 文件中 .symtab 的 section。也即链接过程中使用到的符号。最小符号表 “better than nothing” 。最小符号表至少可以满足你通过函数名打断点。
  2. partial symbol table:部分符号表。在 minimal symbol table 基础上初步分析调试信息(.debug_info section),得到部分符号的部分信息,可以满足一定的调试要求。并且部分符号表记录了读取完整符号表的函数指针,可以在部分符号表的基础上读取完整符号表。
  3. full symbol table:完整符号表。记录了完整的调试信息,占用内存大。gdb 首先是建立部分符号表,只在必要的时候才会建立完整的符号表。

最小符号表

最小符号表是直接读取 ELF 文件的 .symtab section 得到。

最小符号表数据结构简介

在看 minimal_symbol 之前,先看下 general_symbol_infogeneral_symbol_info 是在所有 3 种符号(minimal_symbolpartial_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符号归属的源文件
typeminimal symbol 类型。比如属于 bss 段、text 段等
struct minimal_symbol *hash_next拥有相同 hash key 的 minmal symbol 通过 hash_next 链接在一起

minimal_symbol 最后存储在 objfileper-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等
aclassadress class。符号的地址类型,是寄存器、局部变量、typedef、args等

partial_symbol 以一定的规则组合形成 partial_symtab,一个源文件对应一个 partial_symtab,一个 objfile 中所有 partial_symtab 形成一个链表。

partial_symtab 主要成员如下(详见 gdb/psympriv.h):

成员说明
struct partial_symtab *nextpartial_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 sectionAbbrev 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_offsetstatic_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_psymbolsobjfile->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
typedata type of value
union owner该符号归属的符号表指针
domain符号的类型。变量、函数、label、type等
aclass_indexadress class
unsigned is_argument是否为一个 argument
unsigned is_inlined是否为一个内连函数
unsigned is_cplus_template_function是否为 c++ 木板函数
unsigned short line符号被定义的行数
struct symbol *hash_nextnext hash

struct symtab 主要成员如下:

成员说明
struct symtab *nextsymtab 链表
struct compunit_symtab *compunit_symtabcu symtab 链表
struct linetable *linetable本文件的 core address 和 line number 的 map
const char *filename;源文件名
int nlines源文件总行数
enum language language源文件语言
char *fullname完整文件名

另外,完整符号表定义了 struct pending *file_symbolspending *global_symbolspending *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。函数创建 blockblockstartaddrendaddr 对应函数的 pc 地址范围,将局部符号添加到 block->dict 中,并将函数的 symbolblock 绑定,再将该block 放入pending_blocks 中。

最后,end_symtab_with_blockvector 构建 static_blocks,将 file_symbols 添加到它的 dict 中。构建 global_blocks,将 global_symbols 添加到它的 dict 中。最后将 static_blocksglobla_blockspending_blocks 组成 blockvector。最后放到 symtabcompunit_symtab 中。

符号表的使用

使用符号表的接口为 lookup_symbol。根据当前 pc,找到当前 block,在该 block 的 dict 中查找所需的符号(根据符号名),若找到,则返回。否则在 static_blockdict 查找,若找到,则返回。若还找不到,说明不在当前 cu 中,接着调用 lookup_global_symbol。在 lookup_global_symbol->…->lookup_symbol_in_objfile 中,先调用 lookup_symbol_in_objfile_symtabs,若找到,则返回。否则,这个时候需要遍历 psts,在 obifile->global_symbols 查找,找到对应的 pst,进入完整符号表的构建。这里也可以看到,只在需要的时候才会建立完整的符号表。在完整符号表建立后,上述查找符号表的过程中就一定能够找到所需的符号了。

  • 13
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值