圖文詳解 Gcc -g 調試信息
大家好,這里是物聯網心球。
作為一個Linux開發者,我們經常會通過gcc -g命令來編譯可執行程序,-g選項能夠生成調試信息,開發者根據調試信息能夠快速定位并排查程序問題。
那么,調試信息到底是什么呢?本文我們一探究竟。
1.調試信息的重要性
gdb調試是一種非常重要的調試手段,如果gdb調試的程序沒有調試信息,那么我們將無法從gdb調試器中獲取到有價值的信息,調試過程也會異常困難。下面我們通過幾個場景來驗證一下。
場景1:查看變量
通過print命令打印變量的值時,帶調試信息輸出示例如下:
(gdb) print a
$1 = 100未帶調試信息輸出示例如下:
(gdb) print a
'a' has unknown type; cast it to its declared typegdb調試未帶調試信息的可執行程序時無法識別變量。
場景2:打印數據類型
通過ptype命令打印數據類型,帶調試信息輸出示例如下:
(gdb) ptype it
type = struct item {
char num;
int data;
}未帶調試信息輸出示例如下:
(gdb) ptype it
No symbol table is loaded. Use the "file" command.gdb調試未帶調試信息的可執行程序時無法識別數據類型。
場景3:指定源文件行號設置斷點
通過break命令在源文件指定行號設置斷點,帶調試信息輸出示例如下:
(gdb) break main.c:12
Breakpoint 2 at 0x55555555515c: file main.c, line 14.未帶調試信息輸出示例如下:
(gdb) break main.c:12
No symbol table is loaded. Use the "file" command.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (main.c:12) pending.gdb調試未帶調試信息的可執行程序時,無法在指定源文件行號設置斷點。
場景4:打印堆棧
通過bt命令打印堆棧信息,帶調試信息輸出示例如下:
(gdb) bt
#0 main (argc=1, argv=0x7fffffffe468) at main.c:14未帶調試信息輸出示例如下:
(gdb) bt
#0 0x0000555555555151 in main ()gdb調試未帶調試信息的可執行程序時,打印堆棧信息無行號和其他調試信息。
通過以上幾個場景的對比,我們能夠直觀的感受到,帶調試信息的可執行文件能夠提供更加豐富和關鍵的調試信息,協助開發者高效地解決問題。
2.認識調試信息
調試信息其實并不神秘,它是嵌入在ELF文件中的節,這些節的形式為.debug_*(*代表不同的節)。
我們通過圖1來深入學習調試信息。
圖1 兩種文件對比
圖1左邊為無調試信息可執行文件,右邊為帶調試信息可執行文件。無調試信息可執行文件中的內容比較少,文件占用內存空間也比較小。通過對比,我們發現帶調試信息可執行文件多了一些節:.debug_aranges節、.debug_info節、.debug_abbrev節、.debug_line節、.debug_str節、.debug_line_str節。這些多出來的節就是我們今天的主角:調試信息。這些節并不是隨意生成的,需要遵循一定的規則,這個規則就是DWARF。
3.DWARF介紹
DWARF(全稱:Debugging With Attributed Record Formats)是一種廣泛使用的調試數據格式標準,旨在為編譯器、調試器和其他工具提供一種標準化的機制,用于描述程序的源代碼結構、變量、類型、函數調用棧等信息。
DWARF共經歷了5個版本的迭代:
- DWARF1:1992年發布,為Unix System V的sdb調試器設計,支持C 語言,現已很少使用。
- DWARF2:1993年發布,修正v1問題,增加C++支持,引入數據壓縮。
- DWARF3:2005年發布,增加對多種語言支持,優化數據壓縮。
- DWARF4:2010年發布,進一步改善數據壓縮,支持編譯器優化后代碼描述。
- DWARF5:2017年發布,支持調試信息分離,改進宏和源文件描述。
DWARF的規則很復雜,筆者不建議大家直接研究DWARF標準。我們可以把DWARF標準當做一個手冊,當我們學習的過程中遇到問題再去查閱它。相較于直接學習DWARF標準,更為有效的學習方法是選搞懂各種調試節(.debug_*)的原理和作用。
以DWARF5為例,常見的調試節見表1。
表1 DWARF5常見調試節
通過命令:readelf -w 可執行文件,可顯示所有的調試節。由于該命令展示的內容比較多,限于篇幅,這里不展示輸出示例。
3.1 .debug_aranges節
.debug_aranges節用于提供內存地址與編譯單元之間的映射關系,編譯單元后續會詳細介紹。通過.debug_aranges節,調試器可以快速將程序計數器(PC)的值映射到.debug_info節中的編譯單元,從而獲取相關的調試信息。
通過命令:readelf -wr 可執行文件,可查看.debug_aranges信息,輸出示例如下:
圖片
.debug_aranges節分為兩個部分:頭部信息和地址范圍表。
頭部信息相關字段如下:
- Length:表示該節的總長度(不包括這個長度字段本身)。
- Version:表示 DWARF 格式的版本號,通常為2或4。
- Offset into .debug_info:表示.debug_info節中的偏移量,用于定位編譯單元的起始位置。
- Pointer Size:表示目標系統中指針的大小,以字節為單位。
- Segment Size:表示目標系統中段選擇符的大小,以字節為單位,通常為 0。
地址范圍表由一系列的地址范圍對組成,每個地址范圍對包含兩個字段: - Address:表示地址范圍的起始地址。
- Length:表示地址范圍的長度。
gcc編譯可執行程序時,會將源文件(如.c/.cpp文件)編譯成編譯單元并將編譯單元覆蓋的地址范圍記錄在.debug_aranges節。
gdb需要確定某個指令地址(如:0x7fff1234)對應的源代碼位置時: 首先,通過0x7fff1234地址查詢.debug_aranges節地址范圍表,找到對應的編譯單元在.debug_info中的偏移;然后,通過偏移定位到.debug_info中的編譯單元;最后,從編譯單元開始解析DIE樹,獲取到源碼相關的信息。
3.2 .debug_info節
.debug_info節由調試信息條目(Debugging Information Entry,DIE)構成。DIE是.debug_info節中的一個基本單元,它通過標簽(Tag)和屬性(Attributes)來描述程序中的一個語義實體(如:函數名、參數、局部變量、代碼行號范圍)。每個DIE可以有多個子條目,形成一個樹狀結構,用于描述更復雜的語義關系。
通過命令:readelf -wi 可執行文件,可查看.debug_info信息,輸出示例如下:
圖片
每個 DIE 包含以下幾個部分:
(1)Tag(標簽)
Tag 是一個單字節的值,用于標識調試信息條目(DIE)的類型。常見的Tag如下:
- DW_TAG_compile_unit:表示一個編譯單元,通常是源文件。
- DW_TAG_subprogram:表示一個函數或方法。
- DW_TAG_variable:表示一個變量,可以是全局變量或局部變量。
- DW_TAG_formal_parameter:表示函數的參數。
- DW_TAG_typedef:表示一個類型定義。
- DW_TAG_structure_type:表示一個結構體類型。
- DW_TAG_union_type:表示一個聯合體類型。
- DW_TAG_enumeration_type:表示一個枚舉類型。
- DW_TAG_array_type:表示一個數組類型。
- DW_TAG_base_type:表示一個基本類型,如int 、float等。
(2)Attributes(屬性)
Attributes 是一個屬性列表,每個屬性由一個屬性名稱和一個屬性值組成。常見的Attributes如下:
- DW_AT_name:表示實體的名稱,如變量名、函數名、文件名等。
- DW_AT_type:表示實體的類型,通常是一個偏移量,指向.debug_info節中的另一個 DIE。
- DW_AT_location:表示變量的存儲位置,可以是寄存器或內存地址。
- DW_AT_low_pc和DW_AT_high_pc:表示函數的代碼范圍,分別表示函數的起始地址和結束地址。
- DW_AT_decl_file、DW_AT_decl_line 和DW_AT_decl_column:表示實體在源文件中的聲明位置。
(3)Children(子條目)
Children 是一個 DIE 的子條目列表,每個子條目也是一個 DIE。
3.3 .debug_abbrev節
.debug_abbrev節用于定義.debug_info節中調試信息條目(DIE)的結構和屬性的縮寫表。通過使用縮寫表,.debug_info節可以更高效地存儲和解析調試信息,減少重復數據的存儲。
通過命令:readelf -wa 可執行文件,可查看.debug_abbrev節信息,輸出示例如下:
圖片
.debug_abbrev節中的DIE和.debug_info節中的DIE互為抽象和實現的關系。一個比較形象的比喻,.debug_abbrev節中的DIE如同C++中的類,.debug_info節中的DIE如同C++中的對象。類是數據類型,對象為類的具體實現。
3.4 .debug_line節
.debug_line節建立了機器指令地址與源代碼行號之間的映射關系,是實現源碼級調試的核心基礎。
.debug_line節的核心功能是:
- 提供源代碼行號與機器指令地址的精確映射。
- 使調試器能夠將程序計數器(PC)值轉換為源代碼位置。
- 支持設置斷點、單步執行和堆棧跟蹤等基本調試功能。
通過命令:readelf -wl 可執行文件,可查看.debug_line節信息,輸出示例如下:
圖片
注意,.debug_line節的規則比較復雜,我們可以跳過規則學習,先了解.debug_line的作用。.debug_line節最終將會呈現一個指令地址和源文件行號的映射表,見表2。
表2 .debug_line指令地址和行號映射表
3.5 .debug_str節
.debug_str節是一個字符串池,它存儲了調試過程中所需的所有字符串數據,包括:函數名、變量名、類型名、編譯器信息、其他文本描述。
通過命令:readelf -ws 可執行文件,可查看.debug_str節信息,輸出示例如下:
圖片
從輸出示例可以看到.debug_str包含一些我們比較常見的字符串,如果我們想使用這些字符串,只需要從調試文件指定的位置讀取這些字符串即可。
3.6 .debug_line_str節
.debug_line_str節同樣是一個字符串池。這些字符串通常包括文件名、目錄路徑等,它們被.debug_line節中的行號程序引用。
輸出示例如下:
圖片
.debug_line_str和.debug_str的使用方法類似。
4.調試信息分離
調試信息分離是指將調試信息從可執行文件中分離出來,存儲在單獨的文件中。將調試信息存儲在單獨的文件有幾個優點:
- 減小可執行文件大?。和ㄟ^將調試信息分離到單獨的文件中,可執行文件的大小可以顯著減小。
- 提高安全性:分離的調試信息文件可以存儲在安全的環境中,從而防止調試信息泄露。
- 提高性能:較小的可執行文件可以更快地加載和運行,從而提高應用程序的性能。
4.1 實現原理
為了讓大家更好的理解調試信息分離的過程,我們通過一張圖描述該過程,如圖2所示。

