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

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

このコミットは、GoランタイムにおけるARMアーキテクチャでのパニック時のトレースバック処理を改善するものです。特に、リーフ関数(他の関数を呼び出さない関数)内でパニックが発生した場合に、正確なスタックトレースを生成できるように、リンクレジスタ(LR)の値をスタックに保存する変更が導入されています。これにより、パニック発生時のデバッグ情報がより信頼性の高いものになります。

コミット

commit 7de3d71797a0e6f25975fa5c3c69c7b0e27d23d2
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Wed Feb 6 01:18:37 2013 +0800

    runtime: save LR to stack when panicking to handle leaf function traceback
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/7289047

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

https://github.com/golang/go/commit/7de3d71797a0e6f25975fa5c3c69c7b0e27d23d2

元コミット内容

このコミットは、GoランタイムのARMアーキテクチャ向けコードにおいて、パニック発生時のスタックトレースの正確性を向上させることを目的としています。具体的には、リーフ関数(他の関数を呼び出さない関数)内でパニックが発生した場合に、呼び出し元の正確なアドレス(リンクレジスタ LR に格納されている値)が失われる問題を解決するため、LR の値をスタックに明示的に保存するように変更しています。これにより、sigpanic 関数への偽の呼び出しをシミュレートする際に、LR の値が正しくトレースバック処理に利用されるようになります。

変更の背景

Go言語のランタイムは、プログラムの異常終了(パニック)時に、どこで何が起こったかを開発者が把握できるように、スタックトレース(関数呼び出しの履歴)を生成します。しかし、ARMアーキテクチャのような特定のCPUアーキテクチャでは、関数呼び出しの規約や最適化によって、スタックトレースの生成が複雑になる場合があります。

特に、リーフ関数(leaf function)と呼ばれる、他の関数を一切呼び出さない関数では、コンパイラによる最適化の一環として、呼び出し元のリターンアドレスを保存するためのスタックフレームの構築が省略されることがあります。通常、関数が別の関数を呼び出す際には、呼び出し元のリターンアドレス(呼び出し元に戻るべき命令のアドレス)がリンクレジスタ(LR)に保存され、さらにスタックに退避されることで、関数が終了した後に正しい場所に戻れるようになります。しかし、リーフ関数では、LR をスタックに保存する必要がないため、このステップが省略されることがあります。

この最適化は通常は問題ありませんが、リーフ関数内でパニックが発生した場合に問題を引き起こす可能性がありました。パニック処理では、現在の実行コンテキスト(レジスタの値など)をキャプチャし、スタックを遡って呼び出し履歴を再構築します。リーフ関数で LR がスタックに保存されていない場合、パニック発生時に LR の値が上書きされたり、不正確な値になっていたりすると、正確な呼び出し元を特定できず、スタックトレースが途切れてしまう、あるいは誤った情報が表示される可能性がありました。

このコミットは、この問題を解決するために、パニックが発生する可能性がある場合には、リーフ関数であっても LR の値をスタックに明示的に保存するように変更することで、トレースバックの正確性を保証しようとしています。

前提知識の解説

1. ARMアーキテクチャのレジスタ

  • PC (Program Counter): 現在実行中の命令のアドレスを指すレジスタです。ARMでは、通常、実行中の命令の次の命令のアドレスを指します。
  • LR (Link Register): 関数呼び出し時に、呼び出し元に戻るべきアドレス(リターンアドレス)が保存されるレジスタです。関数が別の関数を呼び出す際、呼び出し元の PC の値が LR にコピーされます。
  • SP (Stack Pointer): スタックの現在のトップ(またはボトム)を指すレジスタです。スタックは、関数呼び出し時のローカル変数やレジスタの退避などに使われるメモリ領域です。

2. リーフ関数 (Leaf Function)

リーフ関数とは、関数本体内で他の関数を一切呼び出さない関数のことです。コンパイラは、リーフ関数に対して特別な最適化を適用することがあります。例えば、LR レジスタの値をスタックに保存する処理を省略することで、関数呼び出しのオーバーヘッドを削減し、実行速度を向上させることができます。これは、リーフ関数が他の関数を呼び出さないため、LR の値がその関数内で上書きされる心配がないためです。

3. スタックフレーム (Stack Frame)

関数が呼び出されるたびに、その関数の実行に必要な情報(ローカル変数、引数、レジスタの退避領域、リターンアドレスなど)を格納するために、スタック上に確保されるメモリ領域をスタックフレームと呼びます。関数が終了すると、そのスタックフレームは解放されます。

4. Goランタイムのパニックとトレースバック

Go言語では、リカバリーできないエラーが発生した場合に「パニック (panic)」が発生します。パニックが発生すると、通常のプログラムフローは中断され、Goランタイムは現在のゴルーチン(軽量スレッド)のスタックを遡り、どこでパニックが発生したか、どのような関数呼び出しの連鎖があったかを示す「スタックトレース (stack traceback)」を生成します。このスタックトレースは、デバッグにおいて非常に重要な情報となります。

