gdb

calvincalvin發表於2024-10-06

gdb控制程式行為

  • 環境變數

    (gdb) set environment VAR=value

多執行緒

相關命令

  • 檢視執行緒列表:在 gdb 中可以使用以下命令檢視當前所有執行緒的列表:

    (gdb) info threads
    

    這將列出所有執行緒及其狀態,包括它們的 LWP ID 和執行緒識別符號。

  • 切換到特定執行緒:如果你想切換除錯上下文到某個特定的執行緒,可以使用:

    (gdb) thread <thread_number>
    

    其中 <thread_number>gdb 為執行緒分配的編號(可以從 info threads 命令中獲取)。

  • 檢視所有執行緒的呼叫棧(批次檢視每個執行緒的棧幀):

    (gdb) thread apply all bt
    

    這會列出所有執行緒的呼叫棧資訊,方便你快速發現哪個執行緒掛掉或崩潰。

使用 pstack 工具檢視執行緒棧
如果你只想快速檢視某個程序的所有執行緒的棧,而不需要全面除錯,可以使用 pstack 工具(需要安裝)。它會顯示所有執行緒的呼叫棧(包括 LWP ID)。

透過 pstop 檢視執行緒狀態

gdb 之外,你也可以透過系統命令檢視執行緒的狀態:

  • 使用 ps 檢視執行緒狀態

    bash

    Copy

    ps -eLf | grep <PID>
    

    該命令會列出指定程序的所有執行緒,並顯示執行緒的狀態、PID、LWP 等資訊。你可以透過 S 列檢視執行緒的狀態:

    • R:正在執行。
    • S:休眠中。
    • D:不可中斷的睡眠(通常是 I/O 操作)。
    • Z:殭屍執行緒。
  • 使用 top 檢視執行緒狀態

    啟動 top 命令並按 H 鍵,可以檢視某個程序的所有執行緒及其狀態。

    bash

    Copy

    top -H -p <PID>
    

    這將顯示該程序的所有執行緒及其 CPU 使用率、狀態等資訊。

總結

  • 使用 gdb 中的 info threads 命令可以檢視所有執行緒的狀態。
  • 使用 thread <id> 切換到特定執行緒,使用 backtrace 檢視其呼叫棧。
  • 使用 thread apply all bt 可以一次性檢視所有執行緒的呼叫棧,快速定位問題執行緒。
  • 透過呼叫棧可以識別執行緒是否因段錯誤、死鎖、或其他系統呼叫而掛起。
  • 使用系統工具如 pstop 也可以檢視執行緒的狀態,輔助判斷哪個執行緒可能掛掉了。

訊號

SIGABRT 訊號通常在以下情景下觸發:

  • 顯式呼叫 abort()
  • 未透過 assert 檢查
  • 記憶體管理錯誤(如非法釋放記憶體或越界訪問)。
  • 未捕獲的異常(在 C++ 中,丟擲異常但未捕獲時會呼叫 std::terminate(),進而觸發 SIGABRT)。
  • 手動傳送 SIGABRT 訊號

情境問題

如何知道一個檔案被哪些程序的哪些函式讀取過?

要知道一個檔案被哪些程序的哪些函式讀取過,這個問題涉及兩部分:

  1. 哪些程序讀取了該檔案:這是系統級別的問題,透過跟蹤檔案系統呼叫可以獲取相關資訊。
  2. 哪些函式讀取了該檔案:這是程序內部的問題,需要了解程序內的呼叫棧。

要實現這兩部分的監控,通常需要結合系統工具和除錯工具來完成。以下是幾種常見的方式:

1. 使用 strace 跟蹤檔案訪問系統呼叫

strace 是 Linux 系統中的一個工具,可以跟蹤程序的系統呼叫,包括 open(), read(), write() 等檔案操作的系統呼叫。透過 strace,你可以知道某個檔案被哪些程序讀取過。

