DWARF详解

DWARF 是一种调试信息格式,通常用于源码级别调试
相关资料比较琐碎, 整理给大家, 希望大家可以用得上

如没有特殊说明, 命令执行环境为 OS X

什么是 DWARF ?

  • DWARF 第一版发布于 1992 年,主要是为UNIX下的调试器提供必要的调试信息,例如PC地址对应的文件名及行号等信息,以方便源码级调试
  • 其包含足够的信息以供调试器完成特定的一些功能, 例如显示当前栈帧(Stack Frame)下的局部变量, 尝试修改一些变量, 直接跳到函数末尾等
  • 有足够的可扩展性,可为多种语言提供调试信息: 如: Ada, C, C++, Fortran, Java, Objective C, Go, Python, Haskell …
  • 除了编译/调试器外,还可用于从运行时地址还原源码对应的符号|行号的工具(如: atos)

目前 DWARF 已经在类UNIX系统中逐步替换 stabs(symbol table strings)成为主流的调试信息格式。
使用GCC或者LLVM系列编译器都可以很方便生成DWARF调试信息。

下面是其发展历史:

DWARF版本   年份    组织                                  详情
----------- ------ ------------------------------------- ----------------------------------------------------------------------------------------------
DWARF1 1992 Unix SVR4, PLSIG, UnixInternational 贝尔实验室 1988 年开发用于SVR4, 用于C compiler & SDB, PLSIG 和 Unix International在1992年为其编写了文档, 缺点: 结构不紧凑
DWARF2 1993 PLSIG PLSIG 做了一些优化(主要是减少调试信息大小),于1993年发布了DWARF 2 草案。但是由于摩托罗拉的 88000 处理器出现了一些严重故障,导致了一家名为 Open88 的公司破产, 而 Open88 公司是 PLSIG 和 UnixInternational 的金主, 随后这两个组织随即解散, DWARF 2 正式标准就从未发布。
DWARF3 2005 Free Standards Group 因为在 HP/Intel IA­64 architecture 架构及 C++ ABI 方面有更好的表现, Free Standards Group 在 2003 年完成了草案并在2005年发布了正式标准
DWARF4 2010 DWARF Debugging Format Committee Free Standards Group 和 Open Source Development Labs 在 2007 年合并为了著名的 Linux Foundation, DWARF委员会随后以独立方式存在并创建了网站 dwarfstd.org, 进行了一系列的更新后(支持LVIM, 对 Type Description存储的优化等),于2010 年发布 DWARF4
DWARF5 2017 DWARF Debugging Format Committee 在经过六年的开发后, DWARF委员会于 2017 年发布了 DWARF5 标准, 主要特性有: 更优的数据压缩, 调试信息从可执行文件的分离, 对宏定义有更好支持, 更快的搜索符号(symbol)

下面列出DWARF的一些竞品,方便大家更了解调试格式的发展

格式                                年份      典型使用环境
---------------------------------- --------- --------------------------------------------------------------
stabs (symbol table strings) 1981 广泛使用于 Unix 环境, 目前已经逐步被 DWARF 替换
COFF (Common Object File Format) 1983 UNIX System V(AT&T), AIX(IBM, XCOFF), DEC、SGI(ECOFF)
IEEE­695 1990 虽是 IEEE 标准, 但是支持少数几种处理器架构, 目前已经基本消亡
PE­COFF (PE/COFF) 1995(?) Windows PE, Windows NT, COFF 最流行的变种
OMF (Object Module Format) 1995(?) CP/M, DOS, OS/2, embedded systems
PDB(Program database) ? Windows, Microsoft Visual Studio 的默认调试格式

我们为什么需要 DWARF ?

上面我们提到DWARF主要为调试器(debugger)服务,我们通过实现一个调试器Demo,帮助大家了解整个技术链。

首先,我们看下一般调试器的样子:

这个简单的调试器只完成一个功能: 打印被调试程序运行时执行的所有指令

下面的例子使用了 Linux ptrace系统调用,
虽然我很想用 Mac 做演示, 但是 ptrace(OS X) 在Mac上已经被阉割了, 缺少 GETREGS 这种关键的 ptrace 功能,
而使用 thread_get_state() 又过于复杂, 所以 …

