GDB を使用した Go コードのデバッグ

以下の手順は、標準ツールチェーン (gc Go コンパイラとツール) に適用されます。Gccgo はネイティブの gdb サポートを備えています。

標準ツールチェーンでビルドされた Go プログラムをデバッグする場合、Delve は GDB より優れた選択肢であることに注意してください。Delve は、Go ランタイム、データ構造、および式を GDB よりもよく理解しています。 Delve は現在、amd64 上の Linux、OSX、および Windows をサポートしています。サポートされているプラットフォームの最新リストについては、Delve のドキュメントを参照してください。

GDB は Go プログラムをよく理解していません。スタック管理、スレッディング、およびランタイムには、GDB が想定する実行モデルとは異なる側面が含まれており、プログラムが gccgo でコンパイルされている場合でも、デバッガを混乱させ、誤った結果を引き起こす可能性があります。その結果、GDB は状況によっては役立つ場合がありますが (たとえば、Cgo コードのデバッグやランタイム自体のデバッグなど)、Go プログラム、特に並行性の高いプログラムの信頼できるデバッガではありません。さらに、これらの問題は困難であるため、Go プロジェクトにとってこれらの問題に対処することは優先事項ではありません。

要するに、以下の手順は、GDB が機能する場合の使用方法のガイドとしてのみ解釈する必要があります。成功の保証として解釈しないでください。この概要に加えて、GDB マニュアルを参照することをお勧めします。

はじめに

Linux、macOS、FreeBSD、または NetBSD で gc ツールチェーンを使用して Go プログラムをコンパイルおよびリンクすると、結果のバイナリには、最新バージョン (≥7.5) の GDB デバッガがライブプロセスまたはコアダンプを検査するために使用できる DWARFv4 デバッグ情報が含まれます。

デバッグ情報を省略するには、リンカに '-w' フラグを渡します (例: go build -ldflags=-w prog.go)。

gc コンパイラによって生成されたコードには、関数呼び出しのインライン化と変数のレジスタ化が含まれています。これらの最適化により、gdb でのデバッグが困難になる場合があります。これらの最適化を無効にする必要がある場合は、go build -gcflags=all="-N -l" を使用してプログラムをビルドします。

gdb を使用してコアダンプを検査する場合、許可されているシステムでは、環境で GOTRACEBACK=crash を設定することにより、プログラムのクラッシュ時にダンプをトリガーできます (詳細については、ランタイムパッケージのドキュメントを参照してください)。

一般的な操作

Go 拡張機能

GDB の最近の拡張メカニズムにより、特定のバイナリ用の拡張スクリプトを読み込むことができます。ツールチェーンはこれを使用して、ランタイムコードの内部 (ゴルーチンなど) を検査し、組み込みのマップ、スライス、およびチャネルタイプをきれいに印刷するための一握りのコマンドで GDB を拡張します。

これがどのように機能するかを確認したい場合、または拡張したい場合は、Go ソースディストリビューションの src/runtime/runtime-gdb.py をご覧ください。リンカ (src/cmd/link/internal/ld/dwarf.go) によって DWARF コードで記述されることが保証されている特別なマジックタイプ (hash<T,U>) と変数 (runtime.mruntime.g) に依存しています。

デバッグ情報がどのように見えるかを知りたい場合は、objdump -W a.out を実行し、.debug_* セクションを参照してください。

既知の問題

  1. 文字列のプリティ印刷は、型文字列に対してのみトリガーされ、それから派生した型に対してはトリガーされません。
  2. ランタイムライブラリの C 部分の型情報がありません。
  3. GDB は Go の名前の修飾を理解しておらず、"fmt.Print" を引用符で囲む必要がある "." を持つ構造化されていないリテラルとして扱います。 pkg.(*MyType).Meth 形式のメソッド名にはさらに強く反対します。
  4. Go 1.11 現在、デバッグ情報はデフォルトで圧縮されています。MacOS でデフォルトで利用可能な gdb の古いバージョンは、圧縮を理解していません。 go build -ldflags=-compressdwarf=false を使用して、非圧縮のデバッグ情報を生成できます。(便宜上、-ldflags オプションを GOFLAGS 環境変数 に入れることができるため、毎回指定する必要はありません。)

チュートリアル

このチュートリアルでは、regexp パッケージの単体テストのバイナリを検査します。バイナリをビルドするには、$GOROOT/src/regexp に変更し、go test -c を実行します。これにより、regexp.test という名前の実行可能ファイルが生成されます。

はじめに

GDB を起動し、regexp.test をデバッグします

$ gdb regexp.test
GNU gdb (GDB) 7.2-gg8
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv  3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
Type "show copying" and "show warranty" for licensing/warranty details.
This GDB was configured as "x86_64-linux".

Reading symbols from  /home/user/go/src/regexp/regexp.test...
done.
Loading Go Runtime support.
(gdb)