步驟:

  1. 跟蹤所有程序對檔案的訪問

    你可以透過 strace 跟蹤所有程序的檔案系統呼叫,並過濾出對特定檔案的訪問。例如,假設你想跟蹤 /path/to/file 檔案的訪問,可以使用以下命令:

    sudo strace -f -e trace=open,read,write -p $(pgrep -d, .) 2>&1 | grep "/path/to/file"
    

    解釋:

    • -f:跟蹤子程序。
    • -e trace=open,read,write:只跟蹤 open(), read(), write() 系統呼叫。
    • -p $(pgrep -d, .):跟蹤所有程序(pgrep -d, . 會輸出所有程序的 PID)。
  2. 跟蹤特定程序的檔案訪問

    如果你知道某個程序的 PID,你可以使用 strace 直接跟蹤該程序:

    sudo strace -f -e trace=open,read,write -p <PID>
    

    這將顯示該程序開啟、讀取、寫入的檔案。你可以結合 grep 過濾出你感興趣的檔案。

  3. 輸出示例

    假設有個程序在讀取 /path/to/filestrace 的輸出可能如下:

    open("/path/to/file", O_RDONLY) = 3
    read(3, "file contents...", 1024) = 1024
    

    這表明程序透過檔案描述符 3 開啟了檔案 /path/to/file 並讀取了 1024 位元組的內容。

總結

  • strace:可以用來跟蹤程序對檔案的系統呼叫,知道哪些程序訪問了檔案。
  • lsof:可以實時檢視哪些程序開啟了檔案。
  • auditd:可以對檔案訪問進行審計,記錄哪些程序訪問了檔案。
  • gdbptrace:可以用來跟蹤程序內部的函式呼叫棧,瞭解哪些函式讀取了檔案。
  • LD_PRELOAD:可以透過插樁方式過載檔案操作函式,捕捉呼叫棧資訊。

透過結合這些工具,你可以追蹤檔案訪問的程序和函式呼叫棧。

使用 lsof 檢視檔案被哪些程序開啟

lsof 是一個用於列出開啟檔案的工具。你可以使用它來檢視某個檔案當前被哪些程序開啟。

步驟:

lsof /path/to/file

這將輸出所有當前開啟該檔案的程序資訊,包括程序 ID、程序名稱、開啟的檔案描述符等。例如:

COMMAND   PID  USER   FD   TYPE DEVICE SIZE/OFF  NODE NAME
cat      1234  user    3r   REG  8,1    1234     5678 /path/to/file

這裡,cat 程序(PID 1234)以只讀模式(3r)開啟了 /path/to/file

3. 使用 auditd 進行檔案訪問審計

auditd 是 Linux 的審計框架,可以對檔案訪問進行詳細的日誌記錄,包括程序訪問檔案的情況。

步驟:

  1. 安裝 auditd

    如果未安裝 auditd,可以使用以下命令安裝:

    sudo apt install auditd
    
  2. 新增審計規則

    使用 auditctl 新增審計規則,跟蹤某個檔案的讀取:

    sudo auditctl -w /path/to/file -p r -k file_read
    

    解釋:

    • -w /path/to/file:監控檔案 /path/to/file
    • -p r:監控讀取操作。
    • -k file_read:為此規則設定一個識別符號 file_read
  3. 檢視審計日誌

    審計日誌會儲存在 /var/log/audit/audit.log 檔案中。你可以使用 ausearch 命令檢視與檔案訪問相關的日誌:

    sudo ausearch -k file_read
    

    這將顯示哪些程序讀取了 /path/to/file,包括詳細的程序資訊。

4. 使用 gdbptrace 獲取函式呼叫棧

straceauditd 可以告訴你哪些程序訪問了某個檔案,但它們無法告訴你程序內部的函式呼叫棧。如果你想知道程序內部的哪些函式讀取了該檔案,則需要使用偵錯程式(如 gdb)或 ptrace 進行更深入的分析。

