iT邦幫忙

2021 iThome 鐵人賽

DAY 9
1
Software Development

微自幹的作業系統輕旅行系列 第 16

GNU Debugger

GNU Debugger,簡稱 GDB,是 GNU 軟體系統中的除錯器,由於其具有可移植的優點,在現今的主流處理器架構與作業系統平台上都可以看見 GDB 的身影。

使用 GDB 進行除錯

如果要使用 GDB 對 C 程式進行除錯,需要在編譯時添加 -g 參數:

$ gcc main.c -g -o main

等到 gcc 編譯完成後,我們便可以使用 gdb 打開可執行檔案進行除錯:

gdb ./main

使用 -g 產生除錯訊息會大大的增加應用程式的檔案大小,一般在發佈應用程式時是不會以 -g 參數編譯的。
除錯完成後,我們可以使用 strip 指令清掉應用程式中的除錯資訊:

strip main

常見指令

gdb 的常見指令如下:

  • help h: 顯示指令簡短說明,如: help breakpoint

  • file: 開啟檔案,等同於 gdb filename

  • run r: 繼續或是重新執行程式。

  • kill: 中止執行中的程式。

  • backtrace bt: 追蹤 Stack,會顯示出上層所有 frame 的簡略資訊。

  • print p: 印出變數內容。

  • list l: 印出程式碼。

  • whatis:印出變數的型態。例: whatis i,印出變數 i 的型態。

  • breakpoint b, bre, break: 設定中斷點

    • 使用 info breakpoint 或是 info b 查看已設定了哪些中斷點。
    • 在程式被中斷後,使用 info line 來查看正停在哪一行。
  • continue c, cont: 由目前中斷的地方開始繼續執行。

  • frame: 顯示正在執行的行數、副程式名稱、及其所傳送的參數等等 frame 資訊。
     frame 2: 看到 #2,也就是上上一層的 frame 的資訊。

  • next n: 單步執行,但遇到 frame 時不會進入 frame 中單步執行。

  • step s: 單步執行。但遇到 frame 時則會進入 frame 中單步執行。

  • until: 直接跑完一個 while 迴圈。

  • return: 中止執行該 frame(視同該 frame 已執行完畢),
     並返回上個 frame 的呼叫點。功用類似 C 裡的 return 指令。

  • finish: 執行完這個 frame。當進入一個過深的 frame 時,如:C 函式庫,
     可能必須下達多個 finish 才能回到原來的進入點。

  • up: 直接回到上一層的 frame,並顯示其 stack 資訊,如進入點及傳入的參數等。

  • up 2: 直接回到上三層的 frame,並顯示其 stack 資訊。

  • down: 直接跳到下一層的 frame,並顯示其 stack 資訊。

    必須使用 up 回到上層的 frame 後,才能用 down 回到該層來。

  • display: 在遇到中斷點時,自動顯示特定變數的內容。

  • undisplay: 取消 display

  • commands: 在遇到中斷點時要自動執行的指令。

  • info: 顯示一些特定的資訊,如:

    • info break 顯示中斷點。
    • info share 顯示共享函式庫資訊。
  • disable: 暫時關閉某個 breakpoint 或 display。

  • enable: 將被暫時關閉的功能啟用。

  • clear/delete: 刪除某個 breakpoint。

  • set: 設定特定參數,如: set env 設定/修改環境變數。

  • unset: 取消特定參數,如: unset env 刪除環境變數。

  • show: 顯示特定參數。如: show environment 顯示環境變數。

  • attach PID: 載入已執行中的程式以進行除錯。其中的 PID 可由 ps 指令取得。

  • detach PID: 釋放 attached program。

  • shell: 執行 Shell 指令,例如: shell ls 會呼叫 shell 並執行 ls 指令。

  • quit: 離開 gdb。

  • <Enter>: 直接執行上個指令。

在程式中產生中斷點

除了可以使用 b 設定中斷點外,透過下面程式碼中的方法,我們同樣可以在程式碼中設置中斷點:

int main() {
    int val = 1;
    val = 42;
    asm("int $3"); // set a breakpoint here
    val = 7;
}

不只如此,我們還可以利用前置處理器讓除錯變得更容易:

int main() {
    int val = 1;
    val = 42;
#ifdef DEBUG
    asm("int $3"); // set a breakpoint here
#endif
    val = 7;
}
$ gcc main.c -g -DDEBUG -o main 
gdb ./main
(gdb) r
[...]
Program received signal SIGTRAP, Trace/breakpoint trap.
main () at main.c:6
6	    val = 7;
(gdb) p val
$1 = 42

上面的範例告訴我們,當 Process 執行到第六行時收到了訊號 SIGTRAP,接著我們可以使用 p 印出變數當前儲存的內容。

TUI Mode

如果覺得 GDB 的命列列模式不夠友善,我們也可以使用 GDB 提供的 TUI Mode。該模式會顯示除錯中的程式碼,並且將當前執行的程式反白:

要使用 TUI Mode,有幾種方法:

  1. 使用命令:
gdb -tui

或是:

gdbtui
  1. 進入 GDB 後使用組合鍵 ctrl + x + a,如果要退出 TUI Mode 則再次使用組合鍵即可。

在嵌入式系統開發中使用 GDB

因為在修改 Timer_handler 時出現了一些異常,所以筆者嘗試為 mini-riscv-os 新增 debug 腳本,效果如下:

make debug
riscv64-unknown-elf-gcc -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima -mabi=ilp32 -g -Wall -T os.ld -o os.elf start.s sys.s lib.c timer.c task.c os.c user.c trap.c lock.c
Press Ctrl-C and then input 'quit' to exit GDB and QEMU
-------------------------------------------------------
Reading symbols from os.elf...
Breakpoint 1 at 0x80000000: file start.s, line 7.
0x00001000 in ?? ()
=> 0x00001000:  97 02 00 00     auipc   t0,0x0

Thread 1 hit Breakpoint 1, _start () at start.s:7
7           csrr t0, mhartid                # read current hart id
=> 0x80000000 <_start+0>:       f3 22 40 f1     csrr    t0,mhartid
(gdb)

修改 Makefile 與添加 gdbinit

因為 mini-riscv-os 使用了 Make 建構工具,為了保持一致性,我們需要在 Makefile 動一點手腳。
至於 gdbinit 則是設定 gdb 的一些參數,內容如下:

set disassemble-next-line on
b _start
target remote : 1234
c

接著,在 Makefile 加入以下內容:

.PHONY : debug
debug: all
	@echo "Press Ctrl-C and then input 'quit' to exit GDB and QEMU"
	@echo "-------------------------------------------------------"
	@${QEMU} ${QFLAGS} -kernel os.elf -s -S &
	@${GDB} os.elf -q -x ./gdbinit

這樣一來,就能夠在開發作業系統時使用 GDB 進行除錯了!

設置中斷點

實際上,我們可以將中斷點設置在指定檔案的指定行數上:

(gdb) b trap.c:27
Breakpoint 2 at 0x80008f78: file trap.c, line 27.
(gdb)

根據上面的範例,當虛擬機執行到 trap.c 的第 27 行時,整個工作都會被暫停下來直到我們按下 c (continue) 或是 s (step)。
這麼做可以讓作業系統每一次發生中斷時都暫停執行,這時就可以利用 gdb 檢查 stack、特定變數或是暫存器的狀態是否符合我們的預期。

Reference


上一篇
RISC V::中斷與異常處理 -- PLIC 介紹
下一篇
GNU Compiler Collection
系列文
微自幹的作業系統輕旅行38

尚未有邦友留言

立即登入留言