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

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

このコミットは、Go言語のランタイムの一部である src/runtime/rt2_amd64.c ファイルに対する変更です。このファイルは、AMD64アーキテクチャにおけるGoランタイムの低レベルな処理、特にトレースバック(スタックトレース)の生成に関連するコードを含んでいます。

コミット

  • コミットハッシュ: 63f38d62ac7807f47d69610cf559393569e3622f
  • Author: Rob Pike r@golang.org
  • Date: Mon Nov 3 15:22:15 2008 -0800

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

https://github.com/golang/go/commit/63f38d62ac7807f47d69610cf559393569e3622f

元コミット内容

    in traceback, handle the case where we've called through a nil function pointer
    
    R=rsc
    DELTA=7  (7 added, 0 deleted, 0 changed)
    OCL=18372
    CL=18372

変更の背景

このコミットは、Goプログラムがパニック(panic)を起こし、その際にトレースバックを生成する過程で発生する可能性のある特定のエッジケースに対処するために導入されました。具体的には、nil(ヌル)関数ポインタを介して関数が呼び出された場合に、トレースバックが正しく処理されない問題がありました。

Go言語では、実行時エラーやプログラマが意図的に発生させる異常な状況(例: スライスへの範囲外アクセス、nilポインタのデリファレンス)は「パニック」として扱われます。パニックが発生すると、Goランタイムは現在のゴルーチン(goroutine)の実行を停止し、スタックを巻き戻しながら(unwind)、各関数の遅延関数(defer function)を実行し、最終的にトレースバック(スタックトレース)を出力してプログラムを終了させます(recoverによって捕捉されない限り)。

このトレースバック生成の過程で、プログラムカウンタ(PC)がnilになるという異常な状態が発生することがありました。これは通常、nil関数ポインタを呼び出そうとした結果として起こります。このような状況では、通常のスタック巻き戻しロジックでは適切な呼び出し元を特定できず、トレースバックが不正確になったり、ランタイム自体がクラッシュしたりする可能性がありました。このコミットは、この特定のnilPCのケースを検出し、トレースバック処理を適切に進めるための修正を加えています。

前提知識の解説

トレースバック (Traceback / Stack Trace)

トレースバック、またはスタックトレースとは、プログラムの実行中にエラーや例外が発生した際に、その時点での関数呼び出しの履歴を順に表示するものです。これにより、どの関数がどの関数を呼び出し、最終的にエラーが発生した場所に至ったのかを追跡できます。デバッグにおいて非常に重要な情報となります。Go言語では、パニックが発生した際に自動的にトレースバックが出力されます。

nil 関数ポインタ

Go言語において、関数は第一級オブジェクトであり、変数に関数ポインタとして代入することができます。nil関数ポインタとは、どの関数も指していない関数ポインタのことです。C言語やC++と同様に、nilポインタをデリファレンス(参照解除)しようとすると、通常はセグメンテーション違反などの実行時エラーを引き起こします。Goでは、nil関数ポインタを呼び出そうとするとパニックが発生します。

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション、スケジューリング(ゴルーチンの管理)、メモリ管理、そしてパニック処理やトレースバック生成といった低レベルな機能が含まれます。Goプログラムは、コンパイル時にランタイムとリンクされ、自己完結型のバイナリとして実行されます。

スタック巻き戻し (Stack Unwinding)

スタック巻き戻しとは、関数呼び出しスタックを逆順にたどっていくプロセスのことです。Goのパニック処理では、パニックが発生したゴルーチンのスタックフレームを一つずつ破棄しながら、defer文で登録された関数を実行していきます。この過程で、各スタックフレームのプログラムカウンタ(PC)とスタックポインタ(SP)を更新し、呼び出し元の情報を特定します。

PC (Program Counter) と SP (Stack Pointer)

  • PC (Program Counter): プログラムカウンタは、次に実行される命令のアドレスを保持するCPUレジスタです。関数呼び出し時には、呼び出し元の関数の次の命令のアドレス(リターンアドレス)がスタックに保存され、呼び出された関数から戻る際にそのアドレスがPCにロードされます。
  • SP (Stack Pointer): スタックポインタは、現在のスタックフレームの最上位(または最下位、アーキテクチャによる)のアドレスを指すCPUレジスタです。関数呼び出しごとにスタックフレームがプッシュされ、関数から戻る際にポップされます。

AMD64アーキテクチャ

