## 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 一樣的運行畫面!

到這邊,你已經會使用 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 中看到:

我們在 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)