Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18530] ファイルの概要

このコミットは、Goランタイムにおけるデバッガ(特にGDB)との連携に関する修正です。具体的には、トレースバック処理中にGDBが挿入したブレークポイント(0xcc命令)に遭遇した場合のプログラムカウンタ(PC)の挙動を調整し、GDBの期待する実行フローを維持することを目的としています。

コミット

commit 92b4741728faca77329b204340b02e8df4f4d097
Author: Ian Lance Taylor <iant@golang.org>
Date:   Fri Feb 14 11:06:53 2014 -0800

    runtime: if traceback sees a breakpoint, don't change the PC
    
    Changing the PC confuses gdb, because execution does not
    continue where gdb expects it.  Not changing the PC has the
    potential to confuse a stack dump, but when running under gdb
    it seems better to confuse a stack dump than to confuse gdb.
    
    Fixes #6776.
    
    LGTM=rsc
    R=golang-codereviews, dvyukov, rsc
    CC=golang-codereviews
    https://golang.org/cl/49580044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/92b4741728faca77329b204340b02e8df4f4d097

元コミット内容

このコミットは、Goランタイムがトレースバック処理中にGDBによって挿入されたブレークポイント(0xcc命令)を検出した場合のプログラムカウンタ(PC)の扱いを変更します。以前は、ブレークポイントに遭遇すると、runtime·findfuncを使用して関数のエントリポイントにPCをリセットしていました。しかし、この挙動はGDBの期待する実行フローと衝突し、GDBが混乱する原因となっていました。

新しい挙動では、0xcc命令を検出してもgobuf->pc(プログラムカウンタ)を変更しません。これにより、runtime·rewindmorestack関数から戻った際に、GDBが挿入したブレークポイント命令が再度実行され、GDBが期待通りにブレークポイントで停止できるようになります。この変更は、スタックダンプの正確性を犠牲にする可能性がありますが、GDB下でのデバッグ体験を優先しています。

変更の背景

この変更の背景には、GoプログラムをGDBでデバッグする際のユーザー体験の改善があります。Goランタイムは、スタックの巻き戻し(unwinding)やトレースバックのために、特定の状況下でプログラムカウンタ(PC)を調整することがあります。特に、runtime·rewindmorestackのような関数は、スタックフレームの調整や関数のエントリポイントへのジャンプを行う際にPCを変更する可能性があります。

しかし、GDBのような外部デバッガは、プログラムの実行フローを厳密に追跡し、ブレークポイントで停止することを期待しています。GDBはブレークポイントを実装するために、通常、ブレークポイントが設定されたアドレスの命令をint3命令(x86アーキテクチャでは0xcc)に置き換えます。プログラムがこの0xcc命令に到達すると、CPUは割り込みを発生させ、GDBに制御が渡ります。

従来のGoランタイムの挙動では、runtime·rewindmorestack0xcc命令を検出すると、それがブレークポイントであると認識し、runtime·findfuncを使ってその命令が属する関数のエントリポイントにPCを強制的に戻していました。この「PCの変更」が問題でした。GDBは0xcc命令で停止することを期待しているにもかかわらず、ランタイムがPCを別の場所(関数のエントリ)に移動させてしまうため、GDBはブレークポイントを「見失い」、デバッグセッションが混乱したり、期待通りに停止しなかったりする問題が発生していました。

この問題はGoのIssue #6776として報告されており、このコミットはその問題を解決するために行われました。コミットメッセージにもあるように、「PCを変更するとGDBが混乱する」という点がこの変更の主要な動機です。スタックダンプの正確性が多少犠牲になったとしても、GDBでのデバッグの信頼性を優先するという判断がなされました。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念を把握しておく必要があります。

  1. プログラムカウンタ (PC):

    • CPUのレジスタの一つで、次に実行される命令のアドレスを保持しています。プログラムの実行フローを制御する上で非常に重要な役割を果たします。
    • Goランタイムやデバッガは、このPCの値を操作することで、実行フローを制御したり、スタックトレースを生成したりします。
  2. ブレークポイント (Breakpoint):

    • デバッグ中にプログラムの実行を一時停止させるための目印です。デバッガはブレークポイントに到達すると、プログラムの実行を停止し、ユーザーに制御を戻します。
    • x86アーキテクチャでは、ソフトウェアブレークポイントは通常、ブレークポイントを設定したい命令をint3命令(オペコード0xcc)に置き換えることで実装されます。int3は1バイトの命令で、デバッガに制御を渡すための特別な割り込みを発生させます。
  3. GDB (GNU Debugger):

    • Unix系システムで広く使われている強力なコマンドラインデバッガです。C, C++, Go, Fortranなど、多くのプログラミング言語に対応しています。
    • GDBは、プログラムの実行を制御し、メモリやレジスタの内容を検査し、ブレークポイントを設定するなどの機能を提供します。
  4. スタックトレース (Stack Trace) / トレースバック (Traceback):

    • プログラムが特定の時点(例えば、エラー発生時やブレークポイント到達時)で、どの関数がどの関数を呼び出したかを示すリストです。
    • 各エントリは、関数名、ファイル名、行番号、およびその関数が呼び出されたアドレス(リターンアドレス)を含みます。
    • Goランタイムは、runtime.Stack()関数やパニック発生時などにスタックトレースを生成します。この処理には、現在のPCの値やスタックフレームの情報が利用されます。
  5. runtime·rewindmorestack:

    • Goランタイム内部の関数で、スタックの拡張(morestackルーチン)に関連する処理の一部として使用されます。
    • Goの関数呼び出し規約では、スタックが不足した場合にmorestackルーチンが呼び出され、新しいスタックセグメントが割り当てられます。rewindmorestackは、このスタック拡張プロセス中に、元の呼び出し元に戻るためのPCの調整などを行う可能性があります。
    • この関数は、特にデバッグ時やパニック発生時など、通常の実行フローから逸脱した状況でPCの値を「巻き戻す」必要がある場合に関与します。
  6. gobuf構造体:

    • Goランタイム内部で使用される構造体で、ゴルーチン(goroutine)の実行コンテキスト(プログラムカウンタ、スタックポインタなど)を保存するために使われます。
    • gobuf->pcは、そのゴルーチンが次に実行を再開する命令のアドレスを指します。

