記事一覧へ English

Internet of Tomohiro

よろしければ投げ銭をお願いします。


NimをGDBでデバッグする

  1. GDBをインストールする
  2. GDBでデバッグできるようにコンパイルする
  3. GDBで簡単なプログラムをデバッグしてみる
  4. GDB Text User Interface
  5. GDBで簡単なプログラムをデバッグしてみる2
  6. NimコンパイラをGDBでデバッグ
  7. 他のGDBの便利な機能

GDB はコマンドライン上でプログラムをデバッグするツールです。 GDBを使えばプログラムを実行中に特定の箇所で停止して変数の値を見たり一行づつプログラムを実行することなどができます。 GDBについて詳しく知りたい方は GDB User Manual を参照して下さい。

GDBをインストールする

GDBがインストールされているかどうかは以下のコマンドで確認できます。

On Linux:

which gdb

On Windows:

where gdb

もしインストールされていなければパッケージマネージャ(apt, pacman, emergeなど)を使ってインストールしてください。 GDBはMinGWに含まれています。 Windowsで Scoop というパッケージマネージャを使う場合、 scoop install gcc でgccをインストールするとGDBも一緒にインストールされます。 scoop install gdb でGDBだけをインストールできますが、gccに付属するGDBよりも古いです。 Scoop を使いたくなければ TDM-GCC からダウンロードできます。

GDBでデバッグできるようにコンパイルする

GDBでデバッグするには以下のように --debugger:native オプションをつけてコンパイルします。

nim c --debugger:native test.nim

GDBで簡単なプログラムをデバッグしてみる

以下のコードを test.nim に保存しGDBでデバッグしてみます。

type
  TestObj = object
    num: int
    val: float
    str: string

proc initTestObj(num: int): TestObj =
  TestObj(num: num, val: 3.141, str: "TestObj")

proc foo(x: int): int =
  let y = x + 2
  return y * 10

proc bar(x: int): int =
  if x == 3:
    return foo(x)
  return x * 100

proc main =
  var a = 1
  a += 3
  let str = "foobar"
  var seq1 = @[0, 1, 2, 3, 4]
  a = bar(1)
  a = bar(2)
  a = bar(3)
  let tobj = initTestObj(11)

main()

このコードを --debugger:native をつけてコンパイルした後に nim-gdb に読み込ませます。

nim c --debugger:native test.nim
nim-gdb test

もしnim-gdbが無い場合は Nim repository から bin/nim-gdb (Windowsの場合 bin/nim-gdb.bat)と tools/debug/nim-gdb.py をダウンロードして下さい。 nim-gdbを使わない場合はGDBを起動した後に source tools/debug/nim-gdb.py を実行して下さい。 nim-gdbはGDBを実行し tools/debug/nim-gdb.py を読み込ませるbashスクリプトです。 nim-gdb.pyはNimの変数がGDBで綺麗に表示されるようにするためのPythonスクリプトです。

nim-gdbはbashスクリプトなのでWindowsではbash等をインストールしないとそのまま使えません。 なので以下のようにして tools\debug\nim-gdb.py をGDBに読み込む必要があります。 GDB起動後に

source {path-to-nim}\tools\debug\nim-gdb.py

を実行するか以下のようにGDBを起動して下さい。

gdb -eval-command "source {path-to-nim}\tools\debug\nim-gdb.py" test

nim-gdbが実行されると以下のようなメッセージが表示されます。 実行ファイルにデバッグに必要な情報が含まれていれば Reading symbols from test...done. と表示されるはずです。 もし --debugger:native オプションをつけ忘れてコンパイルすると Reading symbols from test...(no debugging symbols found)...done と表示されます。

GNU gdb (Gentoo 8.1 p1) 8.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://bugs.gentoo.org/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from test...done.
(gdb)

quit または q コマンドを実行するか Ctrl-d キーを押すとGDBを終了できます。

まずbreak pointを設定してみます。コードの中にブレークポイントを設定することによって、プログラムが実行中にその場所に到達すると一時停止させることができます。 b または break コマンドにプロシージャ名を与えることによってそのプロシージャが始まる場所にbreak pointを設定できます。 GDBではNimで書いたプロシージャの名前を直接指定することができず、プロシージャ名にハッシュ値が付いた名前で指定する必要があります。 例えば、 foo という名前のプロシージャにbreak pointを設定するときは break foo_ と入力した後にtabキーを入力するとプロシージャ名についたハッシュ値が補完されます。

(gdb) break foo_yBfYXi2FBVfUOybMAWEXjA_2
Breakpoint 1 at 0x10544: file /tmp/tmp/test.nim, line 10.

ファイル名と行番号を指定してbreak pointを設定することもできます。

(gdb) break test.nim:21
Breakpoint 2 at 0x1088a: file /tmp/tmp/test.nim, line 21.

run コマンドでプログラムを実行します。 先程設定したtest.nimの21行目のbreak pointでプログラムが停止します。 なので a += 3 が実行される直前で停止します。

