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

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

このコミットは、Goランタイムにおいて、nil関数値を呼び出した際のパニックメッセージを、一般的な「無効なメモリアドレスまたはnilポインタの逆参照」というメッセージから、より具体的で分かりやすい「nil関数値の呼び出し」というメッセージに区別することを目的としています。これにより、開発者はパニックの原因をより迅速に特定できるようになります。

コミット

commit 5032a7dc0cb95eefe92714f572b58e5fa1569d6b
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jan 10 11:46:57 2012 -0800

    runtime: distinct panic message for call of nil func value
    
    R=golang-dev, gri
    CC=golang-dev
    https://golang.org/cl/5531062

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

https://github.com/golang/go/commit/5032a7dc0cb95eefe92714f572b58e5fa1569d6b

元コミット内容

runtime: distinct panic message for call of nil func value

変更の背景

Go言語では、nilポインタの逆参照や、nil関数値の呼び出しは、プログラムの実行時エラー(パニック)を引き起こします。以前のGoランタイムでは、これらのエラーはしばしば「invalid memory address or nil pointer dereference(無効なメモリアドレスまたはnilポインタの逆参照)」という汎用的なパニックメッセージとして報告されていました。

しかし、nil関数値の呼び出しは、特定のプログラミングミス(例えば、関数ポインタが初期化されていない、または意図せずnilになっている場合)を示唆しており、一般的なnilポインタの逆参照とは異なる文脈で発生します。この汎用的なメッセージでは、開発者がエラーの根本原因を特定するのに時間がかかる可能性がありました。

このコミットの目的は、nil関数値の呼び出しによって発生したパニックに対して、より具体的で診断に役立つメッセージを提供することです。これにより、開発者はエラーログを見ただけで、問題がnil関数値の呼び出しによるものであることを即座に理解し、デバッグプロセスを効率化できます。

前提知識の解説

  • Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステムです。ガベージコレクション、スケジューリング、パニック処理など、Go言語の多くの機能はランタイムによって提供されます。
  • パニック (Panic): Goにおける回復不可能なエラーメカニズムです。プログラムが予期せぬ状態に陥った際に発生し、通常はプログラムの実行を停止させます。パニックは、panic関数を明示的に呼び出すか、ランタイムエラー(例: nilポインタの逆参照、配列の範囲外アクセス)によって引き起こされます。
  • nil関数値 (nil func value): Goでは、関数も変数に代入できる「第一級オブジェクト」です。関数型の変数がどの関数も指していない状態をnil関数値と呼びます。このnil関数値を呼び出そうとすると、ランタイムエラーが発生します。
  • シグナル (Signals): オペレーティングシステムがプロセスに送信する非同期通知です。プログラムの異常終了(例: セグメンテーション違反、バスエラー)は、OSによって対応するシグナル(SIGSEGV, SIGBUSなど)としてプロセスに通知されます。
  • SIGSEGV (Segmentation Fault): プログラムがアクセス権のないメモリ領域にアクセスしようとしたり、存在しないメモリ領域にアクセスしようとしたりした場合に発生するシグナルです。
  • SIGBUS (Bus Error): プログラムが不正なメモリアドレスにアクセスしようとした場合に発生するシグナルです。これは通常、アラインメント違反や、物理的に存在しないアドレスへのアクセスなど、より低レベルのメモリ管理エラーに関連します。
  • 低メモリアドレス (Low Memory Addresses): 多くのオペレーティングシステムでは、アドレス空間の非常に低い部分(通常は0x0から始まる数百バイトまたは数キロバイト)は、意図的なnilポインタの逆参照を検出するために保護されています。この領域へのアクセスは、通常SIGSEGVSIGBUSを引き起こします。Goランタイムは、この特性を利用してnilポインタの逆参照を検出します。
  • g->sigpc: Goランタイムの内部構造体g(現在のgoroutineを表す)のメンバーで、シグナルが発生した時点のプログラムカウンタ(Program Counter)の値を保持します。プログラムカウンタは、次に実行される命令のアドレスを示すレジスタです。g->sigpc == 0は、シグナルがアドレス0(または非常に低いアドレス)での命令実行中に発生したことを示唆します。これは、nil関数値の呼び出しが、実質的にアドレス0にあるコードを実行しようとする試みとして現れるため、その検出に利用されます。

技術的詳細

この変更は、Goランタイムのシグナルハンドリング部分、特にruntime·sigpanic関数に焦点を当てています。この関数は、OSからSIGBUSSIGSEGVなどのシグナルを受け取った際に呼び出され、そのシグナルの種類と関連情報に基づいて適切なパニックメッセージを生成します。

変更の核心は、シグナルが発生したメモリアドレス(g->sigcode1)が非常に低い値(0x1000未満)である場合に、さらにプログラムカウンタ(g->sigpc)が0であるかどうかをチェックする点です。

  • g->sigcode1 < 0x1000: これは、アクセス違反が発生したメモリアドレスが、通常nilポインタの逆参照を検出するために保護されている低アドレス領域にあることを示します。
  • g->sigpc == 0: これは、シグナルが発生した命令のアドレスが0であることを意味します。nil関数値を呼び出すと、CPUは実質的にアドレス0にある命令を実行しようとします。これは不正な操作であり、通常はSIGSEGVSIGBUSを引き起こします。