技術的詳細

このコミットは、src/pkg/runtime/sys_x86.cファイル内のruntime·rewindmorestack関数の挙動を変更しています。この関数は、Goランタイムがスタックの拡張やその他の特殊な状況で、プログラムカウンタ(PC)を調整する必要があるときに呼び出されます。

変更前のコードでは、runtime·rewindmorestackが現在のPCが指す命令の最初のバイトをチェックしていました。もしそのバイトが0xccint3命令、GDBが挿入するブレークポイント)であった場合、ランタイムはruntime·findfunc(gobuf->pc)を呼び出して、そのPCが属する関数を見つけ、gobuf->pcをその関数のエントリポイントに設定し直していました。

この挙動は、ランタイムがブレークポイントを「スキップ」し、関数の先頭から実行を再開させようとする意図があったのかもしれません。しかし、GDBの視点から見ると、これは問題を引き起こします。GDBは0xcc命令でプログラムが停止することを期待しており、停止後にはその0xcc命令を元の命令に戻し、その元の命令を実行してから、次の命令に進むというデバッグセッションの標準的なフローを想定しています。

ランタイムが勝手にPCを関数のエントリポイントに移動させてしまうと、GDBはブレークポイントで停止したと認識できず、デバッグセッションが混乱します。GDBは、ブレークポイントがヒットした後に、そのブレークポイント命令(0xcc)を元の命令に戻し、その元の命令を実行してから、次の命令に進むというデバッグセッションの標準的なフローを想定しています。ランタイムがPCを移動させてしまうと、GDBは元の命令を実行する機会を失い、デバッグセッションが期待通りに進行しなくなります。

このコミットによる変更は、この問題を解決するために、0xcc命令が検出された場合にgobuf->pcを変更しないようにしました。

if(pc[0] == 0xcc) {
    // This is a breakpoint inserted by gdb.  We could use
    // runtime·findfunc to find the function.  But if we
    // do that, then we will continue execution at the
    // function entry point, and we will not hit the gdb
    // breakpoint.  So for this case we don't change
    // gobuf->pc, so that when we return we will execute
    // the jump instruction and carry on.  This means that
    // stack unwinding may not work entirely correctly
    // (http://golang.org/issue/5723) but the user is
    // running under gdb anyhow.
    return;
}

この新しいロジックでは、0xccが検出された場合、runtime·rewindmorestackは単にreturnします。これにより、gobuf->pc0xcc命令のアドレスを指したままになります。runtime·rewindmorestackから戻った後、プログラムは0xcc命令を再実行し、GDBが期待通りにブレークポイントで停止できるようになります。