"Go ランタイムサポートの読み込み中" というメッセージは、GDB が $GOROOT/src/runtime/runtime-gdb.py から拡張機能を読み込んだことを意味します。

GDB が Go ランタイムソースと付属のサポートスクリプトを見つけられるようにするには、'-d' フラグを付けて $GOROOT を渡します

$ gdb regexp.test -d $GOROOT

何らかの理由で GDB がまだそのディレクトリまたはそのスクリプトを見つけることができない場合は、gdb に指示することで手動でロードできます (go ソースが ~/go/ にあると仮定します)

(gdb) source ~/go/src/runtime/runtime-gdb.py
Loading Go Runtime support.

ソースの検査

"l" または "list" コマンドを使用してソースコードを検査します。

(gdb) l

関数名を使用して "list" をパラメータ化し、ソースの特定の部分をリストします (パッケージ名で修飾する必要があります)。

(gdb) l main.main

特定のファイルと行番号をリストする

(gdb) l regexp.go:1
(gdb) # Hit enter to repeat last command. Here, this lists next 10 lines.

命名

変数名と関数名は、それらが属するパッケージの名前で修飾する必要があります。 regexp パッケージの Compile 関数は、GDB では 'regexp.Compile' として認識されます。

メソッドは、レシーバータイプの名前で修飾する必要があります。たとえば、*Regexp タイプの String メソッドは 'regexp.(*Regexp).String' として認識されます。

他の変数をシャドウイングする変数は、デバッグ情報で魔法のように番号が追加されます。クロージャによって参照される変数は、魔法のように '&' が前に付いたポインタとして表示されます。

ブレークポイントの設定

TestFind 関数にブレークポイントを設定する

(gdb) b 'regexp.TestFind'
Breakpoint 1 at 0x424908: file /home/user/go/src/regexp/find_test.go, line 148.

プログラムを実行する

(gdb) run
Starting program: /home/user/go/src/regexp/regexp.test

Breakpoint 1, regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148
148	func TestFind(t *testing.T) {

ブレークポイントで実行が一時停止しました。実行中のゴルーチンと、それらが何をしているかを確認します

(gdb) info goroutines
  1  waiting runtime.gosched
* 13  running runtime.goexit

* でマークされたものが現在のゴルーチンです。

スタックの検査

プログラムを一時停止した場所のスタックトレースを確認します

(gdb) bt  # backtrace
#0  regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148
#1  0x000000000042f60b in testing.tRunner (t=0xf8404a89c0, test=0x573720) at /home/user/go/src/testing/testing.go:156
#2  0x000000000040df64 in runtime.initdone () at /home/user/go/src/runtime/proc.c:242
#3  0x000000f8404a89c0 in ?? ()
#4  0x0000000000573720 in ?? ()
#5  0x0000000000000000 in ?? ()

もう 1 つのゴルーチン (番号 1) は runtime.gosched でスタックし、チャネル受信でブロックされています

(gdb) goroutine 1 bt
#0  0x000000000040facb in runtime.gosched () at /home/user/go/src/runtime/proc.c:873
#1  0x00000000004031c9 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
 at  /home/user/go/src/runtime/chan.c:342
#2  0x0000000000403299 in runtime.chanrecv1 (t=void, c=void) at/home/user/go/src/runtime/chan.c:423
#3  0x000000000043075b in testing.RunTests (matchString={void (struct string, struct string, bool *, error *)}
 0x7ffff7f9ef60, tests=  []testing.InternalTest = {...}) at /home/user/go/src/testing/testing.go:201
#4  0x00000000004302b1 in testing.Main (matchString={void (struct string, struct string, bool *, error *)}
 0x7ffff7f9ef80, tests= []testing.InternalTest = {...}, benchmarks= []testing.InternalBenchmark = {...})
at /home/user/go/src/testing/testing.go:168
#5  0x0000000000400dc1 in main.main () at /home/user/go/src/regexp/_testmain.go:98
#6  0x00000000004022e7 in runtime.mainstart () at /home/user/go/src/runtime/amd64/asm.s:78
#7  0x000000000040ea6f in runtime.initdone () at /home/user/go/src/runtime/proc.c:243
#8  0x0000000000000000 in ?? ()

スタックフレームは、予想どおり、現在 regexp.TestFind 関数を実行していることを示しています。

(gdb) info frame
Stack level 0, frame at 0x7ffff7f9ff88:
 rip = 0x425530 in regexp.TestFind (/home/user/go/src/regexp/find_test.go:148);
    saved rip 0x430233
 called by frame at 0x7ffff7f9ffa8
 source language minimal.
 Arglist at 0x7ffff7f9ff78, args: t=0xf840688b60
 Locals at 0x7ffff7f9ff78, Previous frame's sp is 0x7ffff7f9ff88
 Saved registers:
  rip at 0x7ffff7f9ff80

