[インデックス 14346] ファイルの概要
このコミットは、Goコンパイラのcmd/6g
(AMD64アーキテクチャ向け) および cmd/8g
(x86アーキテクチャ向け) におけるレジスタ最適化に関するバグ修正を目的としています。具体的には、間接アドレッシング(ポインタを介したメモリ参照)において使用されるレジスタが、レジスタ割り当ての段階で正しくマークされていなかった問題を解決します。これにより、コンパイラが誤ったレジスタ最適化を行い、結果としてランタイムエラーや予期せぬ動作を引き起こす可能性がありました。
コミット
commit 7c0cbbfa186c10a6538e54f0cb6c5aba089fab8c
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Wed Nov 7 21:36:15 2012 +0100
cmd/6g, cmd/8g: mark used registers in indirect addressing.
Fixes #4094.
Fixes #4353.
R=golang-dev, dave, rsc
CC=golang-dev
https://golang.org/cl/6810090
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7c0cbbfa186c10a6538e54f0cb6c5aba089fab8c
元コミット内容
このコミットは、cmd/6g
(AMD64コンパイラ) と cmd/8g
(x86コンパイラ) において、間接アドレッシング(例: *ptr
や array[index]
のようなメモリ参照)で使用されるレジスタを正しくマークするように変更を加えるものです。これにより、レジスタ割り当ての最適化フェーズでこれらのレジスタが誤って再利用されることを防ぎ、バグを修正します。
変更の背景
この変更は、Goコンパイラのレジスタ割り当てにおける既知のバグ、特にIssue 4094とIssue 4353を修正するために行われました。
- Issue 4094: このIssueは、コンパイラが間接アドレッシングで使用されるレジスタを正しく認識せず、その結果、レジスタが誤って再利用され、不正なコードが生成される可能性を指摘していました。これは、特にポインタ演算や配列のインデックスアクセスにおいて顕在化し、プログラムのクラッシュや予期せぬ動作につながることがありました。
- Issue 4353: このIssueは、
8g
(x86コンパイラ) のオプティマイザにおけるバグが、配列の範囲外アクセス(out of bounds panic)ではなく、ランタイムフォルト(runtime fault)を引き起こすという具体的な問題を示していました。これは、最適化の過程でレジスタの使用状況が正しく追跡されていないことに起因し、結果としてメモリ破壊やセグメンテーションフォルトのような深刻な問題を引き起こす可能性がありました。
これらのバグは、コンパイラのレジスタ割り当てアルゴリズムが、間接アドレッシングの複雑なケースを適切に処理できていなかったことに根本的な原因がありました。コンパイラは、命令のオペランドがレジスタを間接的に参照している場合、そのレジスタが「使用中」であることを認識し、他の目的で再割り当てしないようにする必要があります。この認識の欠如が、最適化の誤りにつながっていました。
前提知識の解説
このコミットを理解するためには、以下の概念が重要です。
- Goコンパイラ (cmd/6g, cmd/8g): Go言語のソースコードを機械語に変換するプログラムです。
cmd/6g
はAMD64アーキテクチャ(64ビット)向け、cmd/8g
はx86アーキテクチャ(32ビット)向けのコンパイラです。これらはGoのツールチェインの一部であり、アセンブラ、リンカなどと連携して実行可能ファイルを生成します。 - レジスタ割り当て (Register Allocation): コンパイラの最適化フェーズの一つで、プログラムの変数や中間結果をCPUのレジスタに割り当てるプロセスです。レジスタはメモリよりも高速にアクセスできるため、レジスタを効率的に使用することはプログラムのパフォーマンスに大きく影響します。レジスタ割り当ては、どの変数をどのレジスタに割り当てるか、いつレジスタを解放するかなどを決定します。
- 間接アドレッシング (Indirect Addressing): メモリ上のデータにアクセスする際、そのデータのメモリアドレスがレジスタや別のメモリ位置に格納されている方式です。例えば、C言語のポインタ
*p
や、配列の要素a[i]
などがこれに該当します。CPUは、レジスタに格納されたアドレス値を使って実際のデータにアクセスします。 Prog
構造体: Goコンパイラの内部で、アセンブリ命令を表すために使用されるデータ構造です。各Prog
インスタンスは、命令の種類(as
)、ソースオペランド(from
)、デスティネーションオペランド(to
)などの情報を含みます。Addr
構造体:Prog
構造体内でオペランド(命令の対象となるデータ)を表すために使用されるデータ構造です。type
フィールドはオペランドの種類(レジスタ、メモリ、即値など)を示し、index
フィールドは間接アドレッシングにおけるインデックスレジスタを示します。D_INDIR
:Addr
構造体のtype
フィールドで使用される定数で、オペランドが間接アドレッシングであることを示します。例えば、D_INDIR + R_AX
は、AX
レジスタが指すメモリ位置を意味します。regopt
関数: コンパイラのレジスタ最適化パスの一部として、レジスタの使用状況を分析し、最適化を行う関数です。この関数は、各命令がどのレジスタを使用し、どのレジスタを定義するかを追跡します。r->use1.b[0]
とr->use2.b[0]
:regopt
関数内で使用されるビットマスクで、命令のソースオペランド(use1
)とデスティネーションオペランド(use2
)が使用するレジスタを追跡するために使われます。各ビットが特定のレジスタに対応しており、ビットを立てることでそのレジスタが使用中であることをマークします。RtoB(reg)
マクロ: レジスタ番号を対応するビットマスクに変換するマクロです。例えば、RtoB(R_AX)
はAX
レジスタに対応するビットを立てるための値になります。
技術的詳細
このコミットの核心は、regopt
関数におけるレジスタ使用状況の追跡ロジックの改善にあります。従来のregopt
関数は、直接的なレジスタの使用(例: MOV AX, BX
)は正しく追跡していましたが、間接アドレッシング(例: MOV AX, [BX+SI]
)において、ベースレジスタ(BX
)やインデックスレジスタ(SI
)が使用されていることを適切にマークしていませんでした。
修正前は、p->from
やp->to
が間接アドレッシングの場合、そのアドレス計算に使用されるレジスタ(ベースレジスタやインデックスレジスタ)が、レジスタ割り当ての「使用済み」リストに適切に追加されていませんでした。これにより、コンパイラはこれらのレジスタが空いていると誤解し、別の目的で再利用してしまう可能性がありました。結果として、命令が実行される際に、本来参照すべきメモリ位置が異なるレジスタ値によって上書きされ、不正なメモリアクセスや計算結果の誤りにつながっていました。
このコミットでは、以下のロジックがregopt
関数に追加されました。
- ソースオペランドの処理:
if(p->from.type >= D_INDIR)
: ソースオペランドp->from
が間接アドレッシングである場合、そのベースレジスタ(p->from.type - D_INDIR
でレジスタ番号を取得)をr->use1.b[0]
にビットOR演算で追加し、「使用済み」としてマークします。if(p->from.index != D_NONE)
: ソースオペランドにインデックスレジスタ(p->from.index
)が指定されている場合、そのインデックスレジスタもr->use1.b[0]
にビットOR演算で追加し、「使用済み」としてマークします。
- デスティネーションオペランドの処理:
if(p->to.type >= D_INDIR)
: デスティネーションオペランドp->to
が間接アドレッシングである場合、そのベースレジスタ(p->to.type - D_INDIR
でレジスタ番号を取得)をr->use2.b[0]
にビットOR演算で追加し、「使用済み」としてマークします。if(p->to.index != D_NONE)
: デスティネーションオペランドにインデックスレジスタ(p->to.index
)が指定されている場合、そのインデックスレジスタもr->use2.b[0]
にビットOR演算で追加し、「使用済み」としてマークします。
これらの変更により、間接アドレッシングに関わるすべてのレジスタが、命令の実行中に「使用中」として正しく認識されるようになります。これにより、レジスタ割り当てアルゴリズムは、これらのレジスタを他の目的で誤って再利用することを避け、正しい機械語コードを生成できるようになります。
test/fixedbugs/issue4353.go
は、この修正が解決する具体的な問題を示すテストケースです。このテストは、大きな配列の範囲外アクセスが、本来発生すべきパニックではなく、ランタイムフォルトを引き起こすというバグを再現します。修正後、このテストは正しくパニックを発生させるようになります。
コアとなるコードの変更箇所
変更は主にsrc/cmd/6g/reg.c
とsrc/cmd/8g/reg.c
のregopt
関数内で行われています。
src/cmd/6g/reg.c
(AMD64コンパイラ)
--- a/src/cmd/6g/reg.c
+++ b/src/cmd/6g/reg.c
@@ -247,6 +247,16 @@ regopt(Prog *firstp)
}
}
+ // Addressing makes some registers used.
+ if(p->from.type >= D_INDIR)
+ r->use1.b[0] |= RtoB(p->from.type-D_INDIR);
+ if(p->from.index != D_NONE)
+ r->use1.b[0] |= RtoB(p->from.index);
+ if(p->to.type >= D_INDIR)
+ r->use2.b[0] |= RtoB(p->to.type-D_INDIR);
+ if(p->to.index != D_NONE)
+ r->use2.b[0] |= RtoB(p->to.index);
+
bit = mkvar(r, &p->from);
if(bany(&bit))
switch(p->as) {
src/cmd/8g/reg.c
(x86コンパイラ)
--- a/src/cmd/8g/reg.c
+++ b/src/cmd/8g/reg.c
@@ -212,6 +212,16 @@ regopt(Prog *firstp)
}
}
+ // Addressing makes some registers used.
+ if(p->from.type >= D_INDIR)
+ r->use1.b[0] |= RtoB(p->from.type-D_INDIR);
+ if(p->from.index != D_NONE)
+ r->use1.b[0] |= RtoB(p->from.index);
+ if(p->to.type >= D_INDIR)
+ r->use2.b[0] |= RtoB(p->to.type-D_INDIR);
+ if(p->to.index != D_NONE)
+ r->use2.b[0] |= RtoB(p->to.index);
+
bit = mkvar(r, &p->from);
if(bany(&bit))
switch(p->as) {
test/fixedbugs/issue4353.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 4353. An optimizer bug in 8g triggers a runtime fault
// instead of an out of bounds panic.
package main
var aib [100000]int
var paib *[100000]int = &aib
var i64 int64 = 100023
func main() {
defer func() { recover() }()
_ = paib[i64]
}
コアとなるコードの解説
追加されたコードブロックは、regopt
関数内で各アセンブリ命令(Prog *p
)を処理するループの中に挿入されています。
// Addressing makes some registers used.
if(p->from.type >= D_INDIR)
r->use1.b[0] |= RtoB(p->from.type-D_INDIR);
if(p->from.index != D_NONE)
r->use1.b[0] |= RtoB(p->from.index);
if(p->to.type >= D_INDIR)
r->use2.b[0] |= RtoB(p->to.type-D_INDIR);
if(p->to.index != D_NONE)
r->use2.b[0] |= RtoB(p->to.index);
p->from.type >= D_INDIR
: これは、命令のソースオペランド(p->from
)が間接アドレッシングを使用しているかどうかをチェックします。D_INDIR
は、間接アドレッシングのタイプを示す定数であり、これ以上の値を持つタイプは間接アドレッシングを示します。r->use1.b[0] |= RtoB(p->from.type-D_INDIR);
: もしソースオペランドが間接アドレッシングであれば、そのベースレジスタ(p->from.type
からD_INDIR
を引くことでレジスタ番号が得られる)をr->use1.b[0]
ビットマスクにOR演算で追加します。RtoB
マクロはレジスタ番号をビットマスクに変換します。これにより、このベースレジスタが命令のソースとして「使用中」であることをマークします。p->from.index != D_NONE
: ソースオペランドにインデックスレジスタが使用されているかどうかをチェックします。D_NONE
はインデックスレジスタが指定されていないことを示します。r->use1.b[0] |= RtoB(p->from.index);
: もしインデックスレジスタが使用されていれば、そのレジスタもr->use1.b[0]
ビットマスクにOR演算で追加し、「使用中」としてマークします。p->to.type >= D_INDIR
とr->use2.b[0] |= RtoB(p->to.type-D_INDIR);
: 上記と同様に、デスティネーションオペランド(p->to
)が間接アドレッシングを使用している場合、そのベースレジスタをr->use2.b[0]
ビットマスクに「使用中」としてマークします。p->to.index != D_NONE
とr->use2.b[0] |= RtoB(p->to.index);
: デスティネーションオペランドにインデックスレジスタが使用されている場合、そのレジスタもr->use2.b[0]
ビットマスクに「使用中」としてマークします。
これらの変更により、regopt
関数は、間接アドレッシングに関わるすべてのレジスタ(ベースレジスタとインデックスレジスタ)を正確に追跡できるようになります。これにより、レジスタ割り当ての最適化フェーズでこれらのレジスタが誤って再利用されることがなくなり、コンパイラが生成するコードの正確性と安定性が向上します。
test/fixedbugs/issue4353.go
は、この修正の重要性を示す簡潔なテストケースです。paib[i64]
というアクセスは、paib
がポインタであり、i64
がインデックスとして使用されるため、間接アドレッシングの典型的な例です。i64
の値が配列の範囲外であるにもかかわらず、修正前のコンパイラは正しくパニックを発生させず、ランタイムフォルトを引き起こしていました。これは、コンパイラがi64
をインデックスレジスタとして使用する際に、そのレジスタの使用状況を正しく追跡できていなかったため、最適化の過程で問題が発生したことを示唆しています。このテストが追加され、修正後に正しくパニックを発生させることで、バグが解決されたことが確認されます。
関連リンク
- Go Issue 4094: https://github.com/golang/go/issues/4094
- Go Issue 4353: https://github.com/golang/go/issues/4353
- Go CL 6810090: https://golang.org/cl/6810090
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/6g/reg.c
,src/cmd/8g/reg.c
,test/fixedbugs/issue4353.go
) - Go Issue Tracker (Issue 4094, Issue 4353の議論内容)
- コンパイラ最適化に関する一般的な知識 (レジスタ割り当て、間接アドレッシング)
- x86およびAMD64アーキテクチャにおけるアドレッシングモードに関する知識