体验编写一个调试器-Demo

  • 启动 Linux 容器(如果本地没有Linux环境)

    docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -it harisekhon/ubuntu-dev /bin/bash
  • 创建工作目录, 创建 被调试程序hello.c

    mkdir ptrace-example-mac; cd ptrace-example-mac

    cat > hello.c <<EOF
    #include <stdio.h>

    int main() {
    printf("Hello, world!?
    ");
    return 0;
    }
    EOF
  • 创建调试器Demo main.c

    cat > main.c <<EOF
    #include <stdio.h>
    #include <stdarg.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <sys/types.h>
    #include <sys/ptrace.h>
    #include <sys/wait.h>
    #include <sys/user.h>
    #include <unistd.h>

    void procmsg(const char* format, ...);
    void run_target(const char* programname);
    void run_debugger(pid_t child_pid);

    int main(int argc, char** argv)
    {
    pid_t child_pid;

    if (argc < 2) {
    fprintf(stderr, "Expected a program name as argument");
    return -1;
    }

    child_pid = fork();
    if (child_pid == 0)
    run_target(argv[1]);
    else if (child_pid > 0)
    run_debugger(child_pid);
    else {
    perror("fork");
    return -1;
    }

    return 0;
    }

    /* Print a message to stdout, prefixed by the process ID
    */
    void procmsg(const char* format, ...)
    {
    va_list ap;
    fprintf(stdout, "[%d] ", getpid());
    va_start(ap, format);
    vfprintf(stdout, format, ap);
    va_end(ap);
    }

    void run_target(const char* programname)
    {
    procmsg("target started. will run '%s'", programname);

    /* Allow tracing of this process */
    if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {
    perror("ptrace");
    return;
    }

    /* Replace this process's image with the given program */
    execl(programname, programname, (char *)0);
    }

    void run_debugger(pid_t child_pid)
    {
    int wait_status;
    unsigned icounter = 0;
    procmsg("debugger started");

    /* Wait for child to stop on its first instruction */
    wait(&wait_status);

    while (WIFSTOPPED(wait_status)) {
    icounter++;
    struct user_regs_struct regs;
    ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
    unsigned instr = ptrace(PTRACE_PEEKTEXT, child_pid, regs.rip, 0);

    procmsg("icounter = %u. IP = 0x%08x. instr = 0x%08x",
    icounter, regs.rip, instr);

    /* Make the child execute another instruction */
    if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) {
    perror("ptrace");
    return;
    }

    /* Wait for child to stop on its next instruction */
    wait(&wait_status);
    }

    procmsg("the child executed %u instructions", icounter);
    }
    EOF
  • 编译

    gcc -O0 hello.c -o hello    # 编译被调试程序
    gcc main.c -o ptrace-example-linux # 编译调试器 demo
  • 编译完成后工作目录如下:

    [email protected]:/ptrace-example-mac# ll
    total 40
    drwxr-xr-x 2 root root 4096 Apr 21 05:19 ./
    drwxr-xr-x 1 root root 4096 Apr 21 05:14 ../
    -rwxr-xr-x 1 root root 8600 Apr 21 05:18 hello*
    -rw-r--r-- 1 root root 81 Apr 21 05:17 hello.c
    -rw-r--r-- 1 root root 1888 Apr 21 05:17 main.c
    -rwxr-xr-x 1 root root 9248 Apr 21 05:19 ptrace-example-linux*
  • 执行

    ./ptrace-example-linux hello    # 执行 Hello 并打印Hello运行过程中执行的指令数

    运行结果如下:

    ...
    [60] icounter = 141731. IP = 0x3818271a. instr = 0x00e7b841
    [60] icounter = 141732. IP = 0x38182720. instr = 0x00003cbe
    [60] icounter = 141733. IP = 0x38182725. instr = 0x0f6619eb
    [60] icounter = 141734. IP = 0x38182740. instr = 0x44d78948
    [60] icounter = 141735. IP = 0x38182743. instr = 0x0fc08944
    [60] icounter = 141736. IP = 0x38182746. instr = 0x3d48050f
    [60] the child executed 141736 instructions

调试器总结

从上面的调试器Demo执行的结果及源码看, 调试器从 ptrace系统调用拿到的信息有

  • IP: 指令指针寄存器, 表示CPU当前执行的指令在内存中的地址
  • instruction: 当前CPU执行的指令内容
  • regs: 部分寄存器
  • PEEKTEXT: 可在程序内存空间取值

但从我们平时使用的调试器提供给我们更多的信息:

  • 当前程序执行指令对应于源码的文件及行号
  • 当前程序栈帧(stack frame|activation record) 下的局部变量

