騰訊wxg一面:如何調(diào)試非調(diào)試版本的可執(zhí)行文件?
作為一名開發(fā)者,你是否遇到過這樣的場景:線上程序突然崩潰,當(dāng)你滿心期待地使用 GDB 調(diào)試工具去定位問題時(shí),卻發(fā)現(xiàn)可執(zhí)行程序沒有調(diào)試信息,這就好比在黑暗中摸索,卻連手電筒都沒有。這是因?yàn)樵诎l(fā)布程序時(shí),為了減小可執(zhí)行文件的大小、提高運(yùn)行效率或者出于安全考慮,我們常常會去掉調(diào)試信息??梢坏┏绦虺隽藛栴},沒有調(diào)試信息,就很難定位到問題的根源,這給調(diào)試工作帶來了極大的挑戰(zhàn)。
從經(jīng)驗(yàn)角度出發(fā),若在項(xiàng)目中遇到此類情況,首先可考慮利用日志記錄來輔助調(diào)試。在關(guān)鍵代碼位置插入打印語句,輸出關(guān)鍵變量值與程序執(zhí)行狀態(tài)信息。例如,在 C++ 程序里,通過自定義日志函數(shù),像void Log(const std::string& message) { std::cout << message << std::endl; },在可能出錯的函數(shù)起始、關(guān)鍵分支處調(diào)用該函數(shù)輸出關(guān)鍵信息。并且,為使日志更具價(jià)值,添加時(shí)間戳與線程 ID(針對多線程程序)是不錯的做法,能更清晰地還原程序執(zhí)行路徑。
此外,對于使用網(wǎng)絡(luò)的應(yīng)用程序,運(yùn)用包嗅探工具如 Wireshark,可捕獲網(wǎng)絡(luò)數(shù)據(jù)包,分析數(shù)據(jù)交互情況,排查因網(wǎng)絡(luò)通信導(dǎo)致的問題。還可借助系統(tǒng)調(diào)用監(jiān)視器,像 Linux 系統(tǒng)下的 strace 工具,跟蹤程序執(zhí)行過程中的系統(tǒng)調(diào)用,洞察程序?qū)ο到y(tǒng)資源的操作,定位異常點(diǎn)。
Part1.理解 Release 與 Debug版本
GDB調(diào)試分為兩種模式,一種是debug版本,一種是release版本。一般GDB主要調(diào)試的是C/C++的程序。
(1)debug版本:debug版本為可調(diào)式版本,生成的可執(zhí)行文件中包含調(diào)試需要的信息。
(2)release版本:release版本為發(fā)行版本,是提供給用戶使用的版本。
在深入探討如何使用 GDB 調(diào)試不帶調(diào)試信息的可執(zhí)行程序之前,我們先來了解一下 Release 版本和 Debug 版本的區(qū)別。這兩種版本就像是同一輛車的兩種不同配置,Debug 版本像是一輛配置齊全、便于隨時(shí)檢查和維修的測試車,而 Release 版本則像是一輛經(jīng)過精簡和優(yōu)化,追求速度和性能的量產(chǎn)車。
1.1編譯器優(yōu)化差異
Debug 版本通常禁用優(yōu)化,以保留程序的原始結(jié)構(gòu)和邏輯,方便開發(fā)者進(jìn)行調(diào)試。例如,在 GCC 編譯器中,Debug 版本常使用-O0選項(xiàng),這意味著編譯器幾乎不會對代碼進(jìn)行優(yōu)化,代碼的執(zhí)行順序和變量的訪問方式與源代碼非常接近,就像你在按照菜譜做菜,每一步都清晰可見。
圖片
而 Release 版本則會啟用各種優(yōu)化選項(xiàng),如-O2、-O3,這些選項(xiàng)會讓編譯器對代碼進(jìn)行大刀闊斧的優(yōu)化,通過減少冗余指令、合并重復(fù)代碼等方式,使程序的執(zhí)行效率大幅提升,就像一位經(jīng)驗(yàn)豐富的廚師,在熟悉菜譜后,能夠靈活調(diào)整步驟,提高做菜的速度和效率。但這也導(dǎo)致了代碼的執(zhí)行順序和變量的訪問方式與源代碼可能有較大差異,給調(diào)試帶來了困難。
圖片
可以明顯看到,生成的.exe文件大小明顯不一樣,而且Debug版本下的文件大小明顯要大于Release版本的。所以Release版本運(yùn)行速度上是最優(yōu)的,以便用戶很好地使用。
所以,我們平時(shí)寫代碼為了方便調(diào)試是要用Debug版本的,當(dāng)我們寫完并調(diào)試完代碼,把代碼發(fā)給用戶的時(shí)候,就給用戶發(fā)Release版本的。
1.2調(diào)試信息有無
Debug 版本包含完整的調(diào)試信息,這些信息就像是地圖上的詳細(xì)標(biāo)注,能幫助開發(fā)者在程序的 “迷宮” 中找到方向。在編譯時(shí)使用-g選項(xiàng),編譯器會將調(diào)試信息嵌入到可執(zhí)行文件中,包括變量名、函數(shù)名、行號等。而 Release 版本默認(rèn)不包含調(diào)試信息,因?yàn)樵诎l(fā)布程序時(shí),這些信息對于普通用戶來說沒有用處,反而會增加文件的大小,所以編譯器通常不會使用-g選項(xiàng),這就好比你拿到了一張沒有標(biāo)注地點(diǎn)的地圖,很難找到自己的位置和目的地。
調(diào)試信息對于調(diào)試的作用至關(guān)重要,它能讓調(diào)試器將程序的執(zhí)行狀態(tài)與源代碼對應(yīng)起來,方便開發(fā)者查看變量的值、跟蹤函數(shù)的調(diào)用等。但同時(shí),調(diào)試信息也會使二進(jìn)制文件的大小顯著增加,因?yàn)樗舜罅款~外的元數(shù)據(jù)。
1.3斷言機(jī)制不同
斷言(assert ())是一種在程序開發(fā)中常用的調(diào)試工具,它就像是程序中的 “報(bào)警器”,當(dāng)某個條件不滿足時(shí),就會觸發(fā)警報(bào)。在 Debug 版本中,斷言通常是啟用的,這有助于開發(fā)者在開發(fā)過程中及時(shí)發(fā)現(xiàn)邏輯錯誤。例如,當(dāng)你編寫一個函數(shù),期望傳入的參數(shù)是一個正數(shù),你可以使用斷言來檢查參數(shù)是否符合預(yù)期。一旦斷言失敗,程序就會停止執(zhí)行,并給出錯誤信息,就像報(bào)警器響起,提醒你有問題需要解決。
而在 Release 版本中,為了提高程序的執(zhí)行效率,斷言通常是被禁用的,因?yàn)樵谏a(chǎn)環(huán)境中,頻繁的斷言檢查可能會影響程序的性能,就像在正式比賽中,為了追求速度,你可能會關(guān)閉一些不必要的檢測機(jī)制。
1.4二進(jìn)制文件大小對比
Release 版本由于去除了調(diào)試符號、進(jìn)行了代碼優(yōu)化等操作,生成的二進(jìn)制文件通常較小,就像一個精簡版的軟件包,只包含了運(yùn)行所需的核心內(nèi)容。而 Debug 版本因?yàn)榘{(diào)試符號和未優(yōu)化的代碼,文件大小往往較大,就像一個包含了所有開發(fā)工具和文檔的完整軟件包。
例如,一個簡單的 C++ 程序,Debug 版本的可執(zhí)行文件可能有幾十 MB,而 Release 版本可能只有幾 MB,這種大小差異在大型項(xiàng)目中更為明顯。
Part2.調(diào)試信息分離核心
2.1GDB的調(diào)試信息分離機(jī)制
GDB 允許程序的調(diào)試信息和可執(zhí)行文件分離,這就像是將汽車的維修手冊和汽車本身分開存放。當(dāng)可執(zhí)行程序出現(xiàn)問題時(shí),GDB 會自動查找和加載調(diào)試信息,就像你在需要維修汽車時(shí),能夠快速找到對應(yīng)的維修手冊。這種機(jī)制的實(shí)現(xiàn)原理是,GDB 通過兩種方式來指定獨(dú)立調(diào)試信息文件:一種是可執(zhí)行文件包含一個調(diào)試鏈接,它會指定獨(dú)立調(diào)試信息文件的名稱;另一種是可執(zhí)行文件包含一個構(gòu)建標(biāo)識符,它是一個唯一的位串,在相應(yīng)的調(diào)試信息文件中也包含這個位串 。
2.2調(diào)試鏈接詳解
調(diào)試鏈接包含兩個關(guān)鍵信息:調(diào)試信息文件的名稱和 CRC 校驗(yàn)和。調(diào)試信息文件的名稱通常為executable.debug,其中executable是相應(yīng)的可執(zhí)行文件的名稱,不帶有前導(dǎo)的目錄名,就像給汽車維修手冊命名時(shí),直接以汽車型號命名一樣,方便查找。CRC 校驗(yàn)和的目的是為了驗(yàn)證可執(zhí)行程序和調(diào)試文件是否匹配,就像通過鑰匙來驗(yàn)證是否是同一把鎖的配套鑰匙。
在生成調(diào)試鏈接時(shí),會將調(diào)試信息文件的名稱和 CRC 校驗(yàn)和添加到可執(zhí)行文件中,當(dāng) GDB 調(diào)試時(shí),會根據(jù)這些信息來查找和加載調(diào)試信息。例如,在編譯生成release with debug info時(shí),它包含調(diào)試信息,然后把它進(jìn)行調(diào)試信息分離,生成一個可執(zhí)行程序(用a代替)和調(diào)試信息文件(用b代替)。接下來把調(diào)試鏈接做出來,就是在可執(zhí)行程序a當(dāng)中,加上一個鏈接,這個鏈接信息包含調(diào)試信息文件b的名字和 CRC 校驗(yàn)和 。
Part3.案例實(shí)踐:以 Redis 為例的調(diào)試流程
3.1編譯帶調(diào)試信息的可執(zhí)行文件
在編譯 Redis 時(shí),我們可以使用-O2或-O3優(yōu)化選項(xiàng)來優(yōu)化代碼,提高運(yùn)行效率,同時(shí)使用-g選項(xiàng)添加調(diào)試信息,-DENDEBUG用于禁用斷言(在 Release 版本中通常會禁用斷言以提高性能)。例如:
make CFLAGS="-O2 -g -DENDEBUG"這樣就會生成一個包含調(diào)試信息的 Redis 可執(zhí)行程序,雖然文件大小可能會有所增加,但為后續(xù)的調(diào)試提供了便利。就像給汽車安裝了一個詳細(xì)的導(dǎo)航系統(tǒng),雖然增加了一些重量,但能讓你在行駛過程中更清楚地知道自己的位置和方向。
3.2轉(zhuǎn)存調(diào)試信息
接下來,我們使用objcopy工具將調(diào)試信息轉(zhuǎn)存到一個新的文件中。命令如下:
objcopy --only-keep-debug ./src/redis-server redis-server.debug這條命令會把./src/redis-server中的調(diào)試信息提取出來,保存到redis-server.debug文件中。此時(shí),redis-server.debug就像是一個裝滿調(diào)試信息的寶箱,里面包含了變量名、函數(shù)名、行號等重要信息。
3.3去除可執(zhí)行程序調(diào)試信息
為了得到一個 “干凈” 的、不包含調(diào)試信息的可執(zhí)行程序,我們使用strip命令:
strip ./src/redis-server -o redis-server經(jīng)過這一步,redis-server就變成了一個體積更小、運(yùn)行效率更高的裸可執(zhí)行程序,就像一輛去掉了多余裝飾和設(shè)備的賽車,速度更快,但缺少了調(diào)試所需的信息。
3.4添加調(diào)試鏈接
最后,我們要為這個裸可執(zhí)行程序添加調(diào)試鏈接,讓它能夠找到對應(yīng)的調(diào)試信息文件。使用objcopy命令:
objcopy --add-gnu-debuglink=redis-server.debug redis-server這條命令會在redis-server中添加一個調(diào)試鏈接,鏈接到redis-server.debug文件,就像在賽車的儀表盤上安裝了一個指向維修手冊的指針,當(dāng)賽車出現(xiàn)問題時(shí),能夠快速找到對應(yīng)的維修手冊。此時(shí),redis-server雖然不包含調(diào)試信息,但它知道從哪里獲取調(diào)試信息,為后續(xù)的調(diào)試做好了準(zhǔn)備。
3.5用 GDB 調(diào)試
現(xiàn)在,我們就可以使用 GDB 來調(diào)試這個帶有調(diào)試鏈接的可執(zhí)行程序了。啟動 GDB 并加載redis-server:
gdb redis-serverGDB 會自動根據(jù)調(diào)試鏈接找到redis-server.debug文件,并加載其中的調(diào)試信息。在 GDB 中,我們可以使用各種調(diào)試命令,如設(shè)置斷點(diǎn)(break)、查看變量值(print)、單步執(zhí)行(step)等,就像一名經(jīng)驗(yàn)豐富的修理工,使用各種工具對賽車進(jìn)行檢查和維修,從而定位和解決程序中的問題。例如,我們可以在某個函數(shù)處設(shè)置斷點(diǎn),查看程序執(zhí)行到該點(diǎn)時(shí)變量的值,分析程序的執(zhí)行邏輯是否正確。















 
 
 









 
 
 
 