Starting program: /tmp/tmp/test

Breakpoint 2, main_Ak9bvQf5hr5bnkcYq9cDinOw ()
    at /tmp/tmp/test.nim:21
21        a += 3

print または p コマンドで変数の中身を見ることができます。

(gdb) print a
$1 = 1

next または n コマンドで一行だけ実行します。 このときに a += 3 が実行されます。

(gdb) next
22        let str = "foobar"
(gdb) print a
$2 = 4

next コマンドに実行したい行数を指定することができます。

(gdb) next 3
25        a = bar(2)
(gdb) print a
$3 = 100

step または s コマンドで次の行で呼ばれるプロシージャの中に入ることができます。

(gdb) step
bar_yBfYXi2FBVfUOybMAWEXjA (x=2) at /tmp/tmp/test.nim:14
14      proc bar(x: int): int =

finish または fin コマンドで今いるプロシージャから抜けるまでプログラムを実行します。

(gdb) finish
Run till exit from #0  bar_yBfYXi2FBVfUOybMAWEXjA (x=2)
    at /tmp/tmp/test.nim:14
0x000055555556494b in main_Ak9bvQf5hr5bnkcYq9cDinOw ()
    at /tmp/tmp/test.nim:25
25        a = bar(2)
Value returned is $4 = 200
(gdb) next
26        a = bar(3)
(gdb) print a
$5 = 200

continue または c コマンドでプログラムが終了するかbreakpointに引っかかるまで実行されます。

(gdb) continue
Continuing.

Breakpoint 1, foo_yBfYXi2FBVfUOybMAWEXjA_2 (x=3)
    at /tmp/tmp/test.nim:10
10      proc foo(x: int): int =

backtrace または bt コマンドでbacktraceを表示します。 main プロシージャの26行目から bar(3) プロシージャが呼ばれ、 bar プロシージャの16行目から foo(3) プロシージャが呼ばれているのがわかります。

(gdb) backtrace
#0  foo_yBfYXi2FBVfUOybMAWEXjA_2 (x=3) at /tmp/tmp/test.nim:10
#1  0x0000555555564697 in bar_yBfYXi2FBVfUOybMAWEXjA (x=3)
    at /tmp/tmp/test.nim:16
#2  0x000055555556496c in main_Ak9bvQf5hr5bnkcYq9cDinOw ()
    at /tmp/tmp/test.nim:26
#3  0x0000555555564b38 in NimMainModule ()
    at /tmp/tmp/test.nim:29
#4  0x0000555555564a46 in NimMainInner ()
    at /tmp/tmp/Nim/lib/system.nim:3154
#5  0x0000555555564a82 in NimMain ()
    at /tmp/tmp/Nim/lib/system.nim:3162
#6  0x0000555555564ad0 in main (argc=1, args=0x7fffffffde58,
    env=0x7fffffffde68) at /tmp/tmp/Nim/lib/system.nim:3169

list または l コマンドで現在いる場所付近のソースコードを表示します。

(gdb) list
5           str: string
6
7       proc initTestObj(num: int): TestObj =
8         TestObj(num: num, val: 3.141, str: "TestObj")
9
10      proc foo(x: int): int =
11        let y = x + 2
12        return y * 10
13
14      proc bar(x: int): int =

info breakpoints または info break コマンドでbreak pointのリストを表示します。