调试器如何从一些十分基础的信息,例如 IP(指令地址),
呈现给我们如此丰富的调试信息呢?

那便我们为什么需要 DWARF, 其提供了程序运行时信息(Runtime)到源码信息的映射(Source File)

IP|regs|address      >>>  Source File
(Runtime)           DWARF    

认识 DWARF

使用 GCC 生成 DWARF 调试信息

认识 DWARF 的第一步便是如何生成 DWARF 信息, 所幸这个过程非常简单

  • 对于 GCC 及 CLang 编译器, 使用参数 -gdwarf-4 即可生成 DWARF4 调试信息

我们使用一个名为 foo.c 的示例程序来演示 生成并探索 DWARF 内容

  • 创建工作目录及 foo.c 文件

    mkdir show-dwarf; cd show-dwarf;

    cat > foo.c <<EOF
    int foo(int a, int b) {
    int c;
    static double d = 5.0;
    c = a + b;
    return c;
    }

    int main() {
    int r;
    r = foo(2, 3);
    return 0;
    }
    EOF
  • 使用 GCC 编译并生成 DWARF4 编译信息

    gcc -O0 -gdwarf-4 foo.c -o foo

    编译完成后, 会发现多了 foo.dSYM 的目录, 当前工作目录文件列表如下:

    masterkang: ~/Documents/show-dwarf > ll
    total 32
    -rwxr-xr-x 1 masterkang staff 8728 4 21 13:58 foo
    -rw-r--r-- 1 masterkang staff 153 4 21 13:57 foo.c
    drwxr-xr-x 3 masterkang staff 96 4 21 13:58 foo.dSYM
  • 使用 lldb 调试 foo 并观察 foo.dSYM 是否起作用

    1. lldb foo 启动 lldb 并设置被调试程序为 foo
    2. 在 lldb 交互命令输入: b foo 设置函数 foo 为断点
    3. 在 lldb 交互命令输入: run 开始运行被调试程序

    调试器稍后之后会在 foo 函数停下来并且打印出对应的源码位置

    masterkang: ~/Documents/show-dwarf > PATH=/usr/bin lldb foo
    (lldb) target create "foo"
    Current executable set to 'foo' (x86_64).
    (lldb) b foo
    Breakpoint 1: where = foo`foo + 10 at foo.c:4, address = 0x0000000100000f6a
    (lldb) run
    Process 96385 launched: '/Users/masterkang/Documents/show-dwarf/foo' (x86_64)
    Process 96385 stopped
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100000f6a foo`foo(a=2, b=3) at foo.c:4
    1 int foo(int a, int b) {
    2 int c;
    3 static double d = 5.0;
    -> 4 c = a + b;
    5 return c;
    6 }
    7
    Target 0: (foo) stopped.
    (lldb)
  • 进入到 foo.dSYM 目录并且找到调试信息文件 foo

    cd foo.dSYM/Contents/Resources/DWARF

    masterkang: ~/Documents/show-dwarf/foo.dSYM/Contents/Resources/DWARF > ll
    total 24
    -rw-r--r-- 1 masterkang staff 9166 4 21 13:58 foo
  • 使用 file 命令查看 foo 文件描述

    masterkang: ~/Documents/show-dwarf/foo.dSYM/Contents/Resources/DWARF > file foo
    foo: Mach-O 64-bit dSYM companion file x86_64

    从描述看, 可以看到这是一个 Mach-O 文件

  • 既然是 Mach-O 文件, 使用 size 命令查看 foo 可执行文件包含的 Segment 和 Section

    masterkang: ~/Documents/show-dwarf/foo.dSYM/Contents/Resources/DWARF > size -x -m -l foo
    Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
    Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
    Section __text: 0x4b (addr 0x100000f60 offset 0)
    Section __unwind_info: 0x48 (addr 0x100000fac offset 0)
    total 0x93
    Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 0)
    Section __data: 0x8 (addr 0x100001000 offset 0)
    total 0x8
    Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 4096)
    Segment __DWARF: 0x1000 (vmaddr 0x100003000 fileoff 8192)
    Section __debug_line: 0x68 (addr 0x100003000 offset 8192)
    Section __debug_pubnames: 0x29 (addr 0x100003068 offset 8296)
    Section __debug_pubtypes: 0x25 (addr 0x100003091 offset 8337)
    Section __debug_aranges: 0x40 (addr 0x1000030b6 offset 8374)
    Section __debug_info: 0xba (addr 0x1000030f6 offset 8438)
    Section __debug_abbrev: 0x78 (addr 0x1000031b0 offset 8624)
    Section __debug_str: 0x78 (addr 0x100003228 offset 8744)
    Section __apple_names: 0x74 (addr 0x1000032a0 offset 8864)
    Section __apple_namespac: 0x24 (addr 0x100003314 offset 8980)
    Section __apple_types: 0x72 (addr 0x100003338 offset 9016)
    Section __apple_objc: 0x24 (addr 0x1000033aa offset 9130)
    total 0x3ce
    total 0x100004000

    可以看到有一个名为 __DWARF 的 Segment, 下面包含 __debug_line,__debug_pubnames, __debug_pubtypes … 等Section。
    这些 Section 便是 DWARF 在 .dSYM 中的存储方式,如何观察这些 Section的内容呢?

  • 使用 dwarfdump 探索 DWARF 内容

    输入命令 dwarfdump foo --debug-info 可展示 __debug_line Section 下的内容(非原始内容, 已经经过格式化处理方便查看)
    下一小节我们会详细说明 DWARF info 段

    下面的这段内容我们会反复引用,下文称之为 DWARF info 示例

    masterkang: ~/Documents/show-dwarf/foo.dSYM/Contents/Resources/DWARF > dwarfdump foo --debug-info
    ----------------------------------------------------------------------
    File: foo (x86_64)
    ----------------------------------------------------------------------
    .debug_info contents:

    0x00000000: Compile Unit: length = 0x000000b6 version = 0x0004 abbr_offset = 0x00000000 addr_size = 0x08 (next CU at 0x000000ba)

    0x0000000b: TAG_compile_unit [1] *
    AT_producer( "Apple LLVM version 9.1.0 (clang-902.0.39.1)" )
    AT_language( DW_LANG_C99 )
    AT_name( "foo.c" )
    AT_stmt_list( 0x00000000 )
    AT_comp_dir( "/Users/masterkang/Documents/show-dwarf" )
    AT_low_pc( 0x0000000100000f60 )
    AT_high_pc( 0x0000004b )

    0x0000002a: TAG_subprogram [2] *
    AT_low_pc( 0x0000000100000f60 )
    AT_high_pc( 0x00000018 )
    AT_frame_base( rbp )
    AT_name( "foo" )
    AT_decl_file( "foo.c" )
    AT_decl_line( 1 )
    AT_prototyped( true )
    AT_type( {0x000000b2} ( int ) )
    AT_external( true )

    0x00000043: TAG_variable [3]
    AT_name( "d" )
    AT_type( {0x00000083} ( double ) )
    AT_decl_file( "foo.c" )
    AT_decl_line( 3 )
    AT_location( [0x0000000100001000] )

    0x00000058: TAG_formal_parameter [4]
    AT_location( fbreg -4 )
    AT_name( "a" )
    AT_decl_file( "foo.c" )
    AT_decl_line( 1 )
    AT_type( {0x000000b2} ( int ) )

    0x00000066: TAG_formal_parameter [4]
    AT_location( fbreg -8 )
    AT_name( "b" )
    AT_decl_file( "foo.c" )
    AT_decl_line( 1 )
    AT_type( {0x000000b2} ( int ) )

    0x00000074: TAG_variable [5]
    AT_location( fbreg -12 )
    AT_name( "c" )
    AT_decl_file( "foo.c" )
    AT_decl_line( 2 )
    AT_type( {0x000000b2} ( int ) )

    0x00000082: NULL

    0x00000083: TAG_base_type [6]
    AT_name( "double" )
    AT_encoding( DW_ATE_float )
    AT_byte_size( 0x08 )

    0x0000008a: TAG_subprogram [7] *
    AT_low_pc( 0x0000000100000f80 )
    AT_high_pc( 0x0000002b )
    AT_frame_base( rbp )
    AT_name( "main" )
    AT_decl_file( "foo.c" )
    AT_decl_line( 8 )
    AT_type( {0x000000b2} ( int ) )
    AT_external( true )

    0x000000a3: TAG_variable [5]
    AT_location( fbreg -8 )
    AT_name( "r" )
    AT_decl_file( "foo.c" )
    AT_decl_line( 9 )
    AT_type( {0x000000b2} ( int ) )

    0x000000b1: NULL

    0x000000b2: TAG_base_type [6]
    AT_name( "int" )
    AT_encoding( DW_ATE_signed )
    AT_byte_size( 0x04 )

    0x000000b9: NULL