圖2 調試信息分離
調試信息分離具體步驟如下:
- 步驟1:編譯帶調試信息的可執行程序。
gcc -g編譯帶調試信息的可執行文件,以test程序為例,命令如下:
gcc -g -o test test.c- 步驟2:生成單獨的調試信息文件。
使用objcopy命令的--only-keep-debug選項,將調試信息提取到一個單獨的文件中,命令如下:
objcopy --only-keep-debug test test.debugtest.debug文件為單獨調試文件。
- 步驟3:去除可執行文件中的調試信息。
使用objcopy命令的--strip-debug選項,移除可執行文件調試信息,
objcopy --strip-debug test執行完該命令,可執行文件中的.debug_*節將全部被刪除。
- 步驟4:添加調試信息文件鏈接。
使用objcopy命令的--add-gnu-debuglink選項,將調試信息文件的路徑添加到可執行文件中。gdb調試可執行文件時,就能夠自動查找到調試信息文件。
objcopy --add-gnu-debuglink=test.debug test執行完該命令,可執行文件中將添加一個.gnu_debuglink節,其中包含調試信息文件的路徑。通過以下命令可以查看.gnu_debuglink節中的內容:
readelf -p .gnu_debuglink test輸出示例如下:
圖片
4.2 gdb調試單獨調試文件
當我們按照上述步驟生成了單獨的調試文件后,我們既能夠獲得一個輕量級的可執行程序,又能夠有一個完整的調試文件。當程序在實際環境運行出現問題時,我們能夠通過單獨的調試文件排查問題。
gdb調試單獨調試文件分為:手動添加調試文件和自動添加調試文件。
手動添加通過gdb add-symbol-file命令完成,輸出示例如下:
(gdb) add-symbol-file test.debug
add symbol table from file "test.debug"
(y or n) y
Reading symbols from test.debug...自動添加的方式需要在可執行程序中添加調試信息文件鏈接(參考調試信息分離步驟4)。注意,可執行文件和調試文件需要在同一個目錄。輸出示例如下:
圖片
























