## gdb with python 透過 VSCode 的 `launch.json` 直接啟動 gdb 雖然很方便,但如果你想在 gdb 運行時對其功能進行更深入、更靈活的魔改,不論是直接在 gdb 中添加或修改命令,或者僅僅透過 `launch.json` 傳遞參數,都難以滿足需求。最有效率的方法是「寫程式」來包裝並啟動一個 gdb 進程,讓外部程式可以根據需求自定義與 gdb 之間的互動。Python 是實現這種包裝的理想語言,因為它具有非常豐富的 gdb 支持 ([gdb Python API](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Python-API.html#Python-API)) >這篇文章不會教你如何配置 VSCode 中的 C/C++ extension 配置方法,而主要在描述如何在 gdb 不直接呼叫 `source <xxx>.py` 的情況下,使用 python 寫的函式,並且與 VSCode 的 `launch.json` 整合,讓使用者有能夠使用 VSCode 漂亮除錯介面的客製化 gdb。 我們透過下面的例子來看如何自己編寫一個透過 python 包裝的 gdb 吧! ### 牛刀小試 在開始前,你應該先你++安裝了 gdb++ 和確認 gdb 的組態 (config) 包含了 python 相關的功能。可以使用以下命令確認: ```bash ## check gdb is installed which gdb # should print your gdb path, e.g., /usr/bin/gdb ## check whether gdb supports python gdb --config | grep python # ... # --with-python=/usr (relocatable) # --with-python-libdir=/usr/lib (relocatable) # ... ``` 請注意只有在 gdb 的 process 中的 python 才能 `import gdb`。因此要區分 `gdb_launcher.py` (負責啟動 gdb),還有 `gdb_extension.py` (跟 gdb 模組有關的客製擴充功能) 首先,建立一個 C 檔案 `test.c`: ```c #include <stdio.h> #define SWAP(a,b,c) ({c t; t = a; a = b; b = t;}) int main(void) { int x = 10, y = 20; SWAP(x, y, int); printf("%d %d\n", x, y); return 0; } ``` 並用 gcc 編譯: ```bash gcc -g test.c ``` 接著,新建一個 python 檔案,名稱叫做 `my_gdb.py` ,添加以下的內容: ```python import sys import subprocess import shutil def main(): args = sys.argv[1:] print("Performing pre-launch tasks with arguments:", args) gdb_path = shutil.which("gdb") process = subprocess.Popen([gdb_path] + args) # wait for the gdb process to complete before exiting process.wait() if __name__ == "__main__": main() ``` 當寫完後,提升該檔案的執行權限 (否則用 VSCode 呼叫會有問題): ```bash chmod +x my_gdb.py ``` 最後將 VSCode 中 `launch.json` 的內容修改如下,`a.out` 換成你要除錯的可執行檔名稱: ```json { "version": "0.2.0", "configurations": [ { "name": "my_dbg", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/a.out", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": false, "MIMode": "gdb", "miDebuggerPath": "${workspaceFolder}/my_gdb.py", "miDebuggerArgs": "-q --interpreter=mi2", "setupCommands": [] } ] } ``` 此時,點擊運行按鈕你就可以看到跟先前 gdb 一樣的運行畫面! ![image](https://hackmd.io/_uploads/ryBT5Kysyl.png) 到這邊,你已經會使用 VSCode 呼叫經過包裝的 gdb 了,接下來呢? ## VSCode 是怎麼啟動 Debugger 的? ### Debug Adapter Protocol (DAP) VSCode 採用的是 [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol//) 為 UI 資訊和 Debug Adapter 中間的橋梁,而 Debug Adapter 則是為了應付各種語言擁有不同介面的除錯器 (debugger)。理論上每種 debugger 如果要使用 VSCode 的除錯介面都要有自己的 Debug Adapter。 ### 啟動流程 當我們按下開始除錯時, VSCode 會根據使用者提供的 `launch.json` 尋找 debugger 的路徑,有可能是原生的 (e.g., `/usr/bin/gdb`),也有可能是經過包裝的,藉由 debug 結束時的輸出我們能一窺 VSCode 的關鍵參數: ```bash! "/home/xxx/gdb_launcher.py" --interpreter=mi -q --tty=${DbgTerm} 0<"/tmp/Microsoft-MIEngine-In-vyzxxbjz.pvi" 1>"/tmp/Microsoft-MIEngine-Out-b0ybaxic.0yg" ``` 關鍵參數包含了 - `miDebuggerPath`: debugger 或 debugger launcher 路徑。 - `--interpreter=mi`: 指定使用 debugger 的 MI 模式,這會讓 debugger 的輸出變成 machine oriented text,例如以 `~` 開頭表示是 debugger 的輸出...。 - `--tty`: 指定 debugger 中要儲存的程式的 STDOUT。通常會是 pseudo terminal (pty) 的形式,例如 `/dev/pty/0`。 - `0<...`: 重定位標準輸入到一個++暫存檔++中。 - `1>...`: 重定位標準輸出到一個++暫存檔++中。這裡比較 tricky,因為我發現使用 ssh 遠端除錯時,VSCode 會把輸出調整到一個 `pty`,且該數字大部分是 `--tty` 參數再加上 1。但注意僅限 ssh 遠端時,在本地使用他還是會輸出到暫存檔中。 經過測試,只要能寫入到該暫存檔中後,便能被寫到 debug console 上。且格式不要求是 MI 格式。 所以在 gdb launcher 中,如果有任何資訊要打到 debug console 上,使用 `print(..., flush=true)` 就會立刻出現,沒有 flush 可能無法正確顯示(因為該進程之後會阻塞)。而如果你有其他的 script 也想知道怎麼輸出到這個 session 的 debug console 中,可以使用: ```python import os stdout_target = os.readlink('/proc/self/fd/1') # write to this file with open(stdout_target, "w") as f: f.write('Hello from other program\n') ``` 總結以上,做到以下事情後,藉由 gdb launcher 開啟的 gdb 就能正常在 VSCode 介面運作: - 要將 gdb launcher 的 `STDIN`、`STDOUT` 繼承會傳遞給 gdb (先前 `Popen` 達成了這件事情) - 要傳遞 `--tty`、`--interpreter=mi` - 為了讓 launch.json 中的 `setupCommands` 也能正常傳入,debugger launcher 應該完整的 pass 其他參數給 gdb。 ### 注意事項 - 不應該使用 `os.ttyname(x)`,因為在本地端 VSCode 啟動的 debug (launcher) 不會是一個 pty,因此會報錯。 ## 如何完善? > commit: [cb104ee](https://github.com/Dennis40816/lab0-c/commit/cb104eed9bf59e57cbbfcfd6253a851c7741b299) 藉由引入上方的 commit 並搭配以下的 launch.json,你便能在 ++編譯好 `qtest`++ 的情況下,在 VSCode 中開始 debug 程序,並透過以下方法使用客製化功能 (e.g., `container_of`): :::spoiler launch.json ```json { "version": "0.2.0", "configurations": [ { "name": "custom gdb for qtest", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/qtest", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": false, "miDebuggerPath": "${workspaceFolder}/scripts/debug/gdb_launcher.py", "miDebuggerArgs": "-q --verbose", "MIMode": "gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true }, { "description": "Enable GDB pretty-printing", "text": "set print pretty on", "ignoreFailures": true }, { "description": "Ignore SIGALRM signal", "text": "handle SIGALRM ignore", "ignoreFailures": true }, { "description": "Backtrace", "text": "backtrace", "ignoreFailures": true } ] } ] } ``` ::: <br> 啟動後你應該會在 debug console 中看到: ![image](https://hackmd.io/_uploads/rkR_q3fi1e.png) 我們在 qtest 中輸入以下,並斷點在 `q_ascend`: ```bash new it 1 it 3 it 2 ascend ``` 接著回到 debug console 輸入: ```bash # 要用雙引號框起來 -exec p $container_of(head->next, "element_t", "list") # $5 = (element_t *) 0x5555555691b0 -exec p $5->value # $6 = 0x555555569170 "1" ``` ==注意事項== - gdb 命令只能在暫停時才能使用。 - 確保這兩個 script 具有執行權限 ( `chmod +x` )。 - 在 debug console 要輸入 gdb 命令,你必須使用 `-exec <your gdb cmd>`,這樣才會被傳遞給 gdb (或說 gdb 的 STDIN)。 ## Content Reference 1. [〈Automate Debugging with GDB Python API〉](https://interrupt.memfault.com/blog/automate-debugging-with-gdb-python-api)