5. シグナルハンドリング (Linux)

LinuxのようなUnix系OSでは、プログラムの異常な動作(例: 無効なメモリアクセス、ゼロ除算)や外部からのイベント(例: Ctrl+C)を「シグナル (signal)」として扱います。プログラムは、特定のシグナルを受信した際に実行される「シグナルハンドラ (signal handler)」を登録できます。Goランタイムは、セグメンテーション違反などの致命的なエラーをシグナルとして捕捉し、それをGoのパニック機構に変換して処理します。このコミットで変更されている signal_linux_arm.c は、Linux ARM環境でのシグナルハンドリングに関連するコードです。

技術的詳細

このコミットの核心は、ARMアーキテクチャにおけるGoランタイムのパニック処理とスタックトレース生成の連携を改善することにあります。

従来の課題

リーフ関数では、LR レジスタをスタックに保存しない最適化が行われることがあります。パニックが発生し、シグナルハンドラが起動されると、ランタイムは現在の実行コンテキスト(レジスタの状態)をキャプチャし、sigpanic という内部関数が呼び出されたかのようにスタックを「偽装」します。この偽装は、通常の関数呼び出しと同様に、PC(プログラムカウンタ)と LR(リンクレジスタ)の値を適切に設定することで行われます。

しかし、リーフ関数内でパニックが発生した場合、LR の値がスタックに保存されていないため、シグナルハンドラが LR の値を参照しようとしても、それが既に上書きされていたり、無効な値になっていたりする可能性がありました。これにより、sigpanic への偽の呼び出しが正しく設定されず、結果としてスタックトレースが途中で途切れたり、誤った情報が表示されたりする問題が発生していました。

解決策

このコミットでは、以下の2つのファイルにわたる変更によってこの問題を解決しています。

  1. src/pkg/runtime/signal_linux_arm.c の変更:

    • シグナルハンドラ内でパニック処理を行う際、LR の値をスタックに明示的に保存するように変更されました。
    • 具体的には、スタックポインタ r->arm_sp を4バイト(uint32 のサイズ)減らし、そのアドレスに現在の LR の値 r->arm_lr を書き込んでいます。
    • これにより、リーフ関数であっても、パニック発生時には LR の値が確実にスタックに退避されるようになります。
    • コメントも更新され、「リーフ関数でのパニックが正しく処理されるように、常にLRをスタックに保存する」という意図が明確にされています。
    • r->arm_pc がゼロでない場合(nil関数への呼び出しなどではない場合)にのみ r->arm_lr = r->arm_pc; を行うロジックは維持されています。これは、PC がゼロの場合に古い LR の方がスタックトレースに有用であるという考慮に基づいています。
  2. src/pkg/runtime/traceback_arm.c の変更:

    • スタックトレースを生成する runtime·gentraceback 関数において、パニックが発生した場合(waspanic フラグが真の場合)に、スタックから PC の値を取得するように変更されました。
    • 具体的には、pc = *(uintptr *)sp; でスタックトップから PC の値(これは signal_linux_arm.c で保存された LR の値に相当します)を読み出し、sp += 4; でスタックポインタを元に戻しています。
    • これにより、signal_linux_arm.c でスタックに保存された LR の値が、トレースバック処理中に正しく読み取られ、スタックトレースの連鎖が途切れることなく、正確な呼び出し元を特定できるようになります。

これらの変更により、リーフ関数内でパニックが発生した場合でも、LR の値がスタックに保存され、トレースバック処理がその保存された値を利用できるようになるため、より完全で正確なスタックトレースが生成されるようになります。

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

src/pkg/runtime/signal_linux_arm.c

--- a/src/pkg/runtime/signal_linux_arm.c
+++ b/src/pkg/runtime/signal_linux_arm.c
@@ -68,11 +68,17 @@ runtime·sighandler(int32 sig, Siginfo *info, void *context, G *gp)
 		gp->sigcode1 = r->fault_address;
 		gp->sigpc = r->arm_pc;
 
-		// If this is a leaf function, we do smash LR,
-		// but we're not going back there anyway.
-		// Don't bother smashing if r->arm_pc is 0,
-		// which is probably a call to a nil func: the
-		// old link register is more useful in the stack trace.
+		// We arrange lr, and pc to pretend the panicking
+		// function calls sigpanic directly.
+		// Always save LR to stack so that panics in leaf
+		// functions are correctly handled. This smashes
+		// the stack frame but we're not going back there
+		// anyway.
+		r->arm_sp -= 4;
+		*(uint32 *)r->arm_sp = r->arm_lr;
+		// Don't bother saving PC if it's zero, which is
+		// probably a call to a nil func: the old link register
+		// is more useful in the stack trace.
 		if(r->arm_pc != 0)
 			r->arm_lr = r->arm_pc;
 		// In case we are panicking from external C code