DWARF 核心: info section

info section 是DWARF的核心,其用来描述程序结构

如果让你去描述上文我们使用的 foo.c 你会怎么表达呢 ?

  • 这是一个C语言程序
  • 包含一个名为 foo 的函数, 这个函数有两个形式参数, 有符号整型 a 和 有符号整型 b
  • foo 函数内声明了一个局部变量: 有符号整型 c, 一个全局变量 d, 其初始化值为 5.0
  • foo 函数返回值为局部变量 c 的值, 类型为有符号整型
  • 包含一个名为 main 的函数, 其调用了 foo 函数并把返回值存储在了名为 r的局部变量
  • main 函数返回值为有符号整型 0
  • foo 函数位于 foo.c 第 1 行
  • main 函数位于 foo.c 第 8 行

是不是感觉整个描述很繁琐并且有很多种类型的东西需要描述?
包括函数、形式参数、局部变量、全局变量甚至还有每个函数位于代码的什么位置。

为此 DWARF 提出了 The Debugging Information Entry (DIE) 来以统一的形式描述这些信息,详见: 「DWARF4 2.1」每个DIE包含:

  • 一个 TAG 属性表达描述什么类型的东西, 如: TAG_subprogram(函数)、TAG_formal_parameter(形式参数)、TAG_variable(变量)、TAG_base_type(基础类型)\ 所有的 TAG 列表位于: 「DWARF4 Figure 1. Tag names」

  • N 个属性(attribute), 用于具体描述一个DIE, 例如 DWARF info 示例 中对函数 foo 的描述:

    0x0000002a:     TAG_subprogram [2] *
    AT_low_pc( 0x0000000100000f60 )
    AT_high_pc( 0x00000018 )
    AT_frame_base( rbp )
    AT_name( "foo" )
    AT_decl_file( "foo.c" )
    AT_decl_line( 1 )
    AT_prototyped( true )
    AT_type( {0x000000b2} ( int ) )
    AT_external( true )

    AT_low_pc, AT_high_pc 分别代表函数的 起始/结束 PC地址
    AT_frame_base 表达函数的栈帧基址(frame base) 为寄存器 rbp 的值
    AT_name 描述函数的名字为 foo
    AT_decl_file 说这个函数在 foo.c 文件中声明
    AT_decl_line 说这个函数在 foo.c 第一行声明
    AT_prototyped 为一个 Bool 值, 为 True时代表这是一个子程序/函数(subroutine)
    AT_type 属性描述这个函数返回值的类型是什么, 对于 foo 函数来说, 为int
    AT_external Bool值, 这个函数是否为全局可访问

