Linux中國

手把手教你使用 GNU 調試器

GNU 調試器是一個發現程序缺陷的強大工具。

如果你是一個程序員,想在你的軟體增加某些功能,你首先考慮實現它的方法:例如寫一個方法、定義一個類,或者創建新的數據類型。然後你用編譯器或解釋器可以理解的編程語言來實現這個功能。但是,如果你覺得你所有代碼都正確,但是編譯器或解釋器依然無法理解你的指令怎麼辦?如果軟體大多數情況下都運行良好,但是在某些環境下出現缺陷怎麼辦?這種情況下,你得知道如何正確使用調試器找到問題的根源。

GNU 調試器 GNU Project Debugger GDB)是一個發現項目缺陷的強大工具。它通過追蹤程序運行過程中發生了什麼來幫助你發現程序錯誤或崩潰的原因。(LCTT 校註:GDB 全程是「GNU Project Debugger」,即 「GNU 項目調試器」,但是通常我們簡稱為「GNU 調試器」)

本文是 GDB 基本用法的實踐教程。請跟隨示例,打開命令行並克隆此倉庫:

git clone https://github.com/hANSIc99/core_dump_example.git

快捷方式

GDB 的每條命令都可以縮短。例如:顯示設定的斷點的 info break 命令可以被縮短為 i break。你可能在其他地方看到過這種縮寫,但在本文中,為了清晰展現使用的函數,我將所寫出整個命令。

命令行參數

你可以將 GDB 附加到每個可執行文件。進入你克隆的倉庫(core_dump_example),運行 make 進行編譯。你現在能看到一個名為 coredump 的可執行文件。(更多信息,請參考我的文章《創建和調試 Linux 的轉儲文件》。)

要將 GDB 附加到這個可執行文件,請輸入: gdb coredump

你的輸出應如下所示:

gdb coredump output

返回結果顯示沒有找到調試符號。

調試信息是 目標文件 object file (可執行文件)的組成部分,調試信息包括數據類型、函數簽名、源代碼和操作碼之間的關係。此時,你有兩種選擇:

  • 繼續調試彙編代碼(參見下文「無符號調試」)
  • 使用調試信息進行編譯,參見下一節內容

使用調試信息進行編譯

為了在二進位文件中包含調試信息,你必須重新編譯。打開 Makefile,刪除第 9 行的注釋標籤(#)後重新編譯:

CFLAGS =-Wall -Werror -std=c++11 -g

-g 告訴編譯器包含調試信息。運行 make clean,接著運行 make,然後再次調用 GDB。你得到如下輸出後就可以調試代碼了:

GDB output with symbols

新增的調試信息會增加可執行文件的大小。在這種情況下,執行文件增加了 2.5 倍(從 26,088 位元組 增加到 65,480 位元組)。

輸入 run -c1,使用 -c1 開關啟動程序。當程序運行到達 State_4 時將崩潰:

gdb output crash on c1 switch

你可以檢索有關程序的其他信息,info source 命令提供了當前文件的信息:

gdb info source output

  • 101 行代碼
  • 語言: C++
  • 編譯器(版本、調優、架構、調試標誌、語言標準)
  • 調試格式:DWARF 2
  • 沒有預處理器宏指令(使用 GCC 編譯時,宏僅在 使用 -g3 標誌編譯 時可用)。

info shared 命令列印了動態庫列表機器在虛擬地址空間的地址,它們在啟動時被載入到該地址,以便程序運行:

gdb info shared output

如果你想了解 Linux 中的庫處理方式,請參見我的文章 在 Linux 中如何處理動態庫和靜態庫

調試程序

你可能已經注意到,你可以在 GDB 中使用 run 命令啟動程序。run 命令接受命令行參數,就像從控制台啟動程序一樣。-c1 開關會導致程序在第 4 階段崩潰。要從頭開始運行程序,你不用退出 GDB,只需再次運行 run 命令。如果沒有 -c1 開關,程序將陷入死循環,你必須使用 Ctrl+C 來結束死循環。

gdb output stopped by sigint

你也可以一步一步運行程序。在 C/C++ 中,入口是 main 函數。使用 list main 命令打開顯示 main 函數的部分源代碼:

gdb output list main

main 函數在第 33 行,因此可以輸入 break 33 在 33 行添加斷點:

gdb output breakpoint added

輸入 run 運行程序。正如預期的那樣,程序在 main 函數處停止。輸入 layout src 並排查看源代碼:

gdb output break at main

你現在處於 GDB 的文本用戶界面(TUI)模式。可以使用鍵盤向上和向下箭頭鍵滾動查看源代碼。

GDB 高亮顯示當前執行行。你可以輸入 nextn)命令逐行執行命令。如果你沒有指定新的命令,GBD 會執行上一條命令。要逐行運行代碼,只需按回車鍵。