AMD64(x86-64)は、64ビットの命令セットアーキテクチャです。Goランタイムは、このアーキテクチャに特化した低レベルなコードを含んでおり、スタックフレームの構造やレジスタの使用方法などが定義されています。src/runtime/rt2_amd64.cは、このアーキテクチャ固有のランタイム処理を実装しています。

技術的詳細

Goランタイムのトレースバック生成は、スタックを逆方向にたどり、各スタックフレームの情報を解析することで行われます。このプロセスでは、各フレームのプログラムカウンタ(PC)とスタックポインタ(SP)が重要な役割を果たします。PCは現在実行中の命令のアドレスを示し、SPはスタックの現在の位置を示します。

通常、関数が呼び出されると、呼び出し元のリターンアドレス(呼び出し元のPC)がスタックにプッシュされます。関数から戻る際には、このリターンアドレスがスタックからポップされ、PCにロードされます。

しかし、nil関数ポインタを呼び出そうとした場合、通常の関数呼び出しメカニズムが機能せず、PCがnil(または無効なアドレス)になることがあります。このような状況でトレースバック処理が開始されると、ランタイムはnilのPCを有効な命令アドレスとして解釈しようとし、結果として不正なメモリアクセスや、無限ループ、あるいはランタイム自体のクラッシュを引き起こす可能性がありました。

このコミットは、traceback関数が受け取るpcnilである場合に、それがnil関数ポインタの呼び出しによるものであると判断し、特別な処理を行うことでこの問題を解決します。具体的には、nilのPCを検出した場合、スタックポインタ(sp)が指す位置からリターンアドレスを読み取り、それを新しいPCとして設定します。そして、スタックポインタを8バイト(AMD64におけるポインタのサイズ)進めることで、現在の(失敗した)スタックフレームを「ポップ」し、トレースバック処理を次の(呼び出し元の)フレームから続行できるようにします。これにより、nil関数ポインタ呼び出しによるパニックでも、正確なトレースバックが生成されるようになります。

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

--- a/src/runtime/rt2_amd64.c
+++ b/src/runtime/rt2_amd64.c
@@ -25,6 +25,13 @@ traceback(uint8 *pc, uint8 *sp, void* r15)
 	// store local copy of per-process data block that we can write as we unwind
 	mcpy((byte*)&g, (byte*)r15, sizeof(G));
 
+	// if the PC is zero, it's probably due to a nil function pointer.
+	// pop the failed frame.
+	if(pc == nil) {
+		pc = ((uint8**)sp)[0];
+		sp += 8;
+	}
+
 	counter = 0;
 	name = "panic";
 	for(;;){

コアとなるコードの解説

追加されたコードは、traceback関数の冒頭に位置しています。

	// if the PC is zero, it's probably due to a nil function pointer.
	// pop the failed frame.
	if(pc == nil) {
		pc = ((uint8**)sp)[0];
		sp += 8;
	}
  1. if(pc == nil):

    • この条件文は、traceback関数に渡されたプログラムカウンタ(pc)がnil(ゼロ)であるかどうかをチェックします。
    • コメントにもあるように、pcnilである場合、それはnil関数ポインタを呼び出そうとした結果である可能性が高いと判断されます。
  2. pc = ((uint8**)sp)[0];:

    • pcnilであった場合、この行が実行されます。
    • spは現在のスタックポインタです。((uint8**)sp)は、spが指すメモリ位置をuint8型へのポインタのポインタとしてキャストしています。これは、スタック上に保存されているリターンアドレス(通常は呼び出し元のPC)を読み取るためです。
    • [0]は、そのポインタが指す最初の要素(つまり、スタックの最上位にある値)を取得します。
    • これにより、nil関数ポインタ呼び出しによって不正になったpcを、スタックに保存されている正しいリターンアドレス(呼び出し元のPC)で上書きします。
  3. sp += 8;:

    • pcを更新した後、スタックポインタspを8バイト(AMD64アーキテクチャにおけるポインタのサイズ)進めます。
    • これは、nil関数ポインタ呼び出しによって生成された「失敗したフレーム」をスタックから事実上「ポップ」する操作に相当します。このフレームは不正な状態にあるため、トレースバック処理から除外することで、次の(呼び出し元の)有効なスタックフレームから処理を続行できるようにします。

この修正により、nil関数ポインタの呼び出しによってパニックが発生した場合でも、Goランタイムは健全な状態を保ち、正確なトレースバックを生成してデバッグを支援できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントやソースコード(src/runtime/ディレクトリ)
  • Go言語のパニックとリカバリに関する一般的な情報源
  • スタックトレース、プログラムカウンタ、スタックポインタに関するコンピュータアーキテクチャの基本概念