[インデックス 19530] ファイルの概要
このコミットは、GoランタイムのARMアーキテクチャにおけるプロファイリングトレースバックの挙動を修正するものです。具体的には、jmpdefer
関数を通過する際のトレースバックの信頼性を向上させ、不整合な実行スナップショットによる問題を防ぎます。
変更されたファイルは以下の通りです。
src/pkg/runtime/asm_arm.s
: ARMアセンブリコード。jmpdefer
関数のコメントが追加されています。src/pkg/runtime/traceback_arm.c
: ARMアーキテクチャ向けのトレースバック処理を記述したCコード。runtime·gentraceback
関数にjmpdefer
に関するチェックが追加されています。
コミット
- コミットハッシュ:
597f87c997dcaa86227725e227f5eb59721a0129
- 作者: Russ Cox rsc@golang.org
- コミット日時: 2014年6月12日 木曜日 16:34:54 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/597f87c997dcaa86227725e227f5eb59721a0129
元コミット内容
runtime: do not trace past jmpdefer during pprof traceback on arm
jmpdefer modifies PC, SP, and LR, and not atomically,
so walking past jmpdefer will often end up in a state
where the three are not a consistent execution snapshot.
This was causing warning messages a few frames later
when the traceback realized it was confused, but given
the right memory it could easily crash instead.
Update #8153
LGTM=minux, iant
R=golang-codereviews, minux, iant
CC=golang-codereviews, r
https://golang.org/cl/107970043
変更の背景
このコミットの背景には、Goプログラムのプロファイリングツール(pprof
)が生成するトレースバックの信頼性の問題がありました。特にARMアーキテクチャにおいて、jmpdefer
というランタイム関数がプログラムカウンタ(PC)、スタックポインタ(SP)、リンクレジスタ(LR)といった重要なレジスタを非アトミックに(つまり、一度にすべてではなく、個別に)変更していました。
プロファイリング中にトレースバック(スタックトレース)を生成する際、システムは現在の実行状態のスナップショットを正確に取得する必要があります。しかし、jmpdefer
がレジスタを非アトミックに変更している最中にプロファイリングの割り込みが発生すると、PC、SP、LRが互いに整合性の取れない状態になる可能性がありました。
このような不整合な状態は、トレースバック処理が混乱し、警告メッセージを出力する原因となっていました。さらに悪いことに、特定のメモリ状態によっては、プログラムがクラッシュする可能性も秘めていました。この問題はGoのIssue #8153として報告されており、このコミットはその問題を解決するために導入されました。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
- Goランタイム (Go Runtime): Goプログラムは、コンパイルされたバイナリだけでなく、ガベージコレクション、スケジューリング、スタック管理、プリミティブな同期メカニズムなどを提供するランタイムシステムと連携して動作します。このランタイムはGo言語で書かれている部分と、Cやアセンブリ言語で書かれている部分があります。
- プロファイリング (Profiling): プロファイリングとは、プログラムの実行時の振る舞いを測定し、パフォーマンスのボトルネックやリソースの使用状況を特定するプロセスです。Goでは、
pprof
ツールがCPU使用率、メモリ割り当て、ゴルーチンブロックなどのプロファイリングデータを提供します。 - トレースバック (Traceback) / スタックトレース (Stack Trace): プログラムが実行されている時点での関数呼び出しの連鎖(コールスタック)をリストアップしたものです。デバッグやプロファイリングにおいて、どの関数がどの関数を呼び出したか、そして現在どの関数が実行されているかを把握するために不可欠です。
- ARMアーキテクチャ (ARM Architecture): スマートフォン、タブレット、組み込みシステムなどで広く使用されているCPUアーキテクチャです。x86とは異なるレジスタセットや命令セットを持っています。
- レジスタ (Registers): CPU内部にある高速な記憶領域で、現在処理中のデータや命令のアドレスなどを一時的に保持します。
- PC (Program Counter): 次に実行される命令のアドレスを保持するレジスタです。
- SP (Stack Pointer): 現在のスタックフレームの最上位(または最下位)のアドレスを指すレジスタです。関数呼び出しやローカル変数の管理に不可欠です。
- LR (Link Register): ARMアーキテクチャ特有のレジスタで、関数呼び出し(
BL
命令)の際に、呼び出し元に戻るアドレス(リターンアドレス)を保持します。
jmpdefer
: Goランタイム内部の関数で、defer
ステートメントの実行に関連するジャンプ処理を行います。defer
はGoの重要な機能の一つで、関数の終了時に特定の処理を実行することを保証します。jmpdefer
は、defer
呼び出しのスタックフレームを調整し、適切なdefer
関数に制御を移す役割を担います。- アトミック操作 (Atomic Operation): 複数の操作が不可分な単一の操作として実行されることを保証するものです。つまり、その操作が完了するまで、他のスレッドや割り込みによって中断されることがありません。非アトミックな操作は、途中で中断されるとデータが不整合な状態になる可能性があります。
技術的詳細
このコミットの技術的な核心は、ARMアーキテクチャにおけるjmpdefer
関数のレジスタ操作と、プロファイリング時のトレースバック生成のタイミングの問題にあります。
Goランタイムのjmpdefer
関数は、defer
呼び出しの処理中にPC、SP、LRレジスタを更新します。しかし、これらの更新はアトミックに行われるわけではありませんでした。つまり、PCが更新された直後にSPが更新される前にプロファイリングの割り込みが発生すると、トレースバック処理はPCとSPが異なる時点の値を参照することになり、結果として不整合なスタックフレームを構築してしまいます。
この不整合は、トレースバックがスタックを遡って関数呼び出しの連鎖を再構築する際に問題を引き起こします。誤ったレジスタ値に基づいてスタックフレームを解釈しようとすると、トレースバックが途中で停止したり、誤った情報を報告したり、最悪の場合、プログラムがクラッシュする可能性がありました。
このコミットでは、runtime·gentraceback
関数(トレースバックを生成するGoランタイムの内部関数)に、jmpdefer
関数内でのプロファイリング割り込みを検出した場合の特別な処理を追加することで、この問題を回避しています。具体的には、トレースバック中に現在のPCがjmpdefer
関数のエントリポイント内にあると判断された場合、それ以上トレースバックを続行しないようにします。これにより、不整合なレジスタ状態でのトレースバック処理を避け、安定性を向上させています。
コミットメッセージにある「This check can be deleted if jmpdefer is changed to restore all three atomically using pop.」というコメントは、将来的にjmpdefer
がPC、SP、LRをアトミックに(例えば、ARMのPOP
命令のように複数のレジスタを一度に復元する命令を使って)更新するように変更されれば、この回避策は不要になるという見通しを示しています。
コアとなるコードの変更箇所
src/pkg/runtime/asm_arm.s
--- a/src/pkg/runtime/asm_arm.s
+++ b/src/pkg/runtime/asm_arm.s
@@ -394,6 +394,10 @@ TEXT runtime·lessstack(SB), NOSPLIT, $-4-0
// 1. grab stored LR for caller
// 2. sub 4 bytes to get back to BL deferreturn
// 3. B to fn
+// TODO(rsc): Push things on stack and then use pop
+// to load all registers simultaneously, so that a profiling
+// interrupt can never see mismatched SP/LR/PC.
+// (And double-check that pop is atomic in that way.)
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-8
MOVW 0(SP), LR
MOVW $-4(LR), LR // BL deferreturn
この変更は、runtime·jmpdefer
関数のアセンブリコードにコメントを追加したものです。これは直接的な機能変更ではありませんが、jmpdefer
がレジスタを非アトミックに更新していること、そして将来的にアトミックな更新(例えばPOP
命令の使用)を検討すべきであるという開発者の意図を示しています。これにより、プロファイリング割り込み時にSP/LR/PCの不整合が発生する可能性を指摘しています。
src/pkg/runtime/traceback_arm.c
--- a/src/pkg/runtime/traceback_arm.c
+++ b/src/pkg/runtime/traceback_arm.c
@@ -110,6 +110,19 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\n \tif(runtime·topofstack(f)) {\n \t\t\tframe.lr = 0;\n \t\t\tflr = nil;\n+\t\t} else if(f->entry == (uintptr)runtime·jmpdefer) {\n+\t\t\t// jmpdefer modifies SP/LR/PC non-atomically.\n+\t\t\t// If a profiling interrupt arrives during jmpdefer,\n+\t\t\t// the stack unwind may see a mismatched register set\n+\t\t\t// and get confused. Stop if we see PC within jmpdefer\n+\t\t\t// to avoid that confusion.\n+\t\t\t// See golang.org/issue/8153.\n+\t\t\t// This check can be deleted if jmpdefer is changed\n+\t\t\t// to restore all three atomically using pop.\n+\t\t\tif(callback != nil)\n+\t\t\t\truntime·throw(\"traceback_arm: found jmpdefer when tracing with callback\");\n+\t\t\tframe.lr = 0;\n+\t\t\tflr = nil;\n \t\t} else {\n \t\t\tif((n == 0 && frame.sp < frame.fp) || frame.lr == 0)\n \t\t\t\tframe.lr = *(uintptr*)frame.sp;\n```
この変更は、`runtime·gentraceback`関数内のスタックフレームを遡るロジックに新しい条件分岐を追加しています。
## コアとなるコードの解説
`src/pkg/runtime/traceback_arm.c`の変更がこのコミットの主要な機能変更です。
`runtime·gentraceback`関数は、Goのプロファイリングツール(`pprof`)がスタックトレースを生成する際に呼び出されるGoランタイムの内部関数です。この関数は、与えられたPC(プログラムカウンタ)、SP(スタックポインタ)、LR(リンクレジスタ)から開始して、コールスタックを遡り、各関数の情報を収集します。
追加されたコードブロックは以下の通りです。
```c
+ } else if(f->entry == (uintptr)runtime·jmpdefer) {
+ // jmpdefer modifies SP/LR/PC non-atomically.
+ // If a profiling interrupt arrives during jmpdefer,
+ // the stack unwind may see a mismatched register set
+ // and get confused. Stop if we see PC within jmpdefer
+ // to avoid that confusion.
+ // See golang.org/issue/8153.
+ // This check can be deleted if jmpdefer is changed
+ // to restore all three atomically using pop.
+ if(callback != nil)
+ runtime·throw("traceback_arm: found jmpdefer when tracing with callback");
+ frame.lr = 0;
+ flr = nil;
このコードは、現在のスタックフレームのentry
ポイント(つまり、その関数が開始するアドレス)がruntime·jmpdefer
関数のアドレスと一致するかどうかをチェックしています。
f->entry == (uintptr)runtime·jmpdefer
: これは、現在トレースバックしている関数がjmpdefer
であるかどうかを判断する条件です。- コメントで説明されているように、
jmpdefer
はPC、SP、LRを非アトミックに修正するため、プロファイリング割り込みがその途中で発生すると、スタックアンワインド(スタックを遡る処理)が不整合なレジスタセットを見て混乱する可能性があります。 - このため、
jmpdefer
内でPCが見つかった場合、それ以上のトレースバックを停止します。これは、不整合な状態での処理を避け、クラッシュや誤ったトレースバック情報の生成を防ぐための回避策です。 if(callback != nil) runtime·throw(...)
: もしトレースバックにコールバック関数が指定されている場合(これは通常、より詳細なデバッグや特殊なプロファイリングシナリオで使用されます)、jmpdefer
内でトレースバックが停止したことを示すエラーをスローします。これは、このような状況での予期せぬ挙動を防ぐための安全策です。frame.lr = 0; flr = nil;
: トレースバックを停止するために、リンクレジスタ(リターンアドレス)をゼロに設定し、関連するポインタをnil
に設定します。これにより、これ以上スタックを遡ることができなくなり、トレースバックが終了します。
この変更により、ARMアーキテクチャにおけるpprof
のトレースバックが、jmpdefer
の非アトミックなレジスタ操作によって引き起こされる不整合な状態に遭遇しても、安定して動作するようになります。
関連リンク
- Go Issue #8153: https://golang.org/issue/8153
- Go CL 107970043: https://golang.org/cl/107970043
参考にした情報源リンク
- Goの公式ドキュメント (Go Runtime, pprof, deferに関する情報)
- ARMアーキテクチャのリファレンスマニュアル (PC, SP, LRレジスタに関する情報)
- Goのソースコード (特に
src/pkg/runtime/
ディレクトリ内のファイル) - GoのIssueトラッカー (Issue #8153の詳細)
- Goのコードレビューシステム (CL 107970043の詳細)
- アトミック操作に関する一般的なコンピュータサイエンスの知識