GDB关联源代码

找到一篇很好的文章,透彻的讲述如何分析是否是debug二进制文件,如何调试、关联源代码。

 

如果你手头上有一个你自己或者别人开发的程序,但它有一些bug。或者你只是想知道这个程序是如何工作的。怎么办呢?你需要一个调试工具。

现在很少有人会直接对着汇编指令进行调试,通常情况下,大家都希望能对照着源代码进行调试。但是,你调试使用的主机,一般来说并不是构建程序的那台,因此你会看到如下这个令人沮丧的消息:

 
  1. $ gdb -q python3.7

  2. Reading symbols from python3.7...done.

  3. (gdb) l

  4. 6 ./Programs/python.c: No such file or directory.

我经常会看到这些报错信息,并且对于调试程序来说,这也非常重要。所以,我认为我们需要详细了解一下GDB是如何在调试会话中显示源代码的。

调试信息

首先,我们从调试信息开始。调试信息是由编译器生成的存在于二进制文件中的特殊段,供调试器和其他相关的工具使用。

在GCC中,有一个著名的-g标志用于生成调试信息。大多数使用某种构建系统的项目都会在构建时默认包含或者通过一些标志来添加调试信息。

例如,在CPython中,你需要执行以下命令:

 
  1. $ ./configure --with-pydebug

  2. $ make -j

-with-pydebug会在调用GCC时添加-g选项。

这个-g选项会生成二进制的调试段,并插入到程序的二进制文件中。调试段通常采用DWARF格式。对于ELF二进制文件来说,调试段的名称一般都是像.debug_ *这样的,例如 .debug_info或者.debug_loc。这些调试段使得调试程序成为可能,可以这么说,它是汇编级别的指令与源代码之间的映射。

要查看程序是否包含调试符号,你可以使用objdump命令列出二进制文件的所有段:

 
  1. $ objdump -h ./python

  2.  
  3. python: file format elf64-x86-64

  4.  
  5. Sections:

  6. Idx Name Size VMA LMA File off Algn

  7. 0 .interp 0000001c 0000000000400238 0000000000400238 00000238 2**0

  8. CONTENTS, ALLOC, LOAD, READONLY, DATA

  9. 1 .note.ABI-tag 00000020 0000000000400254 0000000000400254 00000254 2**2

  10. CONTENTS, ALLOC, LOAD, READONLY, DATA

  11. ...

  12. 25 .bss 00031f70 00000000008d9e00 00000000008d9e00 002d9dfe 2**5

  13. ALLOC

  14. 26 .comment 00000058 0000000000000000 0000000000000000 002d9dfe 2**0

  15. CONTENTS, READONLY

  16. 27 .debug_aranges 000017f0 0000000000000000 0000000000000000 002d9e56 2**0

  17. CONTENTS, READONLY, DEBUGGING

  18. 28 .debug_info 00377bac 0000000000000000 0000000000000000 002db646 2**0

  19. CONTENTS, READONLY, DEBUGGING

  20. 29 .debug_abbrev 0001fcd7 0000000000000000 0000000000000000 006531f2 2**0

  21. CONTENTS, READONLY, DEBUGGING

  22. 30 .debug_line 0008b441 0000000000000000 0000000000000000 00672ec9 2**0

  23. CONTENTS, READONLY, DEBUGGING

  24. 31 .debug_str 00031f18 0000000000000000 0000000000000000 006fe30a 2**0

  25. CONTENTS, READONLY, DEBUGGING

  26. 32 .debug_loc 0034190c 0000000000000000 0000000000000000 00730222 2**0

  27. CONTENTS, READONLY, DEBUGGING

  28. 33 .debug_ranges 00062e10 0000000000000000 0000000000000000 00a71b2e 2**0

  29. CONTENTS, READONLY, DEBUGGING

