https://linux.cn/article-8719-1.html

到目前为止,你已经偶尔听到了关于 dwarves、调试信息、一种无需解析就可以理解源码方式。今天我们会详细介绍源码级的调试信息,作为本指南后面部分使用它的准备。

系列文章索引

随着后面文章的发布,这些链接会逐渐生效。

  1. 准备环境
  2. 断点
  3. 寄存器和内存
  4. Elves 和 dwarves
  5. 源码和信号
  6. 源码级逐步执行
  7. 源码级断点
  8. 调用栈展开
  9. 读取变量
  10. 下一步

ELF 和 DWARF 简介

ELF 和 DWARF 可能是两个你没有听说过,但可能大部分时间都在使用的组件。ELF(Executable and Linkable Format,可执行和可链接格式)是 Linux 系统中使用最广泛的目标文件格式;它指定了一种存储二进制文件的所有不同部分的方式,例如代码、静态数据、调试信息以及字符串。它还告诉加载器如何加载二进制文件并准备执行,其中包括说明二进制文件不同部分在内存中应该放置的地点,哪些位需要根据其它组件的位置固定(重分配)以及其它。在这些博文中我不会用太多篇幅介绍 ELF,但是如果你感兴趣的话,你可以查看这个很好的信息图或该标准。

DWARF是通常和 ELF 一起使用的调试信息格式。它不一定要绑定到 ELF,但它们两者是一起发展的,一起工作得很好。这种格式允许编译器告诉调试器最初的源代码如何和被执行的二进制文件相关联。这些信息分散到不同的 ELF 部分,每个部分都衔接有一份它自己的信息。下面不同部分的定义,信息取自这个稍有过时但非常重要的 DWARF 调试格式简介:

  • .debug_abbrev .debug_info 部分使用的缩略语
  • .debug_aranges 内存地址和编译的映射
  • .debug_frame 调用帧信息
  • .debug_info 包括 DWARF 信息条目(DWARF Information Entries)(DIEs)的核心 DWARF 数据
  • .debug_line 行号程序
  • .debug_loc 位置描述
  • .debug_macinfo 宏描述
  • .debug_pubnames 全局对象和函数查找表
  • .debug_pubtypes 全局类型查找表
  • .debug_ranges DIEs 的引用地址范围
  • .debug_str .debug_info 使用的字符串列表
  • .debug_types 类型描述

我们最关心的是 .debug_line 和 .debug_info 部分,让我们来看一个简单程序的 DWARF 信息。

  1. int main() {
  2. long a = 3;
  3. long b = 2;
  4. long c = a + b;
  5. a = 4;
  6. }

DWARF 行表

如果你用 -g 选项编译这个程序,然后将结果传递给 dwarfdump 执行,在行号部分你应该可以看到类似这样的东西:

  1. .debug_line: line number info for a single cu
  2. Source lines (from CU-DIE at .debug_info offset 0x0000000b):
  3. NS new statement, BB new basic block, ET end of text sequence
  4. PE prologue end, EB epilogue begin
  5. IS=val ISA number, DI=val discriminator value
  6. <pc> [lno,col] NS BB ET PE EB IS= DI= uri: "filepath"
  7. 0x00400670 [ 1, 0] NS uri: "/home/simon/play/MiniDbg/examples/variable.cpp"
  8. 0x00400676 [ 2,10] NS PE
  9. 0x0040067e [ 3,10] NS
  10. 0x00400686 [ 4,14] NS
  11. 0x0040068a [ 4,16]
  12. 0x0040068e [ 4,10]
  13. 0x00400692 [ 5, 7] NS
  14. 0x0040069a [ 6, 1] NS
  15. 0x0040069c [ 6, 1] NS ET

前面几行是一些如何理解 dump 的信息 - 主要的行号数据从以 0x00400670 开头的行开始。实际上这是一个代码内存地址到文件中行列号的映射。NS 表示地址标记一个新语句的开始,这通常用于设置断点或逐步执行。PE 表示函数序言(LCTT 译注:在汇编语言中,function prologue 是程序开始的几行代码,用于准备函数中用到的栈和寄存器)的结束,这对于设置函数断点非常有帮助。ET 表示转换单元的结束。信息实际上并不像这样编码;真正的编码是一种非常节省空间的排序程序,可以通过执行它来建立这些行信息。