コミットメッセージにも記載されているように、この変更はスタックダンプの正確性に影響を与える可能性があります(参照:http://golang.org/issue/5723)。これは、runtime·rewindmorestackがPCを調整しないことで、スタックトレースが期待通りの場所を示さない可能性があるためです。しかし、GDB下でデバッグしているユーザーにとっては、スタックダンプのわずかな不正確さよりも、GDBが正しく機能することの方が重要であるという判断がなされました。

コアとなるコードの変更箇所

変更はsrc/pkg/runtime/sys_x86.cファイルに集中しています。

--- a/src/pkg/runtime/sys_x86.c
+++ b/src/pkg/runtime/sys_x86.c
@@ -27,7 +27,6 @@ void
 runtime·rewindmorestack(Gobuf *gobuf)
 {
 	byte *pc;
-\tFunc *f;
 
 	pc = (byte*)gobuf->pc;
 	if(pc[0] == 0xe9) { // jmp 4-byte offset
@@ -38,12 +37,18 @@ runtime·rewindmorestack(Gobuf *gobuf)
 		gobuf->pc = gobuf->pc + 2 + *(int8*)(pc+1);
 		return;
 	}
-\tif(pc[0] == 0xcc) { // breakpoint inserted by gdb
-\t\tf = runtime·findfunc(gobuf->pc);\
-\t\tif(f != nil) {\
-\t\t\tgobuf->pc = f->entry;\
-\t\t\treturn;\
-\t\t}\
+\tif(pc[0] == 0xcc) {
+\t\t// This is a breakpoint inserted by gdb.  We could use
+\t\t// runtime·findfunc to find the function.  But if we
+\t\t// do that, then we will continue execution at the
+\t\t// function entry point, and we will not hit the gdb
+\t\t// breakpoint.  So for this case we don't change
+\t\t// gobuf->pc, so that when we return we will execute
+\t\t// the jump instruction and carry on.  This means that
+\t\t// stack unwinding may not work entirely correctly
+\t\t// (http://golang.org/issue/5723) but the user is
+\t\t// running under gdb anyhow.
+\t\treturn;
 	}
 	runtime·printf("runtime: pc=%p %x %x %x %x %x\n", pc, pc[0], pc[1], pc[2], pc[3], pc[4]);
 	runtime·throw("runtime: misuse of rewindmorestack");

コアとなるコードの解説

変更の核心は、runtime·rewindmorestack関数内のif(pc[0] == 0xcc)ブロックです。

変更前:

if(pc[0] == 0xcc) { // breakpoint inserted by gdb
    f = runtime·findfunc(gobuf->pc);
    if(f != nil) {
        gobuf->pc = f->entry;
        return;
    }
}

このコードは、現在のプログラムカウンタpcが指す命令の最初のバイトが0xcc(GDBが挿入するブレークポイント)であるかをチェックしていました。もしそうであれば、runtime·findfuncを呼び出して、そのPCが属する関数fを見つけ、gobuf->pcをその関数のエントリポイントf->entryに設定し直していました。これは、ランタイムがブレークポイントを「スキップ」し、関数の先頭から実行を再開させようとする意図があったと考えられます。しかし、これがGDBとの連携において問題を引き起こしていました。

変更後:

if(pc[0] == 0xcc) {
    // This is a breakpoint inserted by gdb.  We could use
    // runtime·findfunc to find the function.  But if we
    // do that, then we will continue execution at the
    // function entry point, and we will not hit the gdb
    // breakpoint.  So for this case we don't change
    // gobuf->pc, so that when we return we will execute
    // the jump instruction and carry on.  This means that
    // stack unwinding may not work entirely correctly
    // (http://golang.org/issue/5723) but the user is
    // running under gdb anyhow.
    return;
}

新しいコードでは、0xccが検出された場合、runtime·findfuncの呼び出しも、gobuf->pcの変更も行わず、単にreturnしています。これにより、gobuf->pc0xcc命令のアドレスを指したままになります。

この変更の意図は、コメントに明確に記述されています。

  • 「これはGDBによって挿入されたブレークポイントである。」
  • runtime·findfuncを使って関数を見つけることもできるが、そうすると関数のエントリポイントから実行が継続され、GDBのブレークポイントにヒットしなくなる。」
  • 「したがって、このケースではgobuf->pcを変更しない。これにより、関数から戻ったときに(0xcc)ジャンプ命令が実行され、処理が続行される。」
  • 「これはスタックの巻き戻しが完全に正しく機能しない可能性があることを意味するが(http://golang.org/issue/5723)、ユーザーはとにかくGDB下で実行している。」

つまり、GDBがブレークポイントで停止し、その後のデバッグセッションを正しく制御できるように、GoランタイムがPCを勝手に変更しないようにした、という非常に実用的な修正です。スタックトレースの正確性よりも、デバッガとの協調性を優先したトレードオフと言えます。

関連リンク

  • Go Issue #6776: runtime: gdb breakpoint causes stack trace to be wrong - このコミットが修正した問題の元のIssue。
  • Go Issue #5723: runtime: stack unwinding can be wrong when breakpoint is hit - コミットメッセージで言及されている、スタック巻き戻しの不正確さに関するIssue。
  • Go Code Review 49580044: runtime: if traceback sees a breakpoint, don't change the PC - このコミットのコードレビューページ。

参考にした情報源リンク

  • Goのソースコード(特にsrc/pkg/runtime/sys_x86.c
  • GDBのドキュメント(ブレークポイントの実装に関する情報)
  • x86アーキテクチャの命令セットリファレンス(int3命令に関する情報)
  • GoのIssueトラッカー(#6776, #5723)
  • Goのコードレビューシステム(CL 49580044)
  • 一般的なデバッグとプログラムカウンタに関する知識
  • google_web_search を利用して、Goランタイム、GDB、ブレークポイント、PCの関連情報を調査。