或者使用readelf命令:

 
  1. $ readelf -S ./python

  2. There are 38 section headers, starting at offset 0xb41840:

  3.  
  4. Section Headers:

  5. [Nr] Name Type Address Offset

  6. Size EntSize Flags Link Info Align

  7. [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0

  8. [ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1

  9.  
  10. ...

  11.  
  12. [26] .bss NOBITS 00000000008d9e00 002d9dfe

  13. 0000000000031f70 0000000000000000 WA 0 0 32

  14. [27] .comment PROGBITS 0000000000000000 002d9dfe

  15. 0000000000000058 0000000000000001 MS 0 0 1

  16. [28] .debug_aranges PROGBITS 0000000000000000 002d9e56

  17. 00000000000017f0 0000000000000000 0 0 1

  18. [29] .debug_info PROGBITS 0000000000000000 002db646

  19. 0000000000377bac 0000000000000000 0 0 1

  20. [30] .debug_abbrev PROGBITS 0000000000000000 006531f2

  21. 000000000001fcd7 0000000000000000 0 0 1

  22. [31] .debug_line PROGBITS 0000000000000000 00672ec9

  23. 000000000008b441 0000000000000000 0 0 1

  24. [32] .debug_str PROGBITS 0000000000000000 006fe30a

  25. 0000000000031f18 0000000000000001 MS 0 0 1

  26. [33] .debug_loc PROGBITS 0000000000000000 00730222

  27. 000000000034190c 0000000000000000 0 0 1

  28. [34] .debug_ranges PROGBITS 0000000000000000 00a71b2e

  29. 0000000000062e10 0000000000000000 0 0 1

  30. [35] .shstrtab STRTAB 0000000000000000 00b416d5

  31. 0000000000000165 0000000000000000 0 0 1

  32. [36] .symtab SYMTAB 0000000000000000 00ad4940

  33. 000000000003f978 0000000000000018 37 8762 8

  34. [37] .strtab STRTAB 0000000000000000 00b142b8

  35. 000000000002d41d 0000000000000000 0 0 1

  36. Key to Flags:

  37. W (write), A (alloc), X (execute), M (merge), S (strings), l (large)

  38. I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)

  39. O (extra OS processing required) o (OS specific), p (processor specific)

在我们刚刚编译的Python程序中,我们可以看到.debug_ *段,因此它是包含调试信息的。

调试信息是DIE(调试信息条目)的一个集合。每个DIE都有一个标签,用来表示DIE的类型以及它的属性,就像变量的名称和行号一样。

GDB如何寻找源代码

为了寻找源代码,GDB会解析.debug_info段并查找所有带有DW_TAG_compile_unit标签的DIE。具有此标签的DIE有两个主要属性DW_AT_comp_dir(编译目录)和DW_AT_name(名称),这就是源代码的路径。把这两个属性结合起来就是某个特定编译单元(对象文件)对应的源文件的完整路径。

要解析调试信息,你仍然可以使用objdump命令:

 
  1. $ objdump -g ./python | vim -

  2.  

你可以看到这些解析出来的调试信息:

 
  1. Contents of the .debug_info section:

  2.  
  3. Compilation Unit @ offset 0x0:

  4. Length: 0x222d (32-bit)

  5. Version: 4

  6. Abbrev Offset: 0x0

  7. Pointer Size: 8

  8. <0>: Abbrev Number: 1 (DW_TAG_compile_unit)

  9. DW_AT_producer : (indirect string, offset: 0xb6b): GNU C99 6.3.1 20161221 (Red Hat 6.3.1-1) -mtune=generic -march=x86-64 -g -Og -std=c99

  10. <10> DW_AT_language : 12 (ANSI C99)

  11. <11> DW_AT_name : (indirect string, offset: 0x10ec): ./Programs/python.c

  12. <15> DW_AT_comp_dir : (indirect string, offset: 0x7a): /home/avd/dev/cpython

  13. <19> DW_AT_low_pc : 0x41d2f6

  14. <21> DW_AT_high_pc : 0x1b3

  15. <29> DW_AT_stmt_list : 0x0

GDB是这样读取的:地址从DW_AT_low_pc = 0×41d2f6DW_AT_low_pc + DW_AT_high_pc = 0×41d2f6 + 0×1b3 = 0×41d4a9对应的源代码文件是位于/home/avd/dev/cpython路径下的./Programs/python.c文件,相当简单吧。

这是GDB向你显示源代码的整个过程:

  • 解析.debug_info查找当前对象文件的DW_AT_name属性的DW_AT_comp_dir属性
  • 按照路径DW_AT_comp_dir/DW_AT_name打开文件
  • 显示文件的内容

如何告诉GDB源代码的位置

所以,要解决./Programs/python.c: No such file or directory.这个问题,我们必须在目标主机上存放源代码(复制或git clone过来),并执行以下任意一个操作:

  1. 重建源代码路径

    你可以在目标主机上重建源代码路径,这样,GDB就能找到对应的源代码了。这是个愚蠢的办法,但是还是很有用的。

    在我这个例子中,我执行了这个命令git clone https://github.com/python/cpython.git /home/avd/dev/cpython来检出所需的版本。

  2. 修改GDB源代码路径

    你可以在调试会话中使用directory

    命令让GDB关联正确的源代码路径:

     
    1. (gdb) list

    2. 6 ./Programs/python.c: No such file or directory.

    3. (gdb) directory /usr/src/python

    4. Source directories searched: /usr/src/python:$cdir:$cwd

    5. (gdb) list

    6. 6 #ifdef __FreeBSD__

    7. 7 #include

    8. 8 #endif

    9. 9

    10. 10 #ifdef MS_WINDOWS

    11. 11 int

    12. 12 wmain(int argc, wchar_t **argv)

    13. 13 {

    14. 14 return Py_Main(argc, argv);

    15. 15 }

  3. 设置GDB路径替换规则

    如果目录结构层次比较复杂,有时候添加源代码路径是不够的。在这种情况下,你可以使用set substitute-path命令来添加源路径的替换规则。

     
    1. (gdb) list

    2. 6 ./Programs/python.c: No such file or directory.

    3. (gdb) set substitute-path /home/avd/dev/cpython /usr/src/python

    4. (gdb) list

    5. 6 #ifdef __FreeBSD__

    6. 7 #include

    7. 8 #endif

    8. 9

    9. 10 #ifdef MS_WINDOWS

    10. 11 int

    11. 12 wmain(int argc, wchar_t **argv)

    12. 13 {

    13. 14 return Py_Main(argc, argv);

    14. 15 }

  4. 把二进制文件移到源代码目录

    你可以通过将二进制文件移动到源代码目录来改变GDB源代码路径。

    mv python /home/user/sources/cpython

    因为GDB会试着在当前目录($cwd)下寻找源代码,所以这个做法也是可以的。

  5. 编译时增加-fdebug-prefix-map选项

    你可以使用-fdebug-prefix-map = old_path = new_path编译选项来替代构建阶段的源路径。下面是在CPython项目中执行此操作的例子:

     
    1. $ make distclean # start clean

    2. $ ./configure CFLAGS="-fdebug-prefix-map=$(pwd)=/usr/src/python" --with-pydebug

    3. $ make -j

    这样,我们就有了新的源代码路径:

     
    1. $ objdump -g ./python

    2. ...

    3. <0>: Abbrev Number: 1 (DW_TAG_compile_unit)

    4. DW_AT_producer : (indirect string, offset: 0xb65): GNU C99 6.3.1 20161221 (Red Hat 6.3.1-1) -mtune=generic -march=x86-64 -g -Og -std=c99

    5. <10> DW_AT_language : 12 (ANSI C99)

    6. <11> DW_AT_name : (indirect string, offset: 0x10ff): ./Programs/python.c

    7. <15> DW_AT_comp_dir : (indirect string, offset: 0x558): /usr/src/python

    8. <19> DW_AT_low_pc : 0x41d336

    9. <21> DW_AT_high_pc : 0x1b3

    10. <29> DW_AT_stmt_list : 0x0

    11. ...

    这个办法是最粗暴了,因为你可以将其设置为类似于`/usr/src/project-name’这样的路径,把源代码包安装到这个路径下,然后就可以任性地调试了。

结论

GDB通过以DWARF格式存储的调试信息来查找源代码信息。DWARF是一种非常简单的格式,实际上,它是一棵DIE(调试信息条目)树,它描述了程序的对象文件以及变量和函数。

有很多种方法可以让GDB找到源代码,其中最简单的方法是使用directoryset substitute-path命令,而-fdebug-prefix-map是最最强大的。

你可能感兴趣的:(利器,gdb,关联源码)