值得注意的是:

  • DWARF 只有有限种类的属性, 全部属性的列表位于: 「DWARF4 Figure 2. Attribute names」
  • AT_low_pc 和 AT_high_pc 描述的机器码地址 不等价于程序在运行时的地址,操作系统处于安全因素, 会应用一种地址空间布局随机化的技术Address Space LayoutRandomization(ASLR),加载可执行文件到内存时,会做一个随机偏移(slide),我们获取到偏移后便可还原运行时地址到DWARF地址

组合使用 DIE 就可以描述整个程序,就像搭乐高积木一样(可能需要更多的技巧)

事实上, 类似于编译器语法树,DWARF 使用 Tree 作为组合 DIE 的方式

一个 DIE 可以包含几个 子DIE, 正如一个文件可以有 N 个函数,
一个函数可以包含 X 个形式参数和 Y 个局部变量
DWARF info 示例 也可以看出, DIE
间用空行分割,并且为缩进分明的树形结构。

DWARF4 标准阅读指引

如果你觉得 DWARF 只是用来描述下你的程序有哪些函数和变量,那就 Too
Young, Too Naive ~

DWARF4 标准从这里可以获取 DWARF4.pdf,
建议下载到本地慢慢读DWARF5 也已经开始得到编译器支持, 详情见 DWART Stand

DWARF4 标准全文 300页左右, 其中正文到 189页,剩余为附录

重要的内容前面有 【重要】标签提示

1. INTRODUCTION...........................................1
介绍 DWARF 目标适用范围,版本变更细节

2. GENERAL DESCRIPTION ...................................7
DWARF 基础概念的描述

2.1 THE DEBUGGING INFORMATION ENTRY (DIE) ............7
【重要】描述了 DIE 的概念

