はじめに

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

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

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

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

一般的な操作

  • コードのファイルと行番号を表示し、ブレークポイントを設定し、逆アセンブルします:
    1. (gdb) list
    2. (gdb) list line
    3. (gdb) list file.go:line
    4. (gdb) break line
    5. (gdb) break file.go:line
    6. (gdb) disas
  • バックトレースを表示し、スタックフレームをアンワインドします:
    1. (gdb) bt
    2. (gdb) frame n
  • ローカル変数、引数、戻り値の名前、型、スタックフレーム上の位置を表示します:
    1. (gdb) info locals
    2. (gdb) info args
    3. (gdb) p variable
    4. (gdb) whatis variable
  • グローバル変数の名前、型、位置を表示します:

    1. (gdb) info variables regexp

Go拡張

最近のGDBの拡張メカニズムにより、特定のバイナリ用の拡張スクリプトをロードできるようになりました。ツールチェーンはこれを使用して、ランタイムコード(goroutinesなど)の内部を検査するためのいくつかのコマンドをGDBに追加し、組み込みのマップ、スライス、チャネル型をきれいに表示します。

  • 文字列、スライス、マップ、チャネル、またはインターフェースをきれいに表示します:
    1. (gdb) p var
  • 文字列、スライス、マップ用の$len()および$cap()関数:
    1. (gdb) p $len(var)
  • インターフェースをその動的型にキャストする関数:
    1. (gdb) p $dtype(var)
    2. (gdb) iface var
    既知の問題: GDBは、インターフェース値の長い名前が短い名前と異なる場合、動的型を自動的に見つけることができません(スタックトレースを印刷する際に煩わしいことがあります。きれいなプリンターは短い型名とポインタを印刷することに戻ります)。
  • goroutinesを検査します:

    1. (gdb) info goroutines
    2. (gdb) goroutine n cmd
    3. (gdb) help goroutine

    例えば:

    1. (gdb) goroutine 12 bt

    特定のgoroutineのIDの代わりにallを渡すことで、すべてのgoroutineを検査できます。例えば:

    1. (gdb) goroutine all bt

    これがどのように機能するかを見たい場合、または拡張したい場合は、Goソース配布のsrc/runtime/runtime-gdb.pyを確認してください。これは、リンカ([src/cmd/link/internal/ld/dwarf.go](https://golang.org/src/cmd/link/internal/ld/dwarf.go)がDWARFコードで説明されることを保証するいくつかの特別なマジック型(`````hash)および変数(runtime.mおよびruntime.g`````)に依存しています。

    デバッグ情報がどのように見えるかに興味がある場合は、objdump -W a.outを実行し、.debug_*セクションをブラウズしてください。

既知の問題

  • 1. 文字列のきれいな表示は、型stringに対してのみトリガーされ、そこから派生した型にはトリガーされません。
  • 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をデバッグします:

  1. $ gdb regexp.test
  2. GNU gdb (GDB) 7.2-gg8
  3. Copyright (C) 2010 Free Software Foundation, Inc.
  4. License GPLv 3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
  5. Type "show copying" and "show warranty" for licensing/warranty details.
  6. This GDB was configured as "x86_64-linux".
  7. Reading symbols from /home/user/go/src/regexp/regexp.test...
  8. done.
  9. Loading Go Runtime support.
  10. (gdb)

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

GDBがGoランタイムソースとそれに付随するサポートスクリプトを見つけるのを助けるために、$GOROOT'-d'フラグで渡します:

  1. $ gdb regexp.test -d $GOROOT

何らかの理由でGDBがそのディレクトリやスクリプトを見つけられない場合は、手動でgdbに読み込ませることができます(Goソースが~/go/にあると仮定します):

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

ソースの検査

  1. ``````bash
  2. (gdb) l
  3. `

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

  1. (gdb) l main.main

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

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

命名

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

メソッドは、その受信者型の名前で修飾する必要があります。たとえば、*Regexp型のStringメソッドは'regexp.(*Regexp).String'として知られています。

他の変数を隠す変数は、デバッグ情報で魔法のように番号が付けられます。クロージャによって参照される変数は、魔法のように’&’でプレフィックスされたポインタとして表示されます。

ブレークポイントの設定

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

プログラムを実行します:

  1. (gdb) run
  2. Starting program: /home/user/go/src/regexp/regexp.test
  3. Breakpoint 1, regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148
  4. 148 func TestFind(t *testing.T) {

実行はブレークポイントで一時停止しました。どのgoroutinesが実行中で、何をしているかを確認します:

  1. (gdb) info goroutines
  2. 1 waiting runtime.gosched
  3. * 13 running runtime.goexit
  1. <a name="Inspecting_the_stack"></a>
  2. ### スタックの検査
  3. プログラムが一時停止している場所のスタックトレースを確認します:
  4. ``````bash
  5. (gdb) bt # backtrace
  6. #0 regexp.TestFind (t=0xf8404a89c0) at /home/user/go/src/regexp/find_test.go:148
  7. #1 0x000000000042f60b in testing.tRunner (t=0xf8404a89c0, test=0x573720) at /home/user/go/src/testing/testing.go:156
  8. #2 0x000000000040df64 in runtime.initdone () at /home/user/go/src/runtime/proc.c:242
  9. #3 0x000000f8404a89c0 in ?? ()
  10. #4 0x0000000000573720 in ?? ()
  11. #5 0x0000000000000000 in ?? ()
  12. `

他のgoroutine、番号1はruntime.goschedでスタックしており、チャネル受信でブロックされています:

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

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

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

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

関数の引数:

  1. (gdb) info args
  2. t = 0xf840688b60

引数を印刷する際、Regexp値へのポインタであることに注意してください。GDBは型名の右側に*を誤って配置し、伝統的なCスタイルで’構造体’キーワードを作成しました。

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

struct hchan<*testing.T>は、ランタイム内部のチャネルの表現です。現在は空であり、gdbはその内容をきれいに印刷していません。

ステップフォワード:

  1. (gdb) n # execute next line
  2. 149 for _, test := range findTests {
  3. (gdb) # enter is repeat
  4. 150 re := MustCompile(test.pat)
  5. (gdb) p test.pat
  6. $4 = ""
  7. (gdb) p re
  8. $5 = (struct regexp.Regexp *) 0xf84068d070
  9. (gdb) p *re
  10. $6 = {expr = "", prog = 0xf840688b80, prefix = "", prefixBytes = []uint8, prefixComplete = true,
  11. prefixRune = 0, cond = 0 '\000', numSubexp = 0, longest = false, mu = {state = 0, sema = 0},
  12. machine = []*regexp.machine}
  13. (gdb) p *re->prog
  14. $7 = {Inst = []regexp/syntax.Inst = {{Op = 5 '\005', Out = 0, Arg = 0, Rune = []int}, {Op =
  15. 6 '\006', Out = 2, Arg = 0, Rune = []int}, {Op = 4 '\004', Out = 0, Arg = 0, Rune = []int}},
  16. Start = 1, NumCap = 2}
  1. ``````bash
  2. (gdb) s
  3. regexp.(*Regexp).String (re=0xf84068d070, noname=void) at /home/user/go/src/regexp/regexp.go:97
  4. 97 func (re *Regexp) String() string {
  5. `

現在地を確認するためにスタックトレースを取得します:

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

ソースコードを確認します:

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

きれいな表示

GDBのきれいな表示メカニズムは、型名に対する正規表現の一致によってトリガーされます。スライスの例:

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

スライス、配列、文字列はCポインタではないため、GDBはサブスクリプション操作を解釈できませんが、ランタイム表現の内部を確認することができます(タブ補完が役立ちます):

  1. (gdb) p slc
  2. $11 = []int = {0, 0}
  3. (gdb) p slc-><TAB>
  4. array slc len
  5. (gdb) p slc->array
  6. $12 = (int *) 0xf84057af00
  7. (gdb) p slc->array[1]
  8. $13 = 0

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

  1. (gdb) p $len(utf)
  2. $23 = 4
  3. (gdb) p $cap(utf)
  4. $24 = 4

チャネルとマップは’参照’型であり、gdbはC++のような型hash<int,string>*へのポインタとして表示します。逆参照はきれいな表示をトリガーします

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

  1. (gdb) p i
  2. $4 = {str = "cbb"}
  3. (gdb) whatis i
  4. type = regexp.input
  5. (gdb) p $dtype(i)
  6. $26 = (struct regexp.inputBytes *) 0xf8400b4930
  7. (gdb) iface i
  8. regexp.input: struct regexp.inputBytes *