(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000555555564544 in foo_yBfYXi2FBVfUOybMAWEXjA_2 at /tmp/tmp/test.nim:10
        breakpoint already hit 1 time
2       breakpoint     keep y   0x000055555556488a in main_Ak9bvQf5hr5bnkcYq9cDinOw at /tmp/tmp/test.nim:21
        breakpoint already hit 1 time

ここに表示されている番号を delete または d コマンドに指定することによってbreakpointを削除することができます。

(gdb) delete 2
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000555555564544 in foo_yBfYXi2FBVfUOybMAWEXjA_2 at /tmp/tmp/test.nim:10
        breakpoint already hit 1 time

info locals コマンドで現在いるプロシージャの全ローカル変数を表示することができます。

(gdb) next
11        let y = x + 2
(gdb) next
12        return y * 10
(gdb) info locals
result = 0
y = 5
TM_ipcYmBC9bj9a1BW35ABoB1Kw_6 = 5
TM_ipcYmBC9bj9a1BW35ABoB1Kw_7 = 93824992304216
FR_ = {prev = 0x7fffffffdc20,
  procname = 0x555555565d58 "foo", line = 11,
  filename = 0x555555565d5c "test.nim", len = 0,
  calldepth = 3}
(gdb) finish
Run till exit from #0  foo_yBfYXi2FBVfUOybMAWEXjA_2 (x=3)
    at /tmp/tmp/test.nim:12
0x0000555555564697 in bar_yBfYXi2FBVfUOybMAWEXjA (x=3)
    at /tmp/tmp/test.nim:16
16          return foo(x)
Value returned is $2 = 50
(gdb) finish
Run till exit from #0  0x0000555555564697 in bar_yBfYXi2FBVfUOy
bMAWEXjA (x=3) at /tmp/tmp/test.nim:16
0x000055555556496c in main_Ak9bvQf5hr5bnkcYq9cDinOw ()
    at /tmp/tmp/test.nim:26
26        a = bar(3)
Value returned is $3 = 50
(gdb) next
27        let tobj = initTestObj(11)
(gdb) next
28
(gdb) info locals
a = 50
TM_ipcYmBC9bj9a1BW35ABoB1Kw_2 = 4
str = "foobar"
seq1 = seq(5, 5) = {0, 1, 2, 3, 4}
tobj = {num = 11, val = 3.141, str = "TestObj"}
FR_ = {prev = 0x7fffffffdce0,
  procname = 0x555555565d75 "main", line = 27,
  filename = 0x555555565d5c "test.nim", len = 0,
  calldepth = 1}

GDB Text User Interface

(Windowsでは使えないっぽい)

GDBでText User Interface(TUI) modeにすると画面を分割してソースコード、アセンブリ言語、レジスタの値を表示できるようになります。 tui enable コマンドでTUI modeになり、 tui disable で元のモードに戻ります。 Ctrl-aキーを押した後にaキーを押すことでもTUI modeを切り替えられます。 GDB起動時に -tui オプションを指定するとTUI modeがデフォルトになります。

画面が乱れたときは Ctrl + L キーで画面をリフレッシュできます。

GDBで簡単なプログラムをデバッグしてみる2

以下のコードを test2.nim に保存しGDBでデバッグしてみます。

import os, strutils

proc main =
  let params = commandLineParams()
  if params.len == 0:
    return
  
  let count = parseInt(params[0])
  
  var x = 0
  var sum = 0
  for i in 0..count:
    inc sum
    if sum == 10:
      inc x
  
  echo sum

main()

デバッグするプログラムに引数を与えるときはnim-gdbを起動するときに --args を指定し実行ファイル名の後に引数を指定します。

nim-gdb --args test2 1000 foo
(gdb) break test2.nim:11
Breakpoint 1 at 0x11b35: file /tmp/tmp/test2.nim, line 11.
(gdb) run
Starting program: /tmp/tmp/test2 1000 foo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, main_3iLAKFrCD49cjgkzKbLZt2A () at /tmp/tmp/test2.nim:11
11        var sum = 0
(gdb) print params
$1 = seq(2, 2) = {"1000", "foo"}

run コマンドに引数を指定することもできます。

nim-gdb test2
(gdb) break test2.nim:11
Breakpoint 1 at 0x11b35: file /tmp/tmp/test2.nim, line 11.
(gdb) run 1000 foo
Starting program: /tmp/tmp/test2 1000 foo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, main_3iLAKFrCD49cjgkzKbLZt2A () at /tmp/tmp/test2.nim:11
11        var sum = 0
(gdb) print params
$1 = seq(2, 2) = {"1000", "foo"}

Watchpointを使うことによって指定した変数の値が変化したときにプログラムを停止させることができます。

(gdb) watch x
Hardware watchpoint 2: x
(gdb) continue
Continuing.

Hardware watchpoint 2: x

Old value = 0
New value = 1
0x0000555555565c3b in main_3iLAKFrCD49cjgkzKbLZt2A () at /tmp/tmp/test2.nim:15
15            inc x
(gdb) print sum
$1 = 10
(gdb) print x
$2 = 1

print コマンドで変数の値を変更することができます。

(gdb) print sum = 0
$3 = 0
(gdb) print sum
$4 = 0
(gdb) continue
Continuing.

Hardware watchpoint 2: x

Old value = 1
New value = 2
0x0000555555565c3b in main_3iLAKFrCD49cjgkzKbLZt2A () at /tmp/tmp/test2.nim:15
15            inc x
(gdb) print sum
$5 = 10
(gdb) print x
$6 = 2

until または u コマンドで一行だけ実行することができますが、ループがある場合はループが終わるまで実行されます。

(gdb) until
2166            inc(res)
(gdb) until
2164          while res <= int(b):
(gdb) until
17        echo sum

NimコンパイラをGDBでデバッグ

NimコンパイラはNim言語で書かれNimコンパイラでコンパイルされます。なのでnim-gdbでデバッグできます。 まずは普通にNim compilerをビルドします。

git clone https://github.com/nim-lang/Nim.git
cd Nim

On linux:

sh build_all.sh

On windows:

build_all.bat

Nimコンパイラをデバッグ用にコンパイルします。 bin ディレクトリに nim_temp が出力されます。

koch temp

nim_temp をデバッグします。

nim-gdb --args bin/nim_temp c ../test.nim

他のGDBの便利な機能


by Tomohiro

記事一覧へ