步驟:

  1. 使用 gdb 附加到程序

    假設你已經知道某個程序(PID 為 <PID>)在訪問檔案,你可以使用 gdb 附加到該程序並設定斷點在 open(), read(), 或 fopen() 等檔案操作函式上:

    gdb -p <PID>
    
  2. 設定斷點

    gdb 中設定斷點,例如你想捕捉 open() 系統呼叫:

    (gdb) break open
    

    如果使用 C 標準庫的 fopen() 函式:

    (gdb) break fopen
    
  3. 繼續執行並捕捉呼叫棧

    繼續執行程序,直到遇到斷點:

    (gdb) continue
    

    當程序命中斷點時,你可以使用 backtrace 命令檢視當前的函式呼叫棧:

    (gdb) backtrace
    

    這將顯示當前函式及其父函式,幫助你確定哪些函式呼叫了 open()read()

  4. 輸出示例

    假設呼叫棧如下:

    #0  open (filename="/path/to/file", flags=O_RDONLY) at open.c:34
    #1  0x00007ffff7abc123 in read_file() at file_reader.c:54
    #2  0x00007ffff7abc456 in main() at main.c:12
    

    這表示 main() 呼叫了 read_file(),而 read_file() 又呼叫了 open() 開啟了 /path/to/file

5. 自定義動態庫插樁 (LD_PRELOAD)

可以透過插樁(interpose)標準庫函式 open, read, fopen 等,來記錄下檔案訪問的詳細資訊。你可以編寫一個自定義的動態庫,過載這些函式,在函式被呼叫時記錄呼叫棧或其他資訊。

步驟:

  1. 編寫一個 C 檔案,實現你要過載的函式:

    #define _GNU_SOURCE
    #include <dlfcn.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <execinfo.h>
    
    typedef int (*orig_open_f_type)(const char *pathname, int flags);
    
    int open(const char *pathname, int flags) {
        // 列印呼叫棧
        void *buffer[10];
        int nptrs = backtrace(buffer, 10);
        backtrace_symbols_fd(buffer, nptrs, STDERR_FILENO);
    
        // 呼叫原始的 open 函式
        orig_open_f_type orig_open;
        orig_open = (orig_open_f_type)dlsym(RTLD_NEXT, "open");
        return orig_open(pathname, flags);
    }
    
  2. 編譯成共享庫:

    gcc -shared -fPIC -o myopen.so myopen.c -ldl
    
  3. 使用 LD_PRELOAD 執行目標程式:

    LD_PRELOAD=./myopen.so ./your_program
    

    這樣,每次 open 函式被呼叫時,你的自定義函式會被呼叫,並列印出呼叫棧。

gdb假設沒有載入符號表 是否能斷點一些特定函式呢?

在 GDB 中,如果程式沒有載入符號表(例如,編譯時未使用 -g 選項),偵錯程式將無法識別函式名或變數名。這是因為符號表包含了可執行檔案中函式的名稱、變數的名稱及其對應的記憶體地址等除錯資訊。沒有符號表的情況下,GDB 只能看到程式的機器程式碼和記憶體地址,而無法直接斷點到特定的函式名。

然而,即使沒有符號表,仍然有一些方法可以對特定的函式或程式碼位置設定斷點,具體方法取決於情況:

1. 使用記憶體地址設定斷點

如果你知道目標函式的記憶體地址,你可以直接在該地址設定斷點。即使沒有符號表,GDB 仍然可以在指定的記憶體地址處暫停程式執行。

步驟:

  1. 啟動 GDB 並載入程式:

    gdb ./your_program
    
  2. 執行程式或使用 start 命令讓程式執行到某個位置。

  3. 如果你知道目標函式的地址(例如,透過反彙編工具或 nm 命令獲取),你可以直接在該地址設定斷點。例如:

    break *0x4005d0
    

    這裡 0x4005d0 是目標函式的記憶體地址。* 表示在該地址處設定斷點。

  4. 執行程式,GDB 會在該地址處暫停。