有時,你會發現文本的輸出有點顯示不正常:

gdb output corrupted

如果發生這種情況,請按 Ctrl+L 重置屏幕。

使用 Ctrl+X+A 可以隨時進入和退出 TUI 模式。你可以在手冊中找到 其他的鍵綁定

要退出 GDB,只需輸入 quit

設置監察點

這個示常式序的核心是一個在無限循環中運行的狀態機。n_state 變數枚舉了當前所有狀態:

while(true){
        switch(n_state){
        case State_1:
                std::cout << "State_1 reached" << std::flush;
                n_state = State_2;
                break;
        case State_2:
                std::cout << "State_2 reached" << std::flush;
                n_state = State_3;
                break;

        (.....)

        }
}

如果你希望當 n_state 的值為 State_5 時停止程序。為此,請在 main 函數處停止程序並為 n_state 設置監察點:

watch n_state == State_5

只有當所需的變數在當前上下文中可用時,使用變數名設置監察點才有效。

當你輸入 continue 繼續運行程序時,你會得到如下輸出:

gdb output stop on watchpoint_1

如果你繼續運行程序,當監察點表達式評估為 false 時 GDB 將停止:

gdb output stop on watchpoint_2

你可以為一般的值變化、特定的值、讀取或寫入時來設置監察點。

更改斷點和監察點

輸入 info watchpoints 列印先前設置的監察點列表:

gdb output info watchpoints

刪除斷點和監察點

如你所見,監察點就是數字。要刪除特定的監察點,請先輸入 delete 後輸入監察點的編號。例如,我的監察點編號為 2;要刪除此監察點,輸入 delete 2

注意: 如果你使用 delete 而沒有指定數字,所有 監察點和斷點將被刪除。

這同樣適用於斷點。在下面的截屏中,我添加了幾個斷點,輸入 info breakpoint 列印斷點列表:

gdb output info breakpoints

要刪除單個斷點,請先輸入 delete 後輸入斷點的編號。另外一種方式:你可以通過指定斷點的行號來刪除斷點。例如,clear 78 命令將刪除第 78 行設置的斷點號 7。

禁用或啟用斷點和監察點

除了刪除斷點或監察點之外,你可以通過輸入 disable,後輸入編號禁用斷點或監察點。在下文中,斷點 3 和 4 被禁用,並在代碼窗口中用減號標記:

disabled breakpoints

也可以通過輸入類似 disable 2 - 4 修改某個範圍內的斷點或監察點。如果要重新激活這些點,請輸入 enable,然後輸入它們的編號。

條件斷點

首先,輸入 delete 刪除所有斷點和監察點。你仍然想使程序停在 main 函數處,如果你不想指定行號,可以通過直接指明該函數來添加斷點。輸入 break main 從而在 main 函數處添加斷點。

輸入 run 從頭開始運行程序,程序將在 main 函數處停止。

main 函數包括變數 n_state_3_count,當狀態機達到狀態 3 時,該變數會遞增。

基於 n_state_3_count 的值添加一個條件斷點,請輸入:

break 54 if n_state_3_count == 3

Set conditional breakpoint

繼續運行程序。程序將在第 54 行停止之前運行狀態機 3 次。要查看 n_state_3_count 的值,請輸入:

print n_state_3_count

print variable

使斷點成為條件斷點

你也可以使現有斷點成為條件斷點。用 clear 54 命令刪除最近添加的斷點,並通過輸入 break 54 命令添加一個簡單的斷點。你可以輸入以下內容使此斷點成為條件斷點:

condition 3 n_state_3_count == 9

3 指的是斷點編號。

modify breakpoint

在其他源文件中設置斷點

如果你的程序由多個源文件組成,你可以在行號前指定文件名來設置斷點,例如,break main. cpp:54

捕捉點

除了斷點和監察點之外,你還可以設置捕獲點。捕獲點適用於執行系統調用、載入共享庫或引發異常等事件。