この二つの条件が同時に満たされる場合、ランタイムは「call of nil func value」という、より具体的なパニックメッセージを出力します。それ以外の場合(例えば、g->sigpc0ではないが、g->sigcode1が低いアドレスである場合)は、引き続き「invalid memory address or nil pointer dereference」という汎用的なメッセージが出力されます。

このロジックは、Goが関数ポインタをnilに設定した場合、その関数ポインタを呼び出すと、実質的にアドレス0にあるコードを実行しようとするという、Goコンパイラとランタイムの内部的な挙動に基づいています。この挙動を利用することで、ランタイムはnil関数値の呼び出しと、他の種類のnilポインタ逆参照を区別できるようになります。

この変更は、Darwin (macOS), FreeBSD, Linux, NetBSD, OpenBSD, Windowsといった主要なオペレーティングシステム向けのランタイムコードに適用されています。これにより、Goがサポートする様々なプラットフォームで、より正確なパニック診断が可能になります。

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

変更は、src/pkg/runtime/thread_darwin.csrc/pkg/runtime/thread_freebsd.csrc/pkg/runtime/thread_linux.csrc/pkg/runtime/thread_netbsd.csrc/pkg/runtime/thread_openbsd.csrc/pkg/runtime/thread_windows.c の各ファイルにある runtime·sigpanic 関数に対して行われています。

以下に、src/pkg/runtime/thread_darwin.csrc/pkg/runtime/thread_windows.c の変更例を示します。他のファイルも同様の変更です。

src/pkg/runtime/thread_darwin.c の変更点:

--- a/src/pkg/runtime/thread_darwin.c
+++ b/src/pkg/runtime/thread_darwin.c
@@ -382,13 +382,19 @@ runtime·sigpanic(void)
 {
 	switch(g->sig) {
 	case SIGBUS:
-		if(g->sigcode0 == BUS_ADRERR && g->sigcode1 < 0x1000)
+		if(g->sigcode0 == BUS_ADRERR && g->sigcode1 < 0x1000) {
+			if(g->sigpc == 0)
+				runtime·panicstring("call of nil func value");
 			runtime·panicstring("invalid memory address or nil pointer dereference");
+		}
 		runtime·printf("unexpected fault address %p\\n", g->sigcode1);
 		runtime·throw("fault");
 	case SIGSEGV:
-		if((g->sigcode0 == 0 || g->sigcode0 == SEGV_MAPERR || g->sigcode0 == SEGV_ACCERR) && g->sigcode1 < 0x1000)
+		if((g->sigcode0 == 0 || g->sigcode0 == SEGV_MAPERR || g->sigcode0 == SEGV_ACCERR) && g->sigcode1 < 0x1000) {
+			if(g->sigpc == 0)
+				runtime·panicstring("call of nil func value");
 			runtime·panicstring("invalid memory address or nil pointer dereference");
+		}
 		runtime·printf("unexpected fault address %p\\n", g->sigcode1);
 		runtime·throw("fault");
 	case SIGFPE:

src/pkg/runtime/thread_windows.c の変更点:

--- a/src/pkg/runtime/thread_windows.c
+++ b/src/pkg/runtime/thread_windows.c
@@ -270,8 +270,11 @@ runtime·sigpanic(void)
 {
 	switch(g->sig) {
 	case EXCEPTION_ACCESS_VIOLATION:
-		if(g->sigcode1 < 0x1000)
+		if(g->sigcode1 < 0x1000) {
+			if(g->sigpc == 0)
+				runtime·panicstring("call of nil func value");
 			runtime·panicstring("invalid memory address or nil pointer dereference");
+		}
 		runtime·printf("unexpected fault address %p\\n\", g->sigcode1);
 		runtime·throw("fault");
 	case EXCEPTION_INT_DIVIDE_BY_ZERO:

コアとなるコードの解説

上記のコード変更は、runtime·sigpanic 関数内の SIGBUS および SIGSEGV (Windowsでは EXCEPTION_ACCESS_VIOLATION) のハンドリング部分に新しい条件分岐を追加しています。

変更前のコードでは、g->sigcode10x1000未満(つまり、低メモリアドレスへのアクセス違反)の場合、一律に「invalid memory address or nil pointer dereference」というパニックメッセージを出力していました。

変更後のコードでは、この条件が満たされた場合に、さらにネストされた if (g->sigpc == 0) という条件が追加されています。

  • if (g->sigpc == 0): この条件が真である場合、シグナルが発生した時点のプログラムカウンタが0であることを意味します。これは、nil関数値を呼び出そうとした結果、CPUがアドレス0にある命令を実行しようとしてアクセス違反が発生した可能性が非常に高いことを示唆します。この場合、runtime·panicstring("call of nil func value") が呼び出され、より具体的なパニックメッセージが出力されます。
  • elseg->sigpc != 0の場合): プログラムカウンタが0ではないが、低メモリアドレスへのアクセス違反が発生した場合は、引き続き「invalid memory address or nil pointer dereference」という汎用的なメッセージが出力されます。これは、一般的なnilポインタの逆参照(例えば、nil構造体のフィールドへのアクセスなど)に該当します。

この変更により、Goランタイムは、nil関数値の呼び出しによるパニックと、その他のnilポインタ逆参照によるパニックを、メッセージレベルで区別できるようになり、デバッグの精度が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Go Runtime, Panicに関する一般的な情報)
  • オペレーティングシステムのシグナルに関する一般的な知識 (SIGSEGV, SIGBUS)
  • Go言語のソースコード (runtimeパッケージの構造と動作に関する一般的な理解)