那么,假设我们想在 variable.cpp 的第 4 行设置断点,我们该怎么做呢?我们查找和该文件对应的条目,然后查找对应的行条目,查找对应的地址,在那里设置一个断点。在我们的例子中,条目是:

  1. 0x00400686 [ 4,14] NS

假设我们想在地址 0x00400686 处设置断点。如果你想尝试的话你可以在已经编写好的调试器上手动实现。

反过来也是如此。如果我们已经有了一个内存地址 - 例如说,一个程序计数器值 - 想找到它在源码中的位置,我们只需要从行表信息中查找最接近的映射地址并从中抓取行号。

DWARF 调试信息

.debug_info 部分是 DWARF 的核心。它给我们关于我们程序中存在的类型、函数、变量、希望和梦想的信息。这部分的基本单元是 DWARF 信息条目(DWARF Information Entry),我们亲切地称之为 DIEs。一个 DIE 包括能告诉你正在展现什么样的源码级实体的标签,后面跟着一系列该实体的属性。这是我上面展示的简单事例程序的 .debug_info 部分:

  1. .debug_info
  2. COMPILE_UNIT<header overall offset = 0x00000000>:
  3. < 0><0x0000000b> DW_TAG_compile_unit
  4. DW_AT_producer clang version 3.9.1 (tags/RELEASE_391/final)
  5. DW_AT_language DW_LANG_C_plus_plus
  6. DW_AT_name /super/secret/path/MiniDbg/examples/variable.cpp
  7. DW_AT_stmt_list 0x00000000
  8. DW_AT_comp_dir /super/secret/path/MiniDbg/build
  9. DW_AT_low_pc 0x00400670
  10. DW_AT_high_pc 0x0040069c
  11. LOCAL_SYMBOLS:
  12. < 1><0x0000002e> DW_TAG_subprogram
  13. DW_AT_low_pc 0x00400670
  14. DW_AT_high_pc 0x0040069c
  15. DW_AT_frame_base DW_OP_reg6
  16. DW_AT_name main
  17. DW_AT_decl_file 0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp
  18. DW_AT_decl_line 0x00000001
  19. DW_AT_type <0x00000077>
  20. DW_AT_external yes(1)
  21. < 2><0x0000004c> DW_TAG_variable
  22. DW_AT_location DW_OP_fbreg -8
  23. DW_AT_name a
  24. DW_AT_decl_file 0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp
  25. DW_AT_decl_line 0x00000002
  26. DW_AT_type <0x0000007e>
  27. < 2><0x0000005a> DW_TAG_variable
  28. DW_AT_location DW_OP_fbreg -16
  29. DW_AT_name b
  30. DW_AT_decl_file 0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp
  31. DW_AT_decl_line 0x00000003
  32. DW_AT_type <0x0000007e>
  33. < 2><0x00000068> DW_TAG_variable
  34. DW_AT_location DW_OP_fbreg -24
  35. DW_AT_name c
  36. DW_AT_decl_file 0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp
  37. DW_AT_decl_line 0x00000004
  38. DW_AT_type <0x0000007e>
  39. < 1><0x00000077> DW_TAG_base_type
  40. DW_AT_name int
  41. DW_AT_encoding DW_ATE_signed
  42. DW_AT_byte_size 0x00000004
  43. < 1><0x0000007e> DW_TAG_base_type
  44. DW_AT_name long int
  45. DW_AT_encoding DW_ATE_signed
  46. DW_AT_byte_size 0x00000008

