天天看點

如何在GDB中關聯源代碼

原文:How to point GDB to your sources

翻譯:雁驚寒

如果你手頭上有一個你自己或者别人開發的程式,但它有一些bug。或者你隻是想知道這個程式是如何工作的。怎麼辦呢?你需要一個調試工具。

現在很少有人會直接對着彙編指令進行調試,通常情況下,大家都希望能對照着源代碼進行調試。但是,你調試使用的主機,一般來說并不是建構程式的那台,是以你會看到如下這個令人沮喪的消息:

$ gdb -q python3.7
Reading symbols from python3.7...done.
(gdb) l
6   ./Programs/python.c: No such file or directory.
           

我經常會看到這些報錯資訊,并且對于調試程式來說,這也非常重要。是以,我認為我們需要詳細了解一下GDB是如何在調試會話中顯示源代碼的。

調試資訊

首先,我們從調試資訊開始。調試資訊是由編譯器生成的存在于二進制檔案中的特殊段,供調試器和其他相關的工具使用。

在GCC中,有一個著名的

-g

标志用于生成調試資訊。大多數使用某種建構系統的項目都會在建構時預設包含或者通過一些标志來添加調試資訊。

例如,在CPython中,你需要執行以下指令:

$ ./configure --with-pydebug
$ make -j
           

-with-pydebug

會在調用GCC時添加

-g

選項。

這個

-g

選項會生成二進制的調試段,并插入到程式的二進制檔案中。調試段通常采用DWARF格式。對于ELF二進制檔案來說,調試段的名稱一般都是像

.debug_ *

這樣的,例如

.debug_info

或者

.debug_loc

。這些調試段使得調試程式成為可能,可以這麼說,它是彙編級别的指令與源代碼之間的映射。

要檢視程式是否包含調試符号,你可以使用

objdump

指令列出二進制檔案的所有段:

$ objdump -h ./python

python:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000400238  0000000000400238  00000238  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.ABI-tag 00000020  0000000000400254  0000000000400254  00000254  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
...
 25 .bss          00031f70  00000000008d9e00  00000000008d9e00  002d9dfe  2**5
                  ALLOC
 26 .comment      00000058  0000000000000000  0000000000000000  002d9dfe  2**0
                  CONTENTS, READONLY
 27 .debug_aranges 000017f0  0000000000000000  0000000000000000  002d9e56  2**0
                  CONTENTS, READONLY, DEBUGGING
 28 .debug_info   00377bac  0000000000000000  0000000000000000  002db646  2**0
                  CONTENTS, READONLY, DEBUGGING
 29 .debug_abbrev 0001fcd7  0000000000000000  0000000000000000  006531f2  2**0
                  CONTENTS, READONLY, DEBUGGING
 30 .debug_line   0008b441  0000000000000000  0000000000000000  00672ec9  2**0
                  CONTENTS, READONLY, DEBUGGING
 31 .debug_str    00031f18  0000000000000000  0000000000000000  006fe30a  2**0
                  CONTENTS, READONLY, DEBUGGING
 32 .debug_loc    0034190c  0000000000000000  0000000000000000  00730222  2**0
                  CONTENTS, READONLY, DEBUGGING
 33 .debug_ranges 00062e10  0000000000000000  0000000000000000  00a71b2e  2**0
                  CONTENTS, READONLY, DEBUGGING
           

或者使用

readelf

指令:

$ readelf -S ./python
There are 38 section headers, starting at offset 0xb41840:

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         0000000000400238  00000238  000000000000001c  0000000000000000   A       0     0     1

...

  [26] .bss              NOBITS           00000000008d9e00  002d9dfe
       0000000000031f70  0000000000000000  WA       0     0     32
  [27] .comment          PROGBITS         0000000000000000  002d9dfe
       0000000000000058  0000000000000001  MS       0     0     1
  [28] .debug_aranges    PROGBITS         0000000000000000  002d9e56
       00000000000017f0  0000000000000000           0     0     1
  [29] .debug_info       PROGBITS         0000000000000000  002db646
       0000000000377bac  0000000000000000           0     0     1
  [30] .debug_abbrev     PROGBITS         0000000000000000  006531f2
       000000000001fcd7  0000000000000000           0     0     1
  [31] .debug_line       PROGBITS         0000000000000000  00672ec9
       000000000008b441  0000000000000000           0     0     1
  [32] .debug_str        PROGBITS         0000000000000000  006fe30a
       0000000000031f18  0000000000000001  MS       0     0     1
  [33] .debug_loc        PROGBITS         0000000000000000  00730222
       000000000034190c  0000000000000000           0     0     1
  [34] .debug_ranges     PROGBITS         0000000000000000  00a71b2e
       0000000000062e10  0000000000000000           0     0     1
  [35] .shstrtab         STRTAB           0000000000000000  00b416d5
       0000000000000165  0000000000000000           0     0     1
  [36] .symtab           SYMTAB           0000000000000000  00ad4940
       000000000003f978  0000000000000018          37   8762     8
  [37] .strtab           STRTAB           0000000000000000  00b142b8
       000000000002d41d  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  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

