[インデックス 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によって強制的に終了させられる状態を指します。これは、捕捉されないシグナル(例えば、SIGSEGV
やSIGBUS
のデフォルト動作)によって引き起こされることが多いです。このコミットの文脈では、SIGBUS
がpanic
ではなく直接クラッシュを引き起こすように修正されたことで、より深刻な、回復不能なエラーとして扱われるようになったことを意味します。
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のパニックを発生させるか、あるいはプログラムをクラッシュさせるかを決定します。
修正前のコードでは、SIGBUS
のcase
ブロック内で、以下のような構造になっていました。
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のパニックとリカバリ、ガベージコレクション、スケジューラなど)
SIGBUS
とSIGSEGV
の違いに関する技術記事