[インデックス 14796] ファイルの概要
このコミットは、Goコンパイラ(cmd/5g
, cmd/6g
, cmd/8g
)において、panic
発生時に戻り値が正しくフラッシュされないというバグ(Issue 4066)を修正するものです。具体的には、defer
関数が存在する場合に、パニック発生時の戻り値のレジスタ割り当てが適切に行われるように、コンパイラのレジスタ割り当てロジックが変更されています。これにより、パニックが発生しても、defer
処理が実行された後に期待される戻り値が保証されるようになります。
コミット
commit f1e4ee3f49fd19d72fa3bbcbce4aab5c2fbef2ed
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date: Fri Jan 4 17:07:21 2013 +0100
cmd/5g, cmd/6g, cmd/8g: flush return parameters in case of panic.
Fixes #4066.
R=rsc, minux.ma
CC=golang-dev
https://golang.org/cl/7040044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f1e4ee3f49fd19d72fa3bbcbce4aab5c2fbef2ed
元コミット内容
cmd/5g, cmd/6g, cmd/8g: flush return parameters in case of panic.
(cmd/5g, cmd/6g, cmd/8g
: パニック発生時に戻り値をフラッシュする。)
Fixes #4066.
(Issue 4066を修正。)
変更の背景
このコミットは、Go言語のIssue 4066「return values not being spilled eagerly enough」を修正するために行われました。この問題は、関数内でpanic
が発生し、かつdefer
ステートメントが存在する場合に、関数の戻り値が正しく設定されない、または期待される値にならないというものでした。
Go言語では、panic
が発生すると通常の実行フローが中断され、スタックを巻き戻しながらdefer
関数が実行されます。defer
関数は、リソースの解放やエラーハンドリングなど、クリーンアップ処理を行うためによく使用されます。しかし、このIssue 4066では、panic
が発生した際に、コンパイラが戻り値の変数をレジスタからメモリに適切に「スピル」(退避)しないことが原因で、defer
関数が実行される時点での戻り値が古い値のままになってしまう可能性がありました。
特に、ループ内で戻り値が更新され、そのループ内でpanic
が発生するようなシナリオでこの問題が顕在化しました。defer
関数がrecover
を使ってパニックから回復し、関数の実行が継続される場合、最終的な戻り値が期待通りにならないというバグにつながっていました。
前提知識の解説
Go言語のpanic
とrecover
panic
: Go言語におけるランタイムエラーのメカニズムです。panic
が呼び出されると、現在の関数の実行が即座に停止し、その関数のdefer
関数が実行されます。その後、呼び出し元の関数へとスタックが巻き戻され(unwind)、各関数のdefer
関数が順に実行されます。このプロセスは、プログラムがクラッシュするか、recover
が呼び出されるまで続きます。defer
:defer
ステートメントは、それを囲む関数がリターンする直前(panic
によるスタックの巻き戻し中も含む)に実行される関数をスケジュールします。複数のdefer
ステートメントがある場合、LIFO(Last-In, First-Out)の順序で実行されます。recover
:recover
はdefer
関数内でのみ有効な組み込み関数です。panic
が発生している最中にrecover
が呼び出されると、panic
を停止させ、panic
に渡された値を返します。これにより、プログラムのクラッシュを防ぎ、エラーハンドリングを行うことができます。
Goコンパイラのレジスタ割り当てと「スピル」
Goコンパイラは、プログラムの実行速度を最適化するために、頻繁に使用される変数をCPUのレジスタに割り当てます。レジスタはメモリよりも高速にアクセスできるため、パフォーマンスが向上します。
しかし、レジスタの数は限られているため、コンパイラは必要に応じてレジスタに割り当てられた変数をメモリに退避させる必要があります。この操作を「スピル」(spill)と呼びます。例えば、関数呼び出しの前後や、レジスタが不足した場合などにスピルが行われます。
Issue 4066は、panic
が発生し、defer
関数が実行されるという特殊な状況下で、コンパイラが戻り値の変数をレジスタからメモリに適切にスピルしないことが原因で発生しました。これにより、defer
関数が戻り値にアクセスしようとした際に、レジスタ内の最新の値ではなく、メモリ内の古い値を見てしまう可能性がありました。
技術的詳細
このコミットは、Goコンパイラのバックエンド部分、特にレジスタ割り当てとコード生成を担当するreg.c
ファイル群(src/cmd/5g/reg.c
, src/cmd/6g/reg.c
, src/cmd/8g/reg.c
)に変更を加えています。これらのファイルは、それぞれ異なるアーキテクチャ(5g: ARM, 6g: AMD64, 8g: x86)向けのコンパイラです。
変更の核心は、prop
関数内のレジスタ割り当てロジックにあります。prop
関数は、レジスタ割り当てのプロパゲーション(伝播)を行う部分で、命令ごとにレジスタの状態を追跡し、変数がレジスタに割り当てられているか、メモリにスピルされているかなどを管理します。
既存のコードでは、Issue 1304のワークアラウンドとして、各命令の前に変更されたグローバル変数をフラッシュする処理がありました。これは、for(z=0; z<BITS; z++) { cal.b[z] |= externs.b[z]; }
の部分です。externs
はグローバル変数のビットマップを表します。
今回の修正では、このグローバル変数のフラッシュ処理に加えて、defer
関数が存在する場合(hasdefer
フラグが真の場合)に、戻り値の変数(ovar
)もフラッシュするロジックが追加されました。ovar
は戻り値の変数のビットマップを表します。
具体的には、cal.b[z] |= ovar.b[z];
という行が追加されています。ここでcal
は「call-used」レジスタのビットマップ、つまり関数呼び出しによって変更される可能性のあるレジスタの集合を表します。ovar
は「output variables」(戻り値の変数)のビットマップです。この操作により、defer
関数が存在する場合には、各命令の前に戻り値の変数がレジスタからメモリに強制的にスピルされるようになります。
これにより、panic
が発生してdefer
関数が実行される際、戻り値の変数は常に最新の値がメモリに反映されている状態となり、defer
関数がその値を正しく参照できるようになります。
コアとなるコードの変更箇所
以下の変更がsrc/cmd/5g/reg.c
, src/cmd/6g/reg.c
, src/cmd/8g/reg.c
の各ファイルに共通して適用されています。
--- a/src/cmd/5g/reg.c
+++ b/src/cmd/5g/reg.c
@@ -1075,8 +1075,12 @@ prop(Reg *r, Bits ref, Bits cal)
default:
// Work around for issue 1304:
// flush modified globals before each instruction.
- for(z=0; z<BITS; z++)
+ for(z=0; z<BITS; z++) {
cal.b[z] |= externs.b[z];
+ // issue 4066: flush modified return variables in case of panic
+ if(hasdefer)
+ cal.b[z] |= ovar.b[z];
+ }
break;
}
for(z=0; z<BITS; z++) {
また、この修正を検証するためのテストケースがtest/fixedbugs/issue4066.go
として追加されています。
// run
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// issue 4066: return values not being spilled eagerly enough
package main
func main() {
n := foo()
if n != 2 {
println(n)
panic("wrong return value")
}
}
type terr struct{}
func foo() (val int) {
val = 0
defer func() {
if x := recover(); x != nil {
_ = x.(terr)
}
}()
for {
val = 2
foo1()
}
panic("unreachable")
}
func foo1() {
panic(terr{})
}
コアとなるコードの解説
変更の核心は、prop
関数内のループにif(hasdefer) cal.b[z] |= ovar.b[z];
という行が追加されたことです。
prop
関数: この関数は、Goコンパイラのレジスタ割り当てフェーズの一部であり、プログラムの各命令におけるレジスタの使用状況を分析し、最適化を行います。for(z=0; z<BITS; z++)
: これはビットマップを操作するためのループです。BITS
はビットマップのサイズを示します。cal.b[z] |= externs.b[z];
: 既存のコードで、グローバル変数(externs
)が変更された場合に、その変更がcal
(call-usedレジスタ)に反映されるようにしています。これは、関数呼び出しによってグローバル変数が変更される可能性があるため、その変更を考慮に入れるためのものです。if(hasdefer)
: この条件は、現在の関数にdefer
ステートメントが存在するかどうかをチェックします。defer
が存在する場合にのみ、以下のロジックが適用されます。これは、defer
が存在しない場合は、panic
が発生しても戻り値の整合性が問題にならないためです。cal.b[z] |= ovar.b[z];
: この行が今回の修正の主要な部分です。ovar
は、関数の戻り値の変数を表すビットマップです。cal.b[z] |= ovar.b[z];
は、戻り値の変数がレジスタに割り当てられている場合、そのレジスタを「call-used」としてマークします。これにより、コンパイラは、defer
関数が実行される可能性がある場合に、戻り値の変数をレジスタからメモリに強制的にスピルするようになります。- この「強制スピル」は、
panic
が発生してdefer
関数が実行される際に、戻り値の変数が常に最新の値でメモリに存在することを保証します。これにより、defer
関数がrecover
を使ってパニックから回復し、関数の実行が継続された場合でも、最終的な戻り値が期待通りの値になることが保証されます。
追加されたテストケースtest/fixedbugs/issue4066.go
は、この修正が正しく機能することを確認します。foo
関数はval
を2
に設定し、その後foo1
を呼び出してpanic
を発生させます。foo
関数にはdefer
があり、recover
を使ってパニックから回復します。修正前は、main
関数で受け取るn
が2
にならないことがありましたが、修正後は常に2
になることが期待されます。
関連リンク
参考にした情報源リンク
- Go Issue 4066: https://github.com/golang/go/issues/4066
- Go言語の
panic
とrecover
に関する公式ドキュメントやチュートリアル (一般的な知識のため特定のURLは省略) - コンパイラのレジスタ割り当てに関する一般的な情報 (一般的な知識のため特定のURLは省略)
- Go言語のコンパイラソースコード (
src/cmd/5g/reg.c
,src/cmd/6g/reg.c
,src/cmd/8g/reg.c
)