原文: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
過來),并執行以下任意一個操作:
-
重建源代碼路徑
你可以在目标主機上重建源代碼路徑,這樣,GDB就能找到對應的源代碼了。這是個愚蠢的辦法,但是還是很有用的。
在我這個例子中,我執行了這個指令
來檢出所需的版本。git clone https://github.com/python/cpython.git /home/avd/dev/cpython
-
修改GDB源代碼路徑
你可以在調試會話中使用
指令讓GDB關聯正确的源代碼路徑:directory <dir>
(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 }
-
設定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 }
-
把二進制檔案移到源代碼目錄
你可以通過将二進制檔案移動到源代碼目錄來改變GDB源代碼路徑。
因為GDB會試着在目前目錄(mv python /home/user/sources/cpython
)下尋找源代碼,是以這個做法也是可以的。$cwd
- 編譯時增加
-fdebug-prefix-map
選項
你可以使用
編譯選項來替代建構階段的源路徑。下面是在CPython項目中執行此操作的例子:-fdebug-prefix-map = old_path = new_path
這樣,我們就有了新的源代碼路徑:$ make distclean # start clean $ ./configure CFLAGS="-fdebug-prefix-map=$(pwd)=/usr/src/python" --with-pydebug $ make -j
這個辦法是最粗暴了,因為你可以将其設定為類似于`/usr/src/project-name’這樣的路徑,把源代碼包安裝到這個路徑下,然後就可以任性地調試了。$ 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 ...
結論
GDB通過以DWARF格式存儲的調試資訊來查找源代碼資訊。DWARF是一種非常簡單的格式,實際上,它是一棵DIE(調試資訊條目)樹,它描述了程式的對象檔案以及變量和函數。
有很多種方法可以讓GDB找到源代碼,其中最簡單的方法是使用
directory
和
set substitute-path
指令,而
-fdebug-prefix-map
是最最強大的。
相關資源
DWARF調試格式介紹
GDB文檔