指令:

$ objdump -g ./python | vim -
           

你可以看到這些解析出來的調試資訊:

Contents of the .debug_info section:

  Compilation Unit @ offset 0x0:
   Length:        0x222d (32-bit)
   Version:       4
   Abbrev Offset: 0x0
   Pointer Size:  8
 <0><b>: Abbrev Number: 1 (DW_TAG_compile_unit)
    <c>   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>   DW_AT_language    : 12   (ANSI C99)
    <11>   DW_AT_name        : (indirect string, offset: 0x10ec): ./Programs/python.c
    <15>   DW_AT_comp_dir    : (indirect string, offset: 0x7a): /home/avd/dev/cpython
    <19>   DW_AT_low_pc      : 0x41d2f6
    <21>   DW_AT_high_pc     : 0x1b3
    <29>   DW_AT_stmt_list   : 0x0
           

GDB是這樣讀取的:位址從

DW_AT_low_pc = 0×41d2f6

DW_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 <dir>

    指令讓GDB關聯正确的源代碼路徑:
    (gdb) list
    6   ./Programs/python.c: No such file or directory.
    (gdb) directory /usr/src/python
    Source directories searched: /usr/src/python:$cdir:$cwd
    (gdb) list
    6   #ifdef __FreeBSD__
    7   #include <fenv.h>
    8   #endif
    9   
    10  #ifdef MS_WINDOWS
    11  int
    12  wmain(int argc, wchar_t **argv)
    13  {
    14      return Py_Main(argc, argv);
    15  }
               
  3. 設定GDB路徑替換規則

    如果目錄結構層次比較複雜,有時候添加源代碼路徑是不夠的。在這種情況下,你可以使用

    set substitute-path

    指令來添加源路徑的替換規則。
    (gdb) list
    6   ./Programs/python.c: No such file or directory.
    (gdb) set substitute-path /home/avd/dev/cpython /usr/src/python
    (gdb) list
    6   #ifdef __FreeBSD__
    7   #include <fenv.h>
    8   #endif
    9   
    10  #ifdef MS_WINDOWS
    11  int
    12  wmain(int argc, wchar_t **argv)
    13  {
    14      return Py_Main(argc, argv);
    15  }
               
  4. 把二進制檔案移到源代碼目錄

    你可以通過将二進制檔案移動到源代碼目錄來改變GDB源代碼路徑。

    mv python /home/user/sources/cpython

    因為GDB會試着在目前目錄(

    $cwd

    )下尋找源代碼,是以這個做法也是可以的。
  5. 編譯時增加

    -fdebug-prefix-map

    選項

    你可以使用

    -fdebug-prefix-map = old_path = new_path

    編譯選項來替代建構階段的源路徑。下面是在CPython項目中執行此操作的例子:
    $ make distclean    # start clean
    $ ./configure CFLAGS="-fdebug-prefix-map=$(pwd)=/usr/src/python" --with-pydebug
    $ make -j
               
    這樣,我們就有了新的源代碼路徑:
    $ objdump -g ./python
    ...
     <0><b>: Abbrev Number: 1 (DW_TAG_compile_unit)
        <c>   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
        <10>   DW_AT_language    : 12       (ANSI C99)
        <11>   DW_AT_name        : (indirect string, offset: 0x10ff): ./Programs/python.c
        <15>   DW_AT_comp_dir    : (indirect string, offset: 0x558): /usr/src/python
        <19>   DW_AT_low_pc      : 0x41d336
        <21>   DW_AT_high_pc     : 0x1b3
        <29>   DW_AT_stmt_list   : 0x0
    ...
               
    這個辦法是最粗暴了,因為你可以将其設定為類似于`/usr/src/project-name’這樣的路徑,把源代碼包安裝到這個路徑下,然後就可以任性地調試了。

結論

GDB通過以DWARF格式存儲的調試資訊來查找源代碼資訊。DWARF是一種非常簡單的格式,實際上,它是一棵DIE(調試資訊條目)樹,它描述了程式的對象檔案以及變量和函數。

有很多種方法可以讓GDB找到源代碼,其中最簡單的方法是使用

directory

set substitute-path

指令,而

-fdebug-prefix-map

是最最強大的。

相關資源

DWARF調試格式介紹

GDB文檔