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

[インデックス 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の公式ドキュメント (Go Runtime, pprof, deferに関する情報)
  • ARMアーキテクチャのリファレンスマニュアル (PC, SP, LRレジスタに関する情報)
  • Goのソースコード (特にsrc/pkg/runtime/ディレクトリ内のファイル)
  • GoのIssueトラッカー (Issue #8153の詳細)
  • Goのコードレビューシステム (CL 107970043の詳細)
  • アトミック操作に関する一般的なコンピュータサイエンスの知識