2.2 ATTRIBUTE TYPES ..................................7
【重要】描述了 Atrribute 的概念, 列出了所有的 Attibute, 以及 Attribute 的值可以拥有哪些数据类型
「DWARF4 Figure 3. Classes of attribute value」

2.3 RELATIONSHIP OF DEBUGGING INFORMATION ENTRIES ...16
DIE 间的关系, 为树形结构

2.4 TARGET ADDRESSES.................................16
关于目标机器地址为32或者是64位的一些描述

2.5 DWARF EXPRESSIONS ...............................17
【重要】DWARF 定义出了一种专用的表达式(Expressions), 由操作码和操作数序列组成一个表达式, 最终可计算出想要表达的值。
主要为了下一节 LOCATION DESCRIPTIONS 服务

2.6 LOCATION DESCRIPTIONS............................25
【重要】描述如何计算函数 `stack frame` | `return address` 的值
利用DWARF Expression 来描述 Location, 甚至可以满足下面的情况: 生命周期中,对象的地址甚至是可以改变的
包括: Single location descriptions 和 Location lists 两种

2.7 TYPES OF PROGRAM ENTITIES .......................32
DW_AT_type 属性的描述

2.8 ACCESSIBILITY OF DECLARATIONS....................32
DW_AT_accessibility 属性的描述

2.9 VISIBILITY OF DECLARATIONS.......................33
DW_AT_visibility 属性的描述

2.10 VIRTUALITY OF DECLARATIONS .....................33
DW_AT_virtuality 属性的描述(目前看来仅适用于 C++虚函数)

2.11 ARTIFICIAL ENTRIES..............................34
DW_AT_artificial 属性的描述, 为一个 flag, 当一个对象|类型是由编译器而不是源代码生成时, 这个flag为 True

2.12 SEGMENTED ADDRESSES ............................34
【重要】DW_AT_segment, 一些系统的内存地址可能不是一个平整的地址空间,而是 Segment基址加上一个Offset
当 DW_AT_segment 出现时, 任何计算结果为地址的属性,例如 DW_AT_low_pc, DW_AT_high_pc, DW_AT_ranges or DW_AT_entry_pc
计算出来的值都需要加上 DW_AT_segment 计算所得到的值

2.13 NON-DEFINING DECLARATIONS AND COMPLETIONS ......35
DWARF 中大多是DIE都是用来描述一个对象的定义(Define),但是C语言允许使用 extern 关键字描述这个变量在其他地方定义,
这里仅仅声明(NON-DEFINING DECLARATIONS)一下,这时候 DW_AT_declaration 属性作为 flag 说明这是否为一个非定义声明

2.14 DECLARATION COORDINATES ........................36
【重要】描述对象在源码中的声明位置,包括属性: DW_AT_decl_file, DW_AT_decl_line, DW_AT_decl_column

2.15 IDENTIFIER NAMES ...............................36
【重要】DW_AT_name 的描述, 关于变量的名字, 函数的名字之类的

2.16 DATA LOCATIONS AND DWARF PROCEDURES.............37
【重要】DW_AT_location 属性的描述, 其值为一个 DWARF 表达式,通常用来描述变量或者形式参数的Location

2.17 CODE ADDRESSES AND RANGES ......................37
【重要】很多实体(entry), 如函数, 编译单元,代码块,Try/Catch 等都可以映射到机器码地址(地址范围),
DW_AT_low_pc, DW_AT_high_pc, DW_AT_ranges, DW_AT_start_scope 属性用来描述这些信息

2.18 ENTRY ADDRESS ..................................40
对于有一个地址范围的实体,DW_AT_entry_pc 属性描述执行入口地址在哪里, 如果没有声明, 那么 DW_AT_low_pc 便是入口地址

2.19 STATIC AND DYNAMIC VALUES OF ATTRIBUTES ........40
描述了一些属性的值可能是静态,也可能是动态的情况

2.20 ENTITY DESCRIPTIONS.............................41
DW_AT_description 属性的描述, 用来对实体的注解性描述

2.21 BYTE AND BIT SIZES..............................41
【重要】用来描述实体所需的存储大小, DW_AT_byte_size|DW_AT_bit_size 分别以字节和比特作为单位,
DW_AT_byte_stride|DW_AT_bit_stride 用于描述Array一个元素占用存储大小

2.22 LINKAGE NAMES ..................................41
【重要】因为一些语言,例如C++,允许多个实体使用相同的名字(如参数个数不同的话,函数名字可以一样),但在链接(Link)期间,必须保证没有相同的名字,
所以这些语言的编译器会使用一种成为符号重整(mangled names)的方式来对名字做一些格式化来满足不重名的要求。
DW_AT_linkage_name 用来记录重整过后的名字