第一个 DIE 表示一个编译单元(CU),实际上是一个包括了所有 #includes 和类似语句的源文件。下面是带含义注释的属性:

  1. DW_AT_producer clang version 3.9.1 (tags/RELEASE_391/final) <-- 产生该二进制文件的编译器
  2. DW_AT_language DW_LANG_C_plus_plus <-- 原编程语言
  3. DW_AT_name /super/secret/path/MiniDbg/examples/variable.cpp <-- 该 CU 表示的文件名称
  4. DW_AT_stmt_list 0x00000000 <-- 跟踪该 CU 的行表偏移
  5. DW_AT_comp_dir /super/secret/path/MiniDbg/build <-- 编译目录
  6. DW_AT_low_pc 0x00400670 <-- 该 CU 的代码起始
  7. DW_AT_high_pc 0x0040069c <-- 该 CU 的代码结尾

其它的 DIEs 遵循类似的模式,你也很可能推测出不同属性的含义。

现在我们可以根据新学到的 DWARF 知识尝试和解决一些实际问题。

当前我在哪个函数?

假设我们有一个程序计数器值然后想找到当前我们在哪一个函数。一个解决该问题的简单算法:

  1. for each compile unit:
  2. if the pc is between DW_AT_low_pc and DW_AT_high_pc:
  3. for each function in the compile unit:
  4. if the pc is between DW_AT_low_pc and DW_AT_high_pc:
  5. return function information

这对于很多目的都有效,但如果有成员函数或者内联(inline),就会变得更加复杂。假如有内联,一旦我们找到其范围包括我们的程序计数器(PC)的函数,我们需要递归遍历该 DIE 的所有孩子检查有没有内联函数能更好地匹配。在我的代码中,我不会为该调试器处理内联,但如果你想要的话你可以添加该功能。

如何在一个函数上设置断点?

再次说明,这取决于你是否想要支持成员函数、命名空间以及类似的东西。对于简单的函数你只需要迭代遍历不同编译单元中的函数直到你找到一个合适的名字。如果你的编译器能够填充 .debug_pubnames 部分,你可以更高效地做到这点。

一旦找到了函数,你可以在 DW_AT_low_pc 给定的内存地址设置一个断点。不过那会在函数序言处中断,但更合适的是在用户代码处中断。由于行表信息可以指定序言的结束的内存地址,你只需要在行表中查找 DW_AT_low_pc 的值,然后一直读取直到被标记为序言结束的条目。一些编译器不会输出这些信息,因此另一种方式是在该函数第二行条目指定的地址处设置断点。

假如我们想在我们示例程序中的 main 函数设置断点。我们查找名为 main 的函数,获取到它的 DIE:

  1. < 1><0x0000002e> DW_TAG_subprogram
  2. DW_AT_low_pc 0x00400670
  3. DW_AT_high_pc 0x0040069c
  4. DW_AT_frame_base DW_OP_reg6
  5. DW_AT_name main
  6. DW_AT_decl_file 0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp
  7. DW_AT_decl_line 0x00000001
  8. DW_AT_type <0x00000077>
  9. DW_AT_external yes(1)

这告诉我们函数从 0x00400670 开始。如果我们在行表中查找这个,我们可以获得条目:

  1. 0x00400670 [ 1, 0] NS uri: "/super/secret/path/MiniDbg/examples/variable.cpp"

我们希望跳过序言,因此我们再读取一个条目:

  1. 0x00400676 [ 2,10] NS PE

Clang 在这个条目中包括了序言结束标记,因此我们知道在这里停止,然后在地址 0x00400676 处设一个断点。

我如何读取一个变量的内容?

读取变量可能非常复杂。它们是难以捉摸的东西,可能在整个函数中移动、保存在寄存器中、被放置于内存、被优化掉、隐藏在角落里,等等。幸运的是我们的简单示例是真的很简单。如果我们想读取变量 a 的内容,我们需要看它的 DW_AT_location 属性:

  1. DW_AT_location DW_OP_fbreg -8

这告诉我们内容被保存在以栈帧基(base of the stack frame)偏移为 -8 的地方。为了找到栈帧基,我们查找所在函数的 DW_AT_frame_base 属性。

  1. DW_AT_frame_base DW_OP_reg6