如何獲取函式地址:

  • 你可以使用 nmobjdump 等工具來列出程式中的函式地址。例如:

    nm ./your_program | grep some_function
    

    如果存在 some_function,它會顯示類似以下的輸出:

    00000000004005d0 T some_function
    

    其中 0x4005d0some_function 的地址。

  • 另一種方法是使用 objdump 來反彙編可執行檔案並找到目標函式:

    objdump -d ./your_program | less
    

    然後在反彙編輸出中查詢你感興趣的函式地址。

2. 使用匯編指令設定斷點

如果你知道某個函式的彙編指令或知道特定的彙編指令模式(如函式的入口處通常是 pushmov 指令),你可以透過反彙編來找到這個函式的地址,並在該地址設定斷點。

步驟:

  1. 反彙編程式的入口點或特定程式碼段:

    disassemble main
    

    如果沒有符號表,GDB 也可以反彙編出彙編指令(即使沒有函式名),例如:

    disassemble 0x4005d0
    
  2. 找到感興趣的彙編指令後,設定斷點:

    break *0x4005d0
    

3. 使用 GDB 的自動分析功能

GDB 有一些自動分析功能,即使沒有符號表,它也可以嘗試自動獲取某些函式的地址。例如,info functions 可以列出程式中找到的所有函式地址(儘管沒有符號名)。

info functions

這將列出程式中的所有已知函式,即使沒有符號表,它也可能找到一些與標準庫函式相關的符號。

4. 使用共享庫中函式的符號

如果你除錯的程式動態連結了共享庫,即使主程式沒有符號表,GDB 仍然可以識別共享庫中的符號。例如,如果程式使用了標準 C 庫函式 printf,你可以使用以下命令為 printf 設定斷點:

break printf

因為共享庫通常會載入自己的符號表,因此 GDB 可以識別這些庫中的函式。

5. 使用條件斷點

如果你不知道函式的記憶體地址,也沒有符號表,但你知道某個特定位置的條件(比如某個暫存器的值或記憶體內容),你可以使用條件斷點來捕捉該條件。例如,如果你知道某個函式的返回地址或特定的暫存器值,可以這樣設定斷點:

break *0x4005d0 if $rax == 5

這表示在地址 0x4005d0 處設定斷點,但僅當暫存器 rax 的值為 5 時才觸發斷點。

小結

  • 沒有符號表:GDB 無法直接透過函式名設定斷點,但你可以使用目標函式的記憶體地址來設定斷點。
  • 獲取地址:可以透過工具如 nm, objdump,或者使用 GDB 的反彙編功能來獲取函式的入口地址。
  • 共享庫函式:即使沒有符號表,動態連結庫中的符號仍然可以被識別,允許為標準庫函式設定斷點。

例如,透過命令 break *<address>,你可以在任何特定的記憶體地址處設定斷點。

假如一個執行緒因為拋了一個異常終止了 如何能知道哪個函式拋異常呢?在gdb中

當一個執行緒因為丟擲異常而終止時,你可以使用 GDB 來除錯並確定哪個函式丟擲了異常。GDB 提供了多種工具來捕獲異常和檢查堆疊,從而幫助你找出異常的來源。

步驟概述:

  1. 設定斷點捕獲異常丟擲
  2. 使用回溯(backtrace)檢視呼叫棧,找出哪個函式丟擲了異常。

1. 在 GDB 中捕獲異常丟擲

GDB 提供了一個特殊的命令可以捕獲 C++ 異常丟擲。當有異常丟擲時,程式會暫停,這樣你就可以在丟擲異常的時刻檢查呼叫棧。

你可以使用以下命令捕獲所有 C++ 異常的丟擲:

catch throw

這個命令會告訴 GDB,在異常被丟擲的時候暫停程式的執行。

