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

[インデックス 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レジスタが一時的に使用されることがあります。

問題は、clearfatAXをゼロで上書きする処理と、ポインタ計算が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レジスタを使用することがありました。

従来のコードでは、clearfatAXをゼロにする命令が、ポインタの宛先アドレスを計算する命令のに配置されていました。

// 修正前:
gconreg(AMOVL, 0, D_AX); // AXをゼロにする
nodreg(&n1, types[tptr], D_DI);
agen(nl, &n1);           // 宛先ポインタを計算し、DIに格納

この順序だと、agen(nl, &n1)が宛先ポインタを計算する際にAXレジスタを一時的に使用した場合、その計算の途中でAXgconreg(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で指定されたノード(通常はゼロ初期化されるべき構造体や配列)のメモリ領域をクリアするコードを生成します。

  1. c = w % 4;q = w / 4; は、クリアすべきバイト数wを、4バイト単位(quads)と残りのバイトに分割しています。これは、x86のブロック操作が通常4バイト単位で効率的に動作するためです。

  2. nodreg(&n1, types[tptr], D_DI); は、n1という一時的なノードを作成し、それがポインタ型であり、DIレジスタ(x86のRDI)を使用することを示します。DIレジスタは、STOS命令などのブロック操作で宛先アドレスを保持するために使用されます。

  3. agen(nl, &n1); は、nlで指定されたノードのアドレスを計算し、その結果をn1(つまりDIレジスタ)に格納するアセンブリコードを生成します。このステップで、ゼロ初期化されるべきメモリ領域の開始アドレスが決定されます。このアドレス計算の過程で、64ビット算術演算が必要な場合、AXレジスタが一時的に使用される可能性がありました。

  4. 修正された行: 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レジスタのゼロ値を利用します。

関連リンク

参考にした情報源リンク