从 System V x86_64 ABI 我们可以知道 reg6 在 x86 中是帧指针寄存器。现在我们读取帧指针的内容,从中减去 8,就找到了我们的变量。如果我们知道它具体是什么,我们还需要看它的类型:

  1. < 2><0x0000004c> DW_TAG_variable
  2. DW_AT_name a
  3. DW_AT_type <0x0000007e>

如果我们在调试信息中查找该类型,我们得到下面的 DIE:

  1. < 1><0x0000007e> DW_TAG_base_type
  2. DW_AT_name long int
  3. DW_AT_encoding DW_ATE_signed
  4. DW_AT_byte_size 0x00000008

这告诉我们该类型是 8 字节(64 位)有符号整型,因此我们可以继续并把这些字节解析为 int64_t 并向用户显示。

当然,类型可能比那要复杂得多,因为它们要能够表示类似 C++ 的类型,但是这能给你它们如何工作的基本认识。

再次回到帧基(frame base),Clang 可以通过帧指针寄存器跟踪帧基。最近版本的 GCC 倾向于使用 DW_OP_call_frame_cfa,它包括解析 .eh_frame ELF 部分,那是一个我不会去写的另外一篇完全不同的文章。如果你告诉 GCC 使用 DWARF 2 而不是最近的版本,它会倾向于输出位置列表,这更便于阅读:

  1. DW_AT_frame_base <loclist at offset 0x00000000 with 4 entries follows>
  2. low-off : 0x00000000 addr 0x00400696 high-off 0x00000001 addr 0x00400697>DW_OP_breg7+8
  3. low-off : 0x00000001 addr 0x00400697 high-off 0x00000004 addr 0x0040069a>DW_OP_breg7+16
  4. low-off : 0x00000004 addr 0x0040069a high-off 0x00000031 addr 0x004006c7>DW_OP_breg6+16
  5. low-off : 0x00000031 addr 0x004006c7 high-off 0x00000032 addr 0x004006c8>DW_OP_breg7+8

位置列表取决于程序计数器所处的位置给出不同的位置。这个例子告诉我们如果程序计数器是在 DW_AT_low_pc 偏移量为 0x0 的位置,那么帧基就在和寄存器 7 中保存的值偏移量为 8 的位置,如果它是在 0x1 和 0x4 之间,那么帧基就在和相同位置偏移量为 16 的位置,以此类推。

休息一会

这里有很多的信息需要你的大脑消化,但好消息是在后面的几篇文章中我们会用一个库替我们完成这些艰难的工作。理解概念仍然很有帮助,尤其是当出现错误或者你想支持一些你使用的 DWARF 库所没有实现的 DWARF 概念时。

如果你想了解更多关于 DWARF 的内容,那么你可以从这里获取其标准。在写这篇博客时,刚刚发布了 DWARF 5,但更普遍支持 DWARF 4。


via: https://blog.tartanllama.xyz/c++/2017/04/05/writing-a-linux-debugger-elf-dwarf/