捕獲異常的具體步驟:

  1. 啟動 GDB,並載入你的程式:

    gdb ./your_program
    
  2. 設定捕捉異常的斷點:

    catch throw
    
  3. 執行程式:

    run
    
  4. 當程式丟擲異常時,GDB 會暫停,並顯示類似以下的資訊:

    Catchpoint 1 (throw)
    

2. 檢視呼叫棧(Backtrace)

當程式由於丟擲異常而暫停時,你可以使用 backtrace(或簡寫 bt)命令檢視呼叫棧,找出哪個函式丟擲了異常。

backtrace

這將顯示當前的呼叫棧,列出從最底層到最頂層的函式呼叫順序。透過這個呼叫棧,你可以找到丟擲異常的函式。

示例:

假設你執行了程式並且捕捉到了異常丟擲:

(gdb) catch throw
Catchpoint 1 (throw)
(gdb) run
Starting program: ./your_program
Catchpoint 1 (throw), __cxa_throw () at /build/glibc-23423....

現在你可以使用 backtrace 來檢視呼叫棧:

(gdb) backtrace
#0  __cxa_throw () at /build/glibc-23423....
#1  0x00000000004015ae in some_function() at main.cpp:10
#2  0x0000000000401234 in main () at main.cpp:20

如上所示,some_function() 函式在 main.cpp 第 10 行丟擲了異常。

3. 捕獲異常處理開始(可選)

除了捕獲異常的丟擲,你還可以捕獲異常的處理過程(即捕獲 catch 處理異常的時刻),方法是使用:

catch catch

這會在異常被捕獲處理的時候暫停程式。如果你想除錯異常處理過程,這個命令非常有用。

4. 其他有用的 GDB 命令

  • info threads:檢視所有執行緒的狀態。如果你除錯的是多執行緒程式,可以使用這個命令來檢視執行緒情況。
  • thread apply all bt:如果程式有多個執行緒,這個命令可以顯示所有執行緒的呼叫棧,幫助你確定哪個執行緒丟擲了異常。

5. 使用除錯符號

為了確保你能夠看到函式名稱、行號等詳細資訊,建議在編譯程式時啟用除錯符號(使用 -g 選項)。例如:

g++ -g -o your_program your_program.cpp

如果沒有除錯符號,GDB 可能只會顯示記憶體地址,而不會顯示函式名稱和程式碼行號。

6. 透過反彙編檢視異常(無除錯符號時)

如果你除錯的程式沒有除錯符號,呼叫棧可能只會顯示記憶體地址而不是函式名。在這種情況下,你可以使用 GDB 的 disassemble 命令來檢視丟擲異常的彙編程式碼,並手動確定丟擲異常的位置。

例如:

disassemble 0x4005d0

這會顯示記憶體地址 0x4005d0 處的彙編指令。你可以透過反彙編和程式碼分析,推測哪個函式丟擲了異常。

總結

  1. 捕獲異常:使用 GDB 的 catch throw 命令捕獲所有 C++ 異常。
  2. 檢視呼叫棧:使用 backtrace 命令檢視呼叫棧,找到丟擲異常的函式。
  3. 多執行緒支援:如果你的程式是多執行緒的,使用 info threadsthread apply all bt 來檢視各執行緒的狀態和呼叫棧。
  4. 除錯符號:確保程式在編譯時加入了除錯符號(-g),這將幫助你在 GDB 中看到更多的除錯資訊。

透過這些步驟,你應該能夠在 GDB 中輕鬆找到哪個函式丟擲了異常,即使是在多執行緒環境下。

我在第二個棧幀 我能拿到什麼上下文呢?

小結