src/pkg/runtime/traceback_arm.c

--- a/src/pkg/runtime/traceback_arm.c
+++ b/src/pkg/runtime/traceback_arm.c
@@ -182,6 +182,12 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr
 		// If this was deferproc or newproc, the caller had an extra 12.
 		if(f->entry == (uintptr)runtime·deferproc || f->entry == (uintptr)runtime·newproc)
 			sp += 12;
+
+		// sighandler saves the lr on stack before fake a call to sigpanic
+		if(waspanic) {
+			pc = *(uintptr *)sp;
+			sp += 4;
+		}
 	}
 	
 	if(pcbuf == nil && (pc = gp->gopc) != 0 && (f = runtime·findfunc(pc)) != nil

コアとなるコードの解説

src/pkg/runtime/signal_linux_arm.c の変更点

このファイルは、Linux ARM環境におけるGoランタイムのシグナルハンドラの実装です。パニックが発生した際に、カーネルからシグナルを受け取り、Goのパニック処理に引き渡す役割を担います。

変更の主要部分は以下の通りです。

  1. スタックポインタの調整とLRの保存:

    r->arm_sp -= 4;
    *(uint32 *)r->arm_sp = r->arm_lr;
    
    • r->arm_sp -= 4;: スタックポインタ arm_sp を4バイト(uint32 のサイズ)減らしています。これは、スタックに新しいデータをプッシュするための領域を確保する操作です。スタックは通常、アドレスの大きい方から小さい方へ成長します。
    • *(uint32 *)r->arm_sp = r->arm_lr;: 減らした後の arm_sp が指すアドレスに、現在のリンクレジスタ arm_lr の値を uint32 型として書き込んでいます。これにより、LR の値がスタックに明示的に保存されます。
    • この操作は、リーフ関数であっても LR の値が確実にスタックに退避されるようにするためのものです。パニックが発生した場合、通常の関数呼び出しとは異なり、関数が正常にリターンすることはないため、スタックフレームが「破壊される」こと(smash the stack frame)は問題になりません。
  2. コメントの更新: 古いコメントは「リーフ関数であればLRを破壊するが、どうせ戻らないから問題ない」というニュアンスでしたが、新しいコメントでは「リーフ関数でのパニックが正しく処理されるように、常にLRをスタックに保存する」という、より積極的な保存の意図が明確にされています。

src/pkg/runtime/traceback_arm.c の変更点

このファイルは、ARMアーキテクチャにおけるGoランタイムのスタックトレース生成ロジックを実装しています。runtime·gentraceback 関数は、与えられたPC(プログラムカウンタ)とSP(スタックポインタ)から、関数呼び出しの履歴を遡ってスタックトレースを構築します。

追加された主要なコードブロックは以下の通りです。

		// sighandler saves the lr on stack before fake a call to sigpanic
		if(waspanic) {
			pc = *(uintptr *)sp;
			sp += 4;
		}
  • if(waspanic): この条件は、現在のトレースバックがパニック処理の一部として行われているかどうかを示します。waspanic は、runtime·gentraceback 関数に渡される引数で、パニックによって呼び出された場合に真となります。
  • pc = *(uintptr *)sp;: waspanic が真の場合、スタックポインタ sp が指すアドレスから uintptr 型の値を読み出し、それを現在の pc(プログラムカウンタ)として設定しています。この *(uintptr *)sp が読み出す値は、signal_linux_arm.c でスタックに保存された LR の値に他なりません。これにより、パニック発生時の正確なリターンアドレス(呼び出し元のアドレス)が pc に設定されます。
  • sp += 4;: pc の値を読み出した後、スタックポインタ sp を4バイト進めています。これは、先ほど読み出した値(LR)がスタックから「ポップ」されたことを意味し、スタックポインタを次のスタックフレームの開始位置に移動させるためのものです。

この変更により、signal_linux_arm.c でスタックに保存された LR の値が、traceback_arm.c のトレースバック処理によって正しく利用され、リーフ関数内でのパニックであっても、正確なスタックトレースが生成されるようになります。

関連リンク

  • Go言語の公式ドキュメント: https://golang.org/
  • Goのランタイムに関する情報: https://go.dev/doc/go1.2#runtime (Go 1.2のリリースノートですが、ランタイムの基本的な概念を理解するのに役立ちます)
  • ARMアーキテクチャのレジスタと関数呼び出し規約に関する一般的な情報 (例: ARM Architecture Reference Manual)

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている https://golang.org/cl/7289047 は、このGerritの変更リストへのリンクです)
  • ARMアーキテクチャに関する一般的な知識 (例: Wikipedia, ARM開発者向けドキュメント)
  • Goのパニックとリカバリーに関するドキュメントやブログ記事
  • Linuxシグナルハンドリングに関するシステムプログラミングの資料