コマンド info locals は、関数にローカルなすべての変数とその値をリストしますが、初期化されていない変数も出力しようとするため、使用するのは少し危険です。初期化されていないスライスは、gdb が任意の大きな配列を出力しようとすることがあります。

関数の引数

(gdb) info args
t = 0xf840688b60

引数を出力するとき、それが Regexp 値へのポインタであることに注意してください。GDB は誤って型名の右側に * を配置し、従来の C スタイルで 'struct' キーワードを作成したことに注意してください。

(gdb) p re
(gdb) p t
$1 = (struct testing.T *) 0xf840688b60
(gdb) p t
$1 = (struct testing.T *) 0xf840688b60
(gdb) p *t
$2 = {errors = "", failed = false, ch = 0xf8406f5690}
(gdb) p *t->ch
$3 = struct hchan<*testing.T>

その struct hchan<*testing.T> は、チャネルのランタイム内部表現です。現在空です。そうでない場合、gdb はその内容をきれいに印刷していたでしょう。

前進する

(gdb) n  # execute next line
149             for _, test := range findTests {
(gdb)    # enter is repeat
150                     re := MustCompile(test.pat)
(gdb) p test.pat
$4 = ""
(gdb) p re
$5 = (struct regexp.Regexp *) 0xf84068d070
(gdb) p *re
$6 = {expr = "", prog = 0xf840688b80, prefix = "", prefixBytes =  []uint8, prefixComplete = true,
  prefixRune = 0, cond = 0 '\000', numSubexp = 0, longest = false, mu = {state = 0, sema = 0},
  machine =  []*regexp.machine}
(gdb) p *re->prog
$7 = {Inst =  []regexp/syntax.Inst = {{Op = 5 '\005', Out = 0, Arg = 0, Rune =  []int}, {Op =
    6 '\006', Out = 2, Arg = 0, Rune =  []int}, {Op = 4 '\004', Out = 0, Arg = 0, Rune =  []int}},
  Start = 1, NumCap = 2}

"s" を使用して String 関数呼び出しにステップインできます

(gdb) s
regexp.(*Regexp).String (re=0xf84068d070, noname=void) at /home/user/go/src/regexp/regexp.go:97
97      func (re *Regexp) String() string {

スタックトレースを取得して、現在地を確認します

(gdb) bt
#0  regexp.(*Regexp).String (re=0xf84068d070, noname=void)
    at /home/user/go/src/regexp/regexp.go:97
#1  0x0000000000425615 in regexp.TestFind (t=0xf840688b60)
    at /home/user/go/src/regexp/find_test.go:151
#2  0x0000000000430233 in testing.tRunner (t=0xf840688b60, test=0x5747b8)
    at /home/user/go/src/testing/testing.go:156
#3  0x000000000040ea6f in runtime.initdone () at /home/user/go/src/runtime/proc.c:243
....

ソースコードを見る

(gdb) l
92              mu      sync.Mutex
93              machine []*machine
94      }
95
96      // String returns the source text used to compile the regular expression.
97      func (re *Regexp) String() string {
98              return re.expr
99      }
100
101     // Compile parses a regular expression and returns, if successful,

プリティ印刷

GDB のプリティ印刷メカニズムは、型名に対する正規表現の一致によってトリガーされます。スライスの例

(gdb) p utf
$22 =  []uint8 = {0 '\000', 0 '\000', 0 '\000', 0 '\000'}

スライス、配列、および文字列は C ポインタではないため、GDB は添え字操作を解釈できませんが、ランタイム表現の内部を見てそれを行うことができます (タブ補完が役立ちます)

(gdb) p slc
$11 =  []int = {0, 0}
(gdb) p slc-><TAB>
array  slc    len
(gdb) p slc->array
$12 = (int *) 0xf84057af00
(gdb) p slc->array[1]
$13 = 0

拡張関数 $len と $cap は、文字列、配列、およびスライスで機能します

(gdb) p $len(utf)
$23 = 4
(gdb) p $cap(utf)
$24 = 4

チャネルとマップは「参照」型であり、gdb は C++ のような型 hash<int,string>* へのポインタとして表示します。逆参照すると、プリティ印刷がトリガーされます

インターフェースはランタイムでは、型記述子へのポインタと値へのポインタとして表されます。Go GDB ランタイム拡張機能はこれをデコードし、ランタイム型に対して自動的にプリティ印刷をトリガーします。拡張関数 $dtype は動的型をデコードします (例は regexp.go の 293 行目のブレークポイントから取得されます)。

(gdb) p i
$4 = {str = "cbb"}
(gdb) whatis i
type = regexp.input
(gdb) p $dtype(i)
$26 = (struct regexp.inputBytes *) 0xf8400b4930
(gdb) iface i
regexp.input: struct regexp.inputBytes *