Try   HackMD

gdb with python

透過 VSCode 的 launch.json 直接啟動 gdb 雖然很方便,但如果你想在 gdb 運行時對其功能進行更深入、更靈活的魔改,不論是直接在 gdb 中添加或修改命令,或者僅僅透過 launch.json 傳遞參數,都難以滿足需求。最有效率的方法是「寫程式」來包裝並啟動一個 gdb 進程,讓外部程式可以根據需求自定義與 gdb 之間的互動。Python 是實現這種包裝的理想語言,因為它具有非常豐富的 gdb 支持 (gdb Python API)

這篇文章不會教你如何配置 VSCode 中的 C/C++ extension 配置方法,而主要在描述如何在 gdb 不直接呼叫 source <xxx>.py 的情況下,使用 python 寫的函式,並且與 VSCode 的 launch.json 整合,讓使用者有能夠使用 VSCode 漂亮除錯介面的客製化 gdb。

我們透過下面的例子來看如何自己編寫一個透過 python 包裝的 gdb 吧!

牛刀小試

在開始前,你應該先你安裝了 gdb 和確認 gdb 的組態 (config) 包含了 python 相關的功能。可以使用以下命令確認:

## 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:

#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 編譯:

gcc -g test.c

接著,新建一個 python 檔案,名稱叫做 my_gdb.py ,添加以下的內容:

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 呼叫會有問題):

chmod +x my_gdb.py

最後將 VSCode 中 launch.json 的內容修改如下,a.out 換成你要除錯的可執行檔名稱:

{
  "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 Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

到這邊,你已經會使用 VSCode 呼叫經過包裝的 gdb 了,接下來呢?

VSCode 是怎麼啟動 Debugger 的?

Debug Adapter Protocol (DAP)

VSCode 採用的是 Debug Adapter Protocol 為 UI 資訊和 Debug Adapter 中間的橋梁,而 Debug Adapter 則是為了應付各種語言擁有不同介面的除錯器 (debugger)。理論上每種 debugger 如果要使用 VSCode 的除錯介面都要有自己的 Debug Adapter。

啟動流程

當我們按下開始除錯時, VSCode 會根據使用者提供的 launch.json 尋找 debugger 的路徑,有可能是原生的 (e.g., /usr/bin/gdb),也有可能是經過包裝的,藉由 debug 結束時的輸出我們能一窺 VSCode 的關鍵參數:

"/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 中,可以使用:

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 的 STDINSTDOUT 繼承會傳遞給 gdb (先前 Popen 達成了這件事情)
  • 要傳遞 --tty--interpreter=mi
  • 為了讓 launch.json 中的 setupCommands 也能正常傳入,debugger launcher 應該完整的 pass 其他參數給 gdb。

注意事項

  • 不應該使用 os.ttyname(x),因為在本地端 VSCode 啟動的 debug (launcher) 不會是一個 pty,因此會報錯。

如何完善?

commit: cb104ee

藉由引入上方的 commit 並搭配以下的 launch.json,你便能在 編譯好 qtest 的情況下,在 VSCode 中開始 debug 程序,並透過以下方法使用客製化功能 (e.g., container_of):

launch.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
        }
      ]
    }
  ]
}

啟動後你應該會在 debug console 中看到:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

我們在 qtest 中輸入以下,並斷點在 q_ascend:

new
it 1
it 3
it 2
ascend

接著回到 debug console 輸入:

# 要用雙引號框起來
-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〉