开发一个 Linux 调试器(四):Elves 和 dwarves相关推荐

  1. found dwarf version #039;4#039; linux,开发一个Linux调试器(四):Elves和dwarves

    到目前为止,你已经偶尔听到了关于 dwarves.调试信息.一种无需解析就可以理解源码方式.今天我们会详细介绍源码级的调试信息,作为本指南后面部分使用它的准备. 系列文章索引 随着后面文章的发布,这些 ...

  2. linux如何调试elf程序,开发一个Linux调试器就需要了解ELF和DWARF

    到目前为止,可能你已经听到了关于调试信息或者关于除了解析代码以外的理解源代码的方法的DWARF的只言片语.今天,我们将介绍源代码级的调试信息的细节,以备在该系列的余下部分使用它. ELF和DWARF简 ...

  3. linux 源码 调试,开发一个Linux调试器(六):源码级逐步执行

    我们计算编写这些函数异常简单的版本,但真正的调试器有 thread plan 的概念,它封装了所有的单步信息.例如,调试器可能有一些复杂的逻辑去决定断点的地位,然后有一些回调函数用于断定单步操作是否完 ...

  4. linux内存地址断点,开发一个 Linux 调试器(三):寄存器和内存

    上一篇博文中我们给调试器添加了一个简单的地址断点.这次,我们将添加读写寄存器和内存的功能,这将使我们能够使用我们的程序计数器.观察状态和改变程序的行为. 注册我们的寄存器 在我们真正读取任何寄存器之前 ...

  5. Linux包含一个名称是()的调试程序,开发一个 Linux 调试器(九):处理变量

    变量是偷偷摸摸的.有时,它们会很高兴地呆在寄存器中,但是一转头就会跑到堆栈中.为了优化,编译器可能会完全将它们从窗口中抛出.无论变量在内存中的如何移动,我们都需要一些方法在调试器中跟踪和操作它们.这篇 ...

  6. 开发一个Linux调试器(八):堆栈展开

    有时你需要知道的最重要的信息是什么,你当前的程序状态是如何到达那里的.有一个 backtrace 命令,它给你提供了程序当前的函数调用链.这篇文章将向你展示如何在 x86_64 上实现堆栈展开以生成这 ...

  7. linux的静态编译elf无法调试,[翻译]自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB...

    自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB 在上一节中,你已经听说了DWARF调试格式,它是程序的调试信息,是一种可以更好理解源码的方式 ...

  8. 【Linux】Linux调试器--gdb详解

    Linux环境基础开发工具使用(二) 一.Linux调试器-gdb使用 1.背景 2.使用 二.Linux项目自动化构建工具-make/Makefile 1.背景 2.依赖关系和依赖方法 3.原理 4 ...

  9. 痞子衡嵌入式:飞思卡尔Kinetis开发板OpenSDA调试器那些事(上)- 背景与架构

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是飞思卡尔Kinetis MCU开发板板载OpenSDA调试器(上篇). 众所周知,嵌入式软件开发几乎离不开调试器,因为写一个稍有代码规模 ...

最新文章

  1. 9 单元测试中不得不知的概念
  2. [NOIP2007] 提高组 洛谷P1099 树网的核
  3. 批量插入使用SqlBulkCopy
  4. LeetCode:Add Two Numbers
  5. java md5.computehash_c# – ObjectDisposedException使用MD5 ComputeHash时
  6. oracle之数据处理
  7. 传统数据中心如何实现向云的平滑升级
  8. 指数有限的子群存在一个右陪集代表元系,同时也是左陪集代表元系
  9. 【转】如何在windows平台开发OpenGL程序使用OpenGL1.2或更高版本
  10. mysql 查询每个班级的前三名
  11. html5页面头部代码,HTML5标签:header元素的使用方法及作用
  12. 计算前复权和后复权价格?A股复权因子的使用
  13. java中指数形式的格式_java – 复数的指数形式
  14. RVM怎么下载和管理ruby版本 - 猿码设计师 ruby rvm
  15. 循环当中的continue用法
  16. multi-kernels、ALLOC与USE、Zero-Copy
  17. Excel 取消单元格合并,并且将空值填充
  18. 13.1 数状数组 ——【小朋友排队】
  19. wps 分节符(连续) 自动变成 分节符(下一页) 解决办法
  20. unfortunately activity has stopped

热门文章

  1. FET细解:FET(IGFET、JFET、MESFET)、IGFET(MOSFET/MISFET、HFET)、HFET(MODFET、HIGFET)
  2. 计算机专业-找工作相关经验
  3. EPICS ‘makeBaseApp’ IOC
  4. Win10前面板插口耳机无声音,无Realtek控制器,前置耳机孔无法使用解决方案!
  5. 一字节BCD码转ASCII码的算法及源码
  6. 2019年开户难,大陆居民如何在香港的银行开个人账户?
  7. 初二因式分解奥数竞赛题_(完整)初中数学竞赛因式分解专题
  8. HTML基于Vue实现Cron生成器
  9. EasyUI API
  10. node.js 最全命令行配置操作win10