要捕獲用於寫入 STDOUT 的 write 系統調用,請輸入:

catch syscall write

catch syscall write output

每當程序寫入控制台輸出時,GDB 將中斷執行。

在手冊中,你可以找到一整章關於 斷點、監察點和捕捉點 的內容。

評估和操作符號

print 命令可以列印變數的值。一般語法是 print <表達式> <值>。修改變數的值,請輸入:

set variable <variable-name> <new-value>.

在下面的截屏中,我將變數 n_state_3_count 的值設為 123

catch syscall write output

/x 表達式以十六進位列印值;使用 & 運算符,你可以列印虛擬地址空間內的地址。

如果你不確定某個符號的數據類型,可以使用 whatis 來查明。

whatis output

如果你要列出 main 函數範圍內可用的所有變數,請輸入 info scope main :

info scope main output

DW_OP_fbreg 值是指基於當前子程序的堆棧偏移量。

或者,如果你已經在一個函數中並且想要列出當前堆棧幀上的所有變數,你可以使用 info locals :

info locals output

查看手冊以了解更多 檢查符號 的內容。

附加調試到一個正在運行的進程

gdb attach <進程 ID> 命令允許你通過指定進程 ID(PID)附加到一個已經在運行的進程進行調試。幸運的是,coredump 程序將其當前 PID 列印到屏幕上,因此你不必使用 pstop 手動查找 PID。

啟動 coredump 應用程序的一個實例:

./coredump

coredump application

操作系統顯示 PID 為 2849。打開一個單獨的控制台窗口,移動到 coredump 應用程序的根目錄,然後用 GDB 附加到該進程進行調試:

gdb attach 2849

attach GDB to coredump

當你用 GDB 附加到進程時,GDB 會立即停止進程運行。輸入 layout srcbacktrace 來檢查調用堆棧:

layout src and backtrace output

輸出顯示在 main.cpp 第 92 行調用 std::this_thread::sleep_for<...>(. ..) 函數時進程中斷。

只要你退出 GDB,該進程將繼續運行。

你可以在 GDB 手冊中找到有關 附加調試正在運行的進程 的更多信息。

在堆棧中移動

在命令窗口,輸入 up 兩次可以在堆棧中向上移動到 main.cpp :

moving up the stack to main.cpp

通常,編譯器將為每個函數或方法創建一個子程序。每個子程序都有自己的棧幀,所以在棧幀中向上移動意味著在調用棧中向上移動。

你可以在手冊中找到有關 堆棧計算 的更多信息。

指定源文件

當調試一個已經在運行的進程時,GDB 將在當前工作目錄中尋找源文件。你也可以使用 目錄命令 手動指定源目錄。

評估轉儲文件

閱讀 創建和調試 Linux 的轉儲文件 了解有關此主題的信息。

參考文章太長,簡單來說就是:

  1. 假設你使用的是最新版本的 Fedora
  2. 使用 -c1 開關調用 coredump:coredump -c1

Crash meme

  1. 使用 GDB 載入最新的轉儲文件:coredumpctl debug
  2. 打開 TUI 模式並輸入 layout src

coredump output

backtrace 的輸出顯示崩潰發生在距離 main.cpp 五個棧幀之外。回車直接跳轉到 main.cpp 中的錯誤代碼行:

up 5 output

看源碼發現程序試圖釋放一個內存管理函數沒有返回的指針。這會導致未定義的行為並引起 SIGABRT

無符號調試

如果沒有源代碼,調試就會變得非常困難。當我在嘗試解決逆向工程的挑戰時,我第一次體驗到了這一點。了解一些 彙編語言 的知識會很有用。

我們用例子看看它是如何運行的。

找到根目錄,打開 Makefile,然後像下面一樣編輯第 9 行:

CFLAGS =-Wall -Werror -std=c++11 #-g

要重新編譯程序,先運行 make clean,再運行 make,最後啟動 GDB。該程序不再有任何調試符號來引導源代碼的走向。

no debugging symbols

info file 命令顯示二進位文件的內存區域和入口點:

info file output

.text 區段始終從入口點開始,其中包含實際的操作碼。要在入口點添加斷點,輸入 break *0x401110 然後輸入 run 開始運行程序:

breakpoint at the entry point

要在某個地址設置斷點,使用取消引用運算符 * 來指定地址。

