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
を設定することにより、プログラムのクラッシュ時にダンプをトリガーできます (詳細については、ランタイムパッケージのドキュメントを参照してください)。
一般的な操作
- コードのファイルと行番号を表示し、ブレークポイントを設定して逆アセンブルする
(gdb) list (gdb) list line (gdb) list file.go:line (gdb) break line (gdb) break file.go:line (gdb) disas
- バックトレースを表示し、スタックフレームを巻き戻す
(gdb) bt (gdb) frame n
- ローカル変数、引数、および戻り値の名前、型、およびスタックフレーム上の場所を表示する
(gdb) info locals (gdb) info args (gdb) p variable (gdb) whatis variable
- グローバル変数の名前、型、および場所を表示する
(gdb) info variables regexp
Go 拡張機能
GDB の最近の拡張メカニズムにより、特定のバイナリ用の拡張スクリプトを読み込むことができます。ツールチェーンはこれを使用して、ランタイムコードの内部 (ゴルーチンなど) を検査し、組み込みのマップ、スライス、およびチャネルタイプをきれいに印刷するための一握りのコマンドで GDB を拡張します。
- 文字列、スライス、マップ、チャネル、またはインターフェースをきれいに印刷する
(gdb) p var
- 文字列、スライス、およびマップの $len() および $cap() 関数
(gdb) p $len(var)
- インターフェースを動的型にキャストする関数
(gdb) p $dtype(var) (gdb) iface var
既知の問題: インターフェース値の長い名前が短い名前と異なる場合、GDB はインターフェース値の動的型を自動的に見つけることができません (スタックトレースを出力するときに厄介です。プリティプリンタは短い型名とポインタを出力します)。
- ゴルーチンの検査
(gdb) info goroutines (gdb) goroutine n cmd (gdb) help goroutine
例えば(gdb) goroutine 12 bt
特定のゴルーチンの ID の代わりにall
を渡すことで、すべてのゴルーチンを検査できます。例えば(gdb) goroutine all bt
これがどのように機能するかを確認したい場合、または拡張したい場合は、Go ソースディストリビューションの src/runtime/runtime-gdb.py をご覧ください。リンカ (src/cmd/link/internal/ld/dwarf.go) によって DWARF コードで記述されることが保証されている特別なマジックタイプ (hash<T,U>
) と変数 (runtime.m
と runtime.g
) に依存しています。
デバッグ情報がどのように見えるかを知りたい場合は、objdump
-W
a.out
を実行し、.debug_*
セクションを参照してください。
既知の問題
- 文字列のプリティ印刷は、型文字列に対してのみトリガーされ、それから派生した型に対してはトリガーされません。
- ランタイムライブラリの C 部分の型情報がありません。
- GDB は Go の名前の修飾を理解しておらず、
"fmt.Print"
を引用符で囲む必要がある"."
を持つ構造化されていないリテラルとして扱います。pkg.(*MyType).Meth
形式のメソッド名にはさらに強く反対します。 - 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 *