當你進入某個特定的棧幀(例如第二個棧幀)時,能夠獲取的上下文資訊包括:

  • 區域性變數和函式引數:使用 info localsinfo args
  • 呼叫棧:使用 backtracebt 檢視呼叫棧的上下文。
  • 原始碼:使用 listframe 檢視當前幀對應的原始碼。
  • 暫存器狀態:使用 info registers 檢視暫存器的值。
  • 記憶體內容:使用 x 命令檢視任意記憶體地址的內容。
  • 全域性變數:使用 printinfo variables 檢視全域性變數的值。
  • 執行緒資訊:使用 info threads 檢視當前執行緒的狀態。
  • 返回地址:使用 info frame 檢視當前幀的返回地址。
  • 動態型別:使用 printptype 檢視 C++ 物件的動態型別。

如何檢索一個so裡 是否定義了某種符號呢?

要檢索一個共享庫(.so 檔案)中是否定義了某個符號(函式、變數等),你可以使用以下幾種工具:

1. nm 命令

nm 是一個常用的工具,用來列出物件檔案中的符號。你可以使用它來檢視 .so 檔案中定義的符號。

使用方法:

nm -D <your_library.so> | grep <symbol_name>
  • -D:表示顯示動態符號(即只顯示共享庫中的符號)。
  • grep <symbol_name>:用於過濾你想要查詢的符號。

示例:

nm -D libexample.so | grep my_function

如果庫中定義了 my_function,則會顯示類似以下的輸出:

0000000000001234 T my_function

其中:

  • T 表示該符號在庫中定義(T 表示符號在程式碼段中)。
  • U 表示該符號是未定義的(即該共享庫引用了該符號,但並未定義它)。
  • 其他符號型別也存在(如 B 表示 BSS 段中的符號,D 表示資料段中的符號等)。

2. readelf 命令

readelf 是另一個工具,可以顯示 ELF 檔案的詳細資訊(ELF 是 Linux 和其他類 Unix 系統中常用的可執行檔案格式)。

使用方法:

readelf -Ws <your_library.so> | grep <symbol_name>
  • -W:表示不折行輸出,顯示完整的符號資訊。
  • -s:表示顯示符號表。

示例:

readelf -Ws libexample.so | grep my_function

同樣,如果庫中定義了 my_function,你會看到類似下面的輸出:

   Num:    Value          Size Type    Bind   Vis      Ndx Name
    33: 0000000000012345   100 FUNC    GLOBAL DEFAULT   12 my_function
  • FUNC 表示這是一個函式。
  • GLOBAL 表示這是一個全域性符號。
  • DEFAULT 表示預設可見性。

3. objdump 命令

objdump 是另一個用於分析二進位制檔案的工具,也可以列出共享庫中的符號。

使用方法:

objdump -T <your_library.so> | grep <symbol_name>
  • -T:表示顯示動態符號表。

示例:

objdump -T libexample.so | grep my_function

輸出類似於:

0000000000012345 g    DF .text  00000000000000a4  Base        my_function
  • g 表示全域性符號。
  • DF 表示這是一個函式符號。
  • .text 表示函式位於程式碼段。

4. lddldconfig

如果你不確定是否包含在某個共享庫中,lddldconfig 可以幫助你檢視共享庫的依賴關係,確認共享庫實際被載入。

例如,使用 ldd 可以列出可執行檔案所依賴的共享庫:

ldd <your_executable>

這不會直接列出符號,但可以幫助你確認共享庫是否被正確載入。

5. objdump + grep 自動化查詢符號(可選)

你可以透過簡單地結合 objdumpgrep 來自動化查詢符號:

for lib in $(ldconfig -p | awk '{print $4}'); do
    echo "Checking $lib"
    objdump -T $lib | grep my_function
done

這將遍歷系統中的所有共享庫,檢查它們是否包含 my_function 符號。

總結

要查詢一個 .so 檔案中是否定義了某個符號,你可以使用以下工具:

  • nm -D <your_library.so> | grep <symbol_name>:列出動態符號。
  • readelf -Ws <your_library.so> | grep <symbol_name>:檢視 ELF 檔案的符號表。
  • objdump -T <your_library.so> | grep <symbol_name>:顯示動態符號表。

這些工具可以幫助你快速檢索某個共享庫是否定義了特定的符號。