3. PROGRAM SCOPE ENTRIES.................................43
程序不同层级实体的描述(编译、模块、函数),但是不包括类型。简单来说,就是存在于二进制程序中 TEXT 段的那些实体

3.1 UNIT ENTRIES......................................43
【重要】Unit Entries 是处在食物链顶端的 DIE (/偷笑),一个可执行文件通常包含一个或多个编译单元(compilation unit),后面会细说

3.2 MODULE, NAMESPACE AND IMPORTING ENTRIES ..........48
DW_TAG_module 为编程语言 module 实体的TAG(如Python), DW_TAG_namespace 是命名空间的TAG(C++)
DW_TAG_imported_declaration | DW_TAG_imported_module 说明此实体被其他地方引用

3.3 SUBROUTINE AND ENTRY POINT ENTRIES ...............53
【重要】描述了大多数语言都会支持的子程序/函数(Subroutine)实体,不同语言差异性比较大... 这部分描述用了整整 11 页

3.4 LEXICAL BLOCK ENTRIES.............................65
语句块(lexical block) 实体相关描述, 语句块可能会有关联的地址范围属性,如 DW_AT_low_pc、DW_AT_high_pc 或者是 DW_AT_ranges

3.5 LABEL ENTRIES ....................................65
LABEL 语句相关描述,TAG 为 DW_TAG_label, label 一般和 goto 配合使用。

3.6 WITH STATEMENT ENTRIES............................66
WITH 语句相关描述,TAG 为 DW_TAG_with_stmt,Pascal / Modula-2 支持 with 语句, 但是这个 with 和 Python 中的 with 是完全不一样的哦

3.7 TRY AND CATCH BLOCK ENTRIES ......................66
try ... catch ... 语句相关描述, TAG 为 DW_TAG_try_block

4. DATA OBJECT AND OBJECT LIST ENTRIES ..................69
程序不可分割的数据实体描述,例如变量、形式参数、常量

4.1 DATA OBJECT ENTRIES...............................69
【重要】对 DW_TAG_variable、DW_TAG_formal_parameter、DW_TAG_constant 相关属性的描述

4.2 COMMON BLOCK ENTRIES..............................73
看起来是 Fortran 的独有特性, fortran 允许一些变量不经形式参数传递到另一个编译单元的函数中,
使用 COMMON 语句标识出这些变量即可,类似于 C 语言定义在函数外边的全局变量

4.3 NAMELIST ENTRIES .................................73
看起来是 Fortran 的独有特性, 类似于 C 语言 struct,组合多个变量方便使用

5. TYPE ENTRIES .........................................75
描述了程序中经常用到的数据类型

5.1 BASE TYPE ENTRIES ................................75
【重要】编程语言定义的基础数据类型, TAG 为 DW_TAG_base_type,描述了可用于描述 Type 的各个属性

5.2 UNSPECIFIED TYPE ENTRIES..........................80
也没看懂到底啥意思,举的例子关于如何描述C语言的变量修饰符。

5.3 TYPEDEF ENTRIES ..................................82
C/C++ 中的 typedef 语句, TAG 为 DW_TAG_typedef

5.4 ARRAY TYPE ENTRIES ...............................83
...
5.15 TEMPLATE ALIAS ENTRIES ..........................103
各种数据类型的描述,如 ARRAY、STRUCT、CLASS、INTERFACE、SUBROUTINE ...

6. OTHER DEBUGGING INFORMATION...........................105
未表示为DIE的调试信息, 即不存储于 `info` 和 `types` section

6.1 ACCELERATED ACCESS ...............................105
【重要】快捷查找符号/Address的能力,包括:
- Lookup by Name: 快速查找全局对象/变量对应的编译单元, 存储于 `pubnames` & `pubtypes` section
- Lookup by Address: 快速定位地址对应的编译单元, 存储于 `aranges` section

6.2 LINE NUMBER INFORMATION ..........................108
【重要】这可谓是相当重要啊,源码级调试就靠它了,存储在 `info` section
DWARF为了节省需要的存储,使用了特殊定义的状态机(Line Number Program)
来生成LINE TABLE,完成 Address 到文件名、行号的映射

6.3 MACRO INFORMATION.................................123
C/C++ 宏定义相关的描述

