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

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

コミット

commit fb7f217fe76f46aedb9cd017c79412600c11f959
Author: Ian Lance Taylor <iant@golang.org>
Date:   Fri Mar 22 17:32:04 2013 -0700

    runtime: correct misplaced right brace in Linux SIGBUS handling
    
    I'm not sure how to write a test for this.  The change in
    behaviour is that if you somehow get a SIGBUS signal for an
    address >= 0x1000, the program will now crash rather than
    calling panic.  As far as I know, on x86 GNU/Linux, the only
    way to get a SIGBUS (rather than a SIGSEGV) is to set the
    stack pointer to an invalid value.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/7906045

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

https://github.com/golang/go/commit/fb7f217fe76f46aedb9cd017c79412600c11f959

元コミット内容

このコミットは、GoランタイムのLinuxにおけるSIGBUSシグナルハンドリングに関するバグ修正です。具体的には、src/pkg/runtime/os_linux.cファイル内で、誤って配置されていた右中括弧(})を修正し、SIGBUSシグナルが発生した場合のプログラムの挙動を意図通りに改善します。

コミットメッセージによると、この変更によって、アドレス0x1000以上のメモリ領域でSIGBUSシグナルが発生した場合、プログラムはパニック(panic)を呼び出すのではなく、クラッシュするようになります。コミット作成者は、この変更に対するテストケースの作成が困難であると述べています。また、x86 GNU/Linux環境では、SIGBUSシグナルが(SIGSEGVではなく)発生する唯一の方法は、スタックポインタを不正な値に設定することであると推測しています。

変更の背景

この変更の背景には、GoランタイムがLinuxシステムからのシグナルをどのように処理するかという、低レベルな問題があります。SIGBUSは、不正なメモリアクセス、特にアラインメント違反や、物理的に存在しないアドレスへのアクセスなど、バスエラーに関連する問題が発生した際にOSからプロセスに送信されるシグナルです。

元のコードでは、SIGBUSシグナルが特定の条件(g->sigcode0 == BUS_ADRERR && g->sigcode1 < 0x1000)を満たした場合に、誤ったロジックパスに入り込む可能性がありました。具体的には、if文のスコープが意図せず早く閉じられてしまい、その後のruntime·panicstring("invalid memory address or nil pointer dereference");が、ifブロックの条件が真であるかどうかにかかわらず実行されてしまう状態でした。

この誤った中括弧の配置は、本来ifブロックの内部で処理されるべきロジックが、そのブロックの外で実行される原因となっていました。結果として、SIGBUSが発生した際に、本来クラッシュすべき状況で不適切なパニックメッセージが表示される、あるいは予期しない挙動を示す可能性がありました。この修正は、Goプログラムが低レベルのメモリアクセスエラーに遭遇した際の堅牢性と予測可能性を高めることを目的としています。

前提知識の解説

1. シグナル (Signals)

Unix系OSにおいて、シグナルはプロセスに対して非同期にイベントを通知するメカニズムです。プログラムの異常終了、ユーザーからの割り込み、タイマーの満了など、様々な状況でシグナルが生成され、プロセスに送信されます。プロセスはシグナルを受信すると、デフォルトの動作(例えば、プロセス終了)、シグナルハンドラによる処理、またはシグナルの無視を選択できます。

2. SIGBUS (Bus Error)

SIGBUSは「バスエラー」を示すシグナルです。これは通常、CPUがメモリにアクセスしようとした際に、ハードウェアレベルでの問題(例えば、存在しない物理アドレスへのアクセス、アラインメント違反、ページングシステムの問題)が発生した場合に生成されます。SIGSEGV(セグメンテーション違反)と混同されがちですが、SIGSEGVが主に不正な仮想メモリアドレスへのアクセス(例えば、読み取り専用メモリへの書き込み、存在しない仮想アドレスへのアクセス)を示すのに対し、SIGBUSはより低レベルのハードウェア的なメモリアクセスエラーに関連します。

Go言語のランタイムは、プログラムの実行中に発生するこれらのシグナルを捕捉し、Goのパニックメカニズムに変換したり、適切なエラー処理を行ったりします。

3. Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理する非常に重要なコンポーネントです。これには、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ割り当て、そしてOSとのインタラクション(システムコール、シグナルハンドリングなど)が含まれます。GoプログラムがOSからシグナルを受信すると、ランタイムがそれを捕捉し、Goのセマンティクス(例えば、パニックの発生)に変換して処理します。src/pkg/runtime/os_linux.cのようなファイルは、特定のOS(この場合はLinux)上でのランタイムの低レベルな動作を定義しています。

4. panic と クラッシュ (Crash)

Goにおけるpanicは、プログラムが回復不可能なエラー状態に陥ったことを示すメカニズムです。panicが発生すると、通常の実行フローは中断され、遅延関数(defer)が実行された後、スタックがアンワインドされます。recover関数を使ってpanicを捕捉し、回復することも可能ですが、捕捉されないpanicはプログラムを終了させます。

一方、「クラッシュ」は、プログラムが予期せず、通常はOSによって強制的に終了させられる状態を指します。これは、捕捉されないシグナル(例えば、SIGSEGVSIGBUSのデフォルト動作)によって引き起こされることが多いです。このコミットの文脈では、SIGBUSpanicではなく直接クラッシュを引き起こすように修正されたことで、より深刻な、回復不能なエラーとして扱われるようになったことを意味します。

5. g->sigcode0, g->sigcode1, g->sigpc

これらはGoランタイム内部のデータ構造(おそらくgは現在のゴルーチンを表す構造体)の一部であり、シグナルハンドラに渡されるシグナルに関する追加情報を含んでいます。

  • g->sigcode0: シグナルに関する追加コード(例: BUS_ADRERRはアドレスエラーを示す)。
  • g->sigcode1: シグナルに関連するアドレスや値。このコミットでは、0x1000との比較が行われています。
  • g->sigpc: シグナルが発生したプログラムカウンタ(Program Counter)の値。つまり、シグナルが発生した命令のアドレス。

技術的詳細

このコミットは、GoランタイムがLinux上でSIGBUSシグナルを処理する際のロジックフローを修正しています。問題の箇所はsrc/pkg/runtime/os_linux.c内のruntime·sigpanic関数です。この関数は、GoプログラムがOSからシグナルを受信した際に、そのシグナルをGoのパニックメカニズムに変換したり、適切なエラー処理を行ったりする役割を担っています。

修正前のコードは以下のようになっていました(関連部分のみ抜粋):

// 修正前
case SIGBUS:
    if(g->sigcode0 == BUS_ADRERR && g->sigcode1 < 0x1000) {
        if(g->sigpc == 0)
            runtime·panicstring("call of nil func value");
        } // ここでifブロックが誤って閉じられていた
        runtime·panicstring("invalid memory address or nil pointer dereference");
    runtime·printf("unexpected fault address %p\\n", g->sigcode1);
    runtime·throw("fault");
// ...

このコードでは、if(g->sigcode0 == BUS_ADRERR && g->sigcode1 < 0x1000)という条件付きブロックの直後に、誤って閉じ括弧}が配置されていました。これにより、runtime·panicstring("invalid memory address or nil pointer dereference");という行が、外側のif文の条件が真であるかどうかにかかわらず、常に実行される状態になっていました。

Goランタイムの意図としては、g->sigcode0 == BUS_ADRERR && g->sigcode1 < 0x1000という特定の条件(おそらく、Goのnilポインタデリファレンスに関連する低アドレスでのバスエラー)の場合にのみ、"call of nil func value"または"invalid memory address or nil pointer dereference"というパニックメッセージを生成し、それ以外のSIGBUS(特に0x1000以上のアドレスでのエラー)は、より深刻な、回復不能なエラーとして扱うべきでした。

修正によって、runtime·panicstring("invalid memory address or nil pointer dereference");の行が正しくifブロックの内部に配置され、その条件が満たされた場合にのみ実行されるようになりました。

// 修正後
case SIGBUS:
    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"); // 正しくifブロック内に移動
    } // ここでifブロックが閉じられる
    runtime·printf("unexpected fault address %p\\n", g->sigcode1);
    runtime·throw("fault");
// ...

この修正により、g->sigcode0 == BUS_ADRERR && g->sigcode1 < 0x1000の条件がの場合、つまり0x1000以上のアドレスでSIGBUSが発生した場合、runtime·panicstring("invalid memory address or nil pointer dereference");は実行されなくなります。代わりに、runtime·printf("unexpected fault address %p\\n", g->sigcode1);runtime·throw("fault");が実行されます。runtime·throw("fault");は、Goランタイムが回復不能なエラーに遭遇した際に呼び出される関数で、通常はプログラムを即座に終了(クラッシュ)させます。

したがって、この修正は、特定の低アドレスでのSIGBUSはGoのパニックとして処理し、それ以外のSIGBUSはより深刻なクラッシュとして扱うという、Goランタイムの意図された挙動を回復させるものです。

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

--- a/src/pkg/runtime/os_linux.c
+++ b/src/pkg/runtime/os_linux.c
@@ -225,8 +225,8 @@ runtime·sigpanic(void)
 		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:

変更はsrc/pkg/runtime/os_linux.cファイルの225行目付近にあります。具体的には、runtime·sigpanic関数内のSIGBUSシグナルを処理するcaseブロックです。

  • 削除された行: }
  • 追加された行: }

この変更は、単に閉じ括弧}の位置を1行下に移動させただけですが、これによりif文のスコープが正しく修正されました。

コアとなるコードの解説

この修正は、Goランタイムのruntime·sigpanic関数内のSIGBUSシグナルハンドリングロジックの論理的な誤りを修正するものです。

runtime·sigpanic関数は、GoプログラムがOSからシグナル(SIGBUS, SIGSEGVなど)を受信した際に呼び出されます。この関数は、受信したシグナルの種類と、シグナルに関連する追加情報(g->sigcode0, g->sigcode1, g->sigpcなど)に基づいて、適切なGoのパニックを発生させるか、あるいはプログラムをクラッシュさせるかを決定します。

修正前のコードでは、SIGBUScaseブロック内で、以下のような構造になっていました。

    if(g->sigcode0 == BUS_ADRERR && g->sigcode1 < 0x1000) {
        if(g->sigpc == 0)
            runtime·panicstring("call of nil func value");
        } // <-- ここで意図せずifブロックが閉じられていた
        runtime·panicstring("invalid memory address or nil pointer dereference"); // <-- この行は常に実行されていた
    // ...

この誤った閉じ括弧のために、runtime·panicstring("invalid memory address or nil pointer dereference");というパニック呼び出しが、外側の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"); // <-- ifブロックの内部に正しく配置
    } // <-- ここでifブロックが正しく閉じられる
    // ...

この修正により、runtime·panicstring("invalid memory address or nil pointer dereference");は、g->sigcode0 == BUS_ADRERR && g->sigcode1 < 0x1000という条件が真である場合にのみ実行されるようになりました。

この条件が偽の場合(つまり、0x1000以上のアドレスでSIGBUSが発生した場合など)、プログラムはifブロックをスキップし、その後のruntime·printf("unexpected fault address %p\\n", g->sigcode1);runtime·throw("fault");が実行されます。runtime·throw("fault");は、Goランタイムが回復不能なエラーに遭遇した際に呼び出され、プログラムをクラッシュさせます。

したがって、この修正は、SIGBUSシグナルが発生した際のエラー処理の粒度を正確にし、特定の条件下のSIGBUSはGoのパニックとして、それ以外のより深刻なSIGBUSはプログラムのクラッシュとして適切に扱うように、ランタイムの挙動を調整しています。

関連リンク

  • Gerrit Change-ID: https://golang.org/cl/7906045
    • このコミットの元となったGoのコードレビューシステム(Gerrit)上の変更セットです。通常、ここにはコミットに関する議論や追加情報が含まれています。

参考にした情報源リンク

  • 上記のGerrit Change-IDのページ
  • Go言語のソースコード(特にsrc/pkg/runtime/os_linux.c
  • Unix/Linuxシグナルに関する一般的なドキュメント(man 7 signalなど)
  • Goランタイムの内部動作に関するドキュメントや記事(Goのパニックとリカバリ、ガベージコレクション、スケジューラなど)
  • SIGBUSSIGSEGVの違いに関する技術記事