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

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