6.4 CALL FRAME INFORMATION ...........................126
【重要】调用栈相关信息,存储在 `frame` section,典型应用场景为 异常发生时堆栈回溯(stack unwinding)

7. DATA REPRESENTATION ..................................139
这一节描述如何将调试信息序列化为二进制文件

APPENDIX A -- ATTRIBUTES BY TAG VALUE (INFORMATIVE)......191
【重要】描述了不同 DIE 一般会拥有的属性

APPENDIX B -- DEBUG SECTION RELATIONSHIPS (INFORMATIVE)..213
【重要】描述了不同DWARF section 之间的关系

APPENDIX C -- VARIABLE LENGTH DATA: ENCODING/DECODING (INFORMATIVE)...217
LEB128 编码算法

APPENDIX D -- EXAMPLES (INFORMATIVE) ....................219
各种示例

重要的有下面几个

D.1 COMPILATION UNITS AND ABBREVIATIONS TABLE EXAMPLE ...219
D.5 LINE NUMBER PROGRAM EXAMPLE .........................237
D.6 CALL FRAME INFORMATION EXAMPLE ......................239

APPENDIX E -- DWARF COMPRESSION AND DUPLICATE ELIMINATION (INFORMATIVE)..63
【重要】DWARF 压缩及减少冗余的措施

DWARF Sections

DWARF 各个 section 可以有独立的版本号, 下面是各个 Section 及其在不同 DWARF 版本中对应的版本号

Section 间的关系如下:

info & types 是毫无疑问的宇宙中心,描述了程序结构已经类型声明。
为了支持快速 Name/Type/Address 的查找,pubnames & pubtypes & aranges 可快速定位到编译单元
为了减少 info 段所需存储,abbrev 统一描述了不同 DIE 所包含的属性,info仅需要引用 abbrev 中的 id 就好
为了减少 info 段所需存储,str用于复用字符串,避免字符串重复,DW_FORM_strp
loc 存储了 Location 表达式,DW_AT_location
ranges 存储了 地址范围表达式,DW_AT_ranges
macinfo 存储了 宏定义相关的信息,DW_AT_macinfo
line 存储了行号相关信息, DW_AT_stmt_list 属性指向编译单元对应的Line Table

开发 DWARF 相关工具

如果你不是编译器/调试器开发者,了解 DWARF格式的重要目的一般是为了开发一些工具,完成程序运行时到源码之间的映射。例如:

  • atos: 找到程序运行时的内存地址对应的文件名,行号及所在函数名
  • atosl: atos 的 Linux 实现, 由 facebook 开发

另外, 有一些领域可以预见到DWARF信息会提供一些帮助,例如:
静态代码扫描(static code analysis), 二进制代码性能分析(performance analysis of binary)

下面给出了一些开发相关的库:

  • libdwarf, 一个 C语言库,用来解析 DWARF 格式,被适用于 atosl 中
  • libdwarf/libdwarf2.1.pdf, libdwarf 的使用文档
  • dwarfdump, 一个程序, 用于探索DWARF内容
  • golang debug/dwarf, Golang 标准库 debug/dwarf 提供了简单的 DWARF解析能力
  • LLVM-DWARF:
    LLVM 项目中与 DWARF 相关的部分,可了解编译器生成DWARF信息的细节

作为一个Golang 使用 debug/dwarf 的例子, ParseFile 打印了 address对应的文件名及行号

package dwarfexample

import (
"debug/macho"
"debug/dwarf"
"log"
"github.com/go-errors/errors"
)

func ParseFile(path string, address int64) (err error) {
var f *macho.FatFile
if f, err = macho.OpenFat(path); err != nil {
return errors.New("open file error: " + err.Error())
}

var d *dwarf.Data
if d, err = f.Arches[1].DWARF(); err != nil {
return
}

r := d.Reader()

var entry *dwarf.Entry
if entry, err = r.SeekPC(address); err != nil {
log.Print("Not Found ...")
return
} else {
log.Print("Found ...")
}

log.Printf("tag: %+v, lowpc: %+v", entry.Tag, entry.Val(dwarf.AttrLowpc))

var lineReader *dwarf.LineReader
if lineReader, err = d.LineReader(entry); err != nil {
return
}

var line dwarf.LineEntry

if err = lineReader.SeekPC(0x1005AC550, &line); err != nil {
return
}

log.Printf("line %+v:%+v", line.File.Name, line.Line)

return
}

DWARF 资源

下面是DWARF相关的资源链接: