[インデックス 16793] ファイルの概要
このコミットは、Goコンパイラのcmd/8g
(x86アーキテクチャ向けコンパイラ)におけるバグ修正です。具体的には、構造体のゼロ初期化を行うclearfat
関数と、ポインタ計算における64ビット算術演算がインターリーブ(混在)することで発生するレジスタ破壊(clobbering)の問題を解決します。
コミット
commit 85a7c090c4f831b6d29556c36bbe0a6cd8e8da6d
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date: Wed Jul 17 11:04:34 2013 +0200
cmd/8g: Make clearfat non-interleaved with pointer calculations.
clearfat (used to zero initialize structures) will use AX for x86 block ops. If we write to AX while calculating the dest pointer, we will fill the structure with incorrect values.
Since 64-bit arithmetic uses AX to synthesize a 64-bit register, getting an adress by indexing with 64-bit ops can clobber the register.
Fixes #5820.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/11383043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/85a7c090c4f831b6d29556c36bbe0a6cd8e8da6d
元コミット内容
cmd/8g: Make clearfat non-interleaved with pointer calculations.
clearfat (used to zero initialize structures) will use AX for x86 block ops. If we write to AX while calculating the dest pointer, we will fill the structure with incorrect values.
Since 64-bit arithmetic uses AX to synthesize a 64-bit register, getting an adress by indexing with 64-bit ops can clobber the register.
Fixes #5820.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/11383043
変更の背景
このコミットは、Goコンパイラが生成するx86アセンブリコードにおける特定のバグ、具体的にはGo issue 5820を修正するために行われました。この問題は、構造体をゼロ初期化する際に使用されるclearfat
ルーチンと、64ビットのポインタ計算が同時に行われる場合に発生しました。
clearfat
は、x86のブロック操作(例えばrep stosd
のような命令)を利用してメモリ領域を効率的にゼロクリアします。これらの操作は、通常AX
レジスタ(またはEAX
/RAX
)をゼロ値のソースとして使用します。一方、64ビットアーキテクチャにおけるポインタ計算、特に配列のインデックス計算などでは、64ビットの値を扱うために複数のレジスタを組み合わせて使用することがあり、その過程でAX
レジスタが一時的に使用されることがあります。
問題は、clearfat
がAX
をゼロで上書きする処理と、ポインタ計算がAX
を使用する処理がインターリーブ(混在)して実行された場合に発生しました。ポインタ計算の途中でAX
がゼロに上書きされてしまうと、計算中のポインタアドレスが不正な値になり、結果として構造体が誤った値で初期化されたり、メモリ破壊が発生したりする可能性がありました。
このバグは、特に64ビット環境で大きな構造体や配列を扱う際に顕在化し、プログラムのクラッシュや予期せぬ動作を引き起こす原因となっていました。
前提知識の解説
clearfat
とは
clearfat
はGoコンパイラ内部で使用されるルーチンで、主に構造体や配列などの複合データ型をゼロ値で初期化するために利用されます。Go言語では、変数が宣言されると自動的にその型のゼロ値で初期化されるという保証があります。この保証を実現するために、コンパイラは必要に応じてclearfat
のようなルーチンを呼び出し、メモリ領域を効率的にゼロクリアします。x86アーキテクチャでは、rep stos
命令(繰り返しストア命令)のようなブロック操作命令を利用して、高速なメモリクリアを実現します。これらの命令は、通常、AX
(またはEAX
/RAX
)レジスタにストアする値を保持します。
x86ブロック操作とAX
レジスタ
x86アーキテクチャには、メモリブロックを操作するための特別な命令群があります。例えば、STOS
(Store String)命令は、AL
/AX
/EAX
/RAX
レジスタの内容をES:DI
(またはRDI
)が指すメモリ位置にストアし、DI
/RDI
をインクリメント/デクリメントします。これにREP
プレフィックスを付けると、CX
(またはECX
/RCX
)レジスタで指定された回数だけ操作を繰り返します。clearfat
のようなゼロクリア操作では、AX
レジスタに0をセットし、rep stos
命令を使って指定されたメモリ領域をゼロで埋めます。
64ビット算術演算とレジスタの利用
x86-64(AMD64)アーキテクチャでは、64ビットのレジスタ(RAX
, RBX
, RCX
, RDX
など)が導入されています。しかし、一部の操作、特に古い命令セットや特定のコンパイラの最適化戦略では、64ビットの値を直接扱うのではなく、32ビットレジスタ(EAX
など)や16ビットレジスタ(AX
など)を組み合わせて64ビットの演算を合成することがあります。例えば、64ビットのアドレス計算を行う際に、一時的にAX
レジスタが中間結果の格納や、より大きなレジスタの一部として使用されることがあります。
レジスタ破壊(Register Clobbering)
レジスタ破壊とは、ある処理が特定のレジスタを使用している最中に、別の処理がそのレジスタの内容を予期せず上書きしてしまう現象を指します。これは、コンパイラがレジスタ割り当てを最適化する際に、異なる目的で同じレジスタを再利用しようとしたり、アセンブリコードレベルでの命令の順序が不適切であったりする場合に発生します。レジスタ破壊が発生すると、プログラムは不正なデータを使用したり、誤ったメモリ位置にアクセスしたりする可能性があり、結果としてクラッシュやデータ破損につながります。
技術的詳細
この問題の核心は、Goコンパイラ(cmd/8g
)が生成するアセンブリコードの命令順序にありました。clearfat
ルーチンは、構造体をゼロ初期化するためにAX
レジスタにゼロをロードし、その後rep stos
のようなブロック操作でメモリをクリアします。
一方で、ポインタ計算、特に64ビット環境での複雑なアドレス計算(例: array[index]
のようなインデックスアクセス)は、その計算過程で一時的にAX
レジスタを使用することがありました。
従来のコードでは、clearfat
がAX
をゼロにする命令が、ポインタの宛先アドレスを計算する命令の前に配置されていました。
// 修正前:
gconreg(AMOVL, 0, D_AX); // AXをゼロにする
nodreg(&n1, types[tptr], D_DI);
agen(nl, &n1); // 宛先ポインタを計算し、DIに格納
この順序だと、agen(nl, &n1)
が宛先ポインタを計算する際にAX
レジスタを一時的に使用した場合、その計算の途中でAX
がgconreg(AMOVL, 0, D_AX)
によってゼロに上書きされてしまう可能性がありました。これにより、DI
レジスタに格納されるべき最終的な宛先ポインタが不正な値となり、結果としてclearfat
が誤ったメモリ領域をゼロクリアしてしまう、あるいはプログラムがクラッシュするという問題が発生していました。
特に、64ビットのインデックス(uint64
など)を使用して配列にアクセスする場合、そのインデックス値をアドレスに変換する計算が複雑になり、AX
レジスタがその計算の一部として利用される可能性が高まりました。
このコミットは、AX
レジスタをゼロにする操作を、宛先ポインタの計算が完了した後に移動することで、このレジスタ破壊の問題を解決しました。これにより、ポインタ計算がAX
を安全に使用できるようになり、計算結果が破壊されることなく、clearfat
が正しいメモリ領域をゼロ初期化できるようになりました。
コアとなるコードの変更箇所
変更はsrc/cmd/8g/ggen.c
ファイル内のclearfat
関数にあります。
--- a/src/cmd/8g/ggen.c
+++ b/src/cmd/8g/ggen.c
@@ -78,9 +78,9 @@ clearfat(Node *nl)
c = w % 4; // bytes
q = w / 4; // quads
- gconreg(AMOVL, 0, D_AX);
nodreg(&n1, types[tptr], D_DI);
agen(nl, &n1);
+ gconreg(AMOVL, 0, D_AX);
if(q >= 4) {
gconreg(AMOVL, q, D_CX);
この差分は、gconreg(AMOVL, 0, D_AX);
という行が、agen(nl, &n1);
の呼び出しの前から後に移動したことを示しています。
また、この修正を検証するためのテストケースがtest/fixedbugs/issue5820.go
として追加されています。
// run
// Copyright 2013 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 5820: register clobber when clearfat and 64 bit arithmetic is interleaved.
package main
func main() {
array := make([][]int, 2)
index := uint64(1)
array[index] = nil
if array[1] != nil {
panic("array[1] != nil")
}
}
このテストコードは、[][]int
型のスライスを作成し、uint64
型のインデックスを使用して要素にnil
を代入しています。その後、その要素がnil
であることを確認しています。このシナリオは、clearfat
が関与する可能性のある構造体のゼロ初期化と、64ビットのインデックス計算が同時に発生する状況を再現し、レジスタ破壊が発生しないことを検証します。
コアとなるコードの解説
clearfat
関数は、引数nl
で指定されたノード(通常はゼロ初期化されるべき構造体や配列)のメモリ領域をクリアするコードを生成します。
-
c = w % 4;
とq = w / 4;
は、クリアすべきバイト数w
を、4バイト単位(quads)と残りのバイトに分割しています。これは、x86のブロック操作が通常4バイト単位で効率的に動作するためです。 -
nodreg(&n1, types[tptr], D_DI);
は、n1
という一時的なノードを作成し、それがポインタ型であり、DI
レジスタ(x86のRDI
)を使用することを示します。DI
レジスタは、STOS
命令などのブロック操作で宛先アドレスを保持するために使用されます。 -
agen(nl, &n1);
は、nl
で指定されたノードのアドレスを計算し、その結果をn1
(つまりDI
レジスタ)に格納するアセンブリコードを生成します。このステップで、ゼロ初期化されるべきメモリ領域の開始アドレスが決定されます。このアドレス計算の過程で、64ビット算術演算が必要な場合、AX
レジスタが一時的に使用される可能性がありました。 -
修正された行:
gconreg(AMOVL, 0, D_AX);
gconreg
は、定数をレジスタにロードするアセンブリ命令を生成する関数です。AMOVL
は、32ビットの移動命令(MOV
)を意味します。0
は、ロードする定数値(ゼロ)です。D_AX
は、ターゲットレジスタがAX
(またはEAX
)であることを示します。
この行は、
AX
レジスタにゼロをロードするアセンブリ命令を生成します。 修正前は、この命令がagen(nl, &n1);
の前にありました。 修正後は、この命令がagen(nl, &n1);
の後に移動しました。
この変更により、宛先ポインタの計算(agen(nl, &n1);
)が完全に終了し、DI
レジスタに正しいアドレスが格納された後に、AX
レジスタがゼロで上書きされるようになりました。これにより、ポインタ計算中にAX
が破壊されることがなくなり、clearfat
が意図した通りに動作するようになりました。
その後のif(q >= 4)
ブロックでは、CX
レジスタにクリアすべき4バイト単位の数(quads)をロードし、rep stosd
のような命令を生成して実際のゼロクリアを実行します。この操作はAX
レジスタのゼロ値を利用します。
関連リンク
- Go issue 5820: https://go.dev/issue/5820
- Go CL 11383043: https://go.googlesource.com/go/+/11383043