選擇反彙編程序風格

在深入研究彙編之前,你可以選擇要使用的 彙編風格。 GDB 默認是 AT&T,但我更喜歡 Intel 語法。變更風格如下:

set disassembly-flavor intel

changing assembly flavor

現在輸入 layout asm 調出彙編代碼窗口,輸入 layout reg 調出寄存器窗口。你現在應該看到如下輸出:

layout asm and layout reg output

保存配置文件

儘管你已經輸入了許多命令,但實際上還沒有開始調試。如果你正在大量調試應用程序或嘗試解決逆向工程的難題,則將 GDB 特定設置保存在文件中會很有用。

該項目的 GitHub 存儲庫中的 gdbinit 配置文件包含最近使用的命令:

set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg

set write on 命令使你能夠在程序運行期間修改二進位文件。

退出 GDB 並使用配置文件重新啟動 GDB : gdb -x gdbinit coredump

閱讀指令

應用 c2 開關後,程序將崩潰。程序在入口函數處停止,因此你必須寫入 continue 才能繼續運行:

continuing execution after crash

idiv 指令進行整數除法運算:RAX 寄存器中為被除數,指定參數為除數。商被載入到 RAX 寄存器中,餘數被載入到 RDX 中。

從寄存器角度,你可以看到 RAX 包含 5,因此你必須找出存儲堆棧中位置為 rbp-0x4 的值。

讀取內存

要讀取原始內存內容,你必須指定比讀取符號更多的參數。在彙編輸出中向上滾動一點,可以看到堆棧的劃分:

stack division output

你最感興趣的應該是 rbp-0x4 的值,因為它是 idiv 的存儲參數。你可以從截圖中看到rbp-0x8 位置的下一個變數,所以 rbp-0x4 位置的變數是 4 位元組寬。

在 GDB 中,你可以使用 x 命令查看任何內存內容:

x/ < 可選參數 nfu > < 內存地址 addr >

可選參數:

  • n:單元大小的重複計數(默認值:1)
  • f:格式說明符,如 printf
  • u:單元大小
    • b:位元組
    • h:半字(2 個位元組)
    • w: 字(4 個位元組)(默認)
    • g: 雙字(8 個位元組)

要列印 rbp-0x4 的值,請輸入 x/u $rbp-4 :

print value

如果你能記住這種模式,則可以直接查看內存。參見手冊中的 查看內存 部分。

操作彙編

子程序 zeroDivide() 發生運算異常。當你用向上箭頭鍵向上滾動一點時,你會找到下面信息:

0x401211 <_Z10zeroDividev>              push   rbp
0x401212 <_Z10zeroDividev+1>            mov    rbp,rsp

這被稱為 函數前言

  1. 調用函數的基指針(rbp)存放在棧上
  2. 棧指針(rsp)的值被載入到基指針(rbp

完全跳過這個子程序。你可以使用 backtrace 查看調用堆棧。在 main 函數之前只有一個堆棧幀,所以你可以用一次 up 回到 main :

Callstack assembly

在你的 main 函數中,你會找到下面信息:

0x401431 <main+497>     cmp    BYTE PTR [rbp-0x12],0x0
0x401435 <main+501>     je     0x40145f <main+543>
0x401437 <main+503>     call   0x401211<_Z10zeroDividev>

子程序 zeroDivide() 僅在 jump equal (je)true 時進入。你可以輕鬆地將其替換為 jump-not-equal (jne) 指令,該指令的操作碼為 0x75(假設你使用的是 x86/64 架構;其他架構上的操作碼不同)。輸入 run 重新啟動程序。當程序在入口函數處停止時,設置操作碼:

set *(unsigned char*)0x401435 = 0x75

最後,輸入 continue 。該程序將跳過子程序 zeroDivide() 並且不會再崩潰。

總結

你會在許多集成開發環境(IDE)中發現 GDB 運行在後台,包括 Qt Creator 和 VSCodium 的 本地調試 擴展。

GDB in VSCodium

了解如何充分利用 GDB 的功能很有用。一般情況下,並非所有 GDB 的功能都可以在 IDE 中使用,因此你可以從命令行使用 GDB 的經驗中受益。

via: https://opensource.com/article/21/1/gnu-project-debugger

作者:Stephan Avenwedde 選題:lkxed 譯者:Maisie-x 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國