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

[インデックス 12100] ファイルの概要

このコミットは、Goコンパイラ(cmd/gc)におけるバグ修正に関するものです。具体的には、変数が256回参照されると、コンパイラがその変数を誤って未使用と判断し、スタック上の不正な位置(0(SP))に配置してしまう問題に対処しています。これにより、ゼロ初期化時に深刻なメモリ破損が発生する可能性がありました。このコミットは、変数の使用回数をカウントする代わりに、一度でも使用されたら「使用済み」とマークするように変更することで、このオーバーフローとそれに伴う問題を解決しています。また、このバグを再現し、修正を検証するための新しいテストケースが追加されています。

コミット

  • コミットハッシュ: f2ad374ae6663bb5cb7473bc868979e20cad70ad
  • 作者: Rémy Oudompheng oudomphe@phare.normalesup.org
  • 日付: Tue Feb 21 16:38:01 2012 +1100

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/f2ad374ae6663bb5cb7473bc868979e20cad70ad

元コミット内容

cmd/gc: don't believe that variables mentioned 256 times are unused.

Such variables would be put at 0(SP), leading to serious
corruptions at zero initialization.
Fixes #3084.

R=golang-dev, r
CC=golang-dev, remy
https://golang.org/cl/5683052

変更の背景

Goコンパイラ(gc)の内部では、関数の自動変数(スタック上に割り当てられるローカル変数)の使用状況を追跡するメカニズムが存在しました。このメカニズムは、変数がコード内で参照されるたびに、その変数の「使用回数」をインクリメントするカウンターを使用していました。

しかし、このカウンターが8ビットの符号なし整数(uint8)として実装されていたため、変数の使用回数が255回を超えて256回に達すると、カウンターがオーバーフローして0に戻ってしまうというバグがありました。コンパイラは、この「使用回数」が0であることを見て、その変数が未使用であると誤って判断していました。

未使用と判断された変数は、最適化の一環としてスタック上のオフセット0(SP)(スタックポインタからのオフセット0)に配置されることがありました。これは通常、関数の引数や戻り値、あるいは特定のレジスタに割り当てられるべき変数に対して行われる処理です。しかし、ローカル変数がこの位置に誤って配置されると、他の重要なデータ(例えば、関数の引数や戻り値、あるいはスタックフレームのメタデータ)とメモリ領域が衝突し、ゼロ初期化の際にそれらのデータが上書きされてしまうという深刻なメモリ破損を引き起こしました。

この問題は、GoのIssue #3084として報告され、このコミットによって修正されました。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識があると役立ちます。

  • Goコンパイラ (gc): Go言語の公式コンパイラです。ソースコードを機械語に変換する役割を担います。cmd/gcは、Goコンパイラのフロントエンドとバックエンドの一部を含むディレクトリです。
  • 自動変数 (Automatic Variables): 関数内で宣言されるローカル変数のことです。これらは通常、関数の呼び出し時にスタック上にメモリが割り当てられ、関数が終了すると解放されます。
  • スタック (Stack): プログラム実行時に一時的なデータを格納するために使用されるメモリ領域です。関数呼び出し、ローカル変数の割り当て、関数の戻りアドレスの保存などに利用されます。スタックはLIFO(Last-In, First-Out)の原則で動作します。
  • スタックポインタ (SP): 現在のスタックの最上位(または最下位、アーキテクチャによる)を指すレジスタです。変数が0(SP)に配置されるというのは、スタックポインタが指すアドレスからオフセット0の位置、つまりスタックポインタが指すまさにその位置にデータが置かれることを意味します。
  • ゼロ初期化 (Zero Initialization): Go言語では、変数が宣言されると、その型に応じたゼロ値で自動的に初期化されます(例: int0string""、ポインタはnil)。この処理はコンパイラによって生成されるコードによって行われます。
  • D_AUTO: Goコンパイラの内部表現におけるノードの種類の一つで、自動変数(ローカル変数)を表します。
  • ggen.c: Goコンパイラのバックエンドの一部で、アセンブリコード生成(ggenerategengeneratorの略)に関連する処理を行うC言語のソースファイルです。5g, 6g, 8gはそれぞれ、ARM (5), x86-64 (6), ARM64 (8) などの異なるアーキテクチャ向けのコンパイラバックエンドを指します。
  • カウンターオーバーフロー: 固定ビット幅の数値型(例: 8ビット整数)が表現できる最大値を超えた場合に、その値が最小値に戻ってしまう現象です。8ビット符号なし整数では、255の次に1を足すと0になります。

技術的詳細

このバグは、Goコンパイラのコード生成フェーズにおいて、自動変数の使用回数を追跡するusedフィールドがuint8型であったことに起因します。markautoused関数は、プログラムの命令(Prog)を走査し、D_AUTO型のノード(自動変数)が参照されるたびに、そのノードのusedフィールドをインクリメントしていました。

// 変更前 (概念的な表現)
p->from.node->used++;
p->to.node->used++;

usedフィールドがuint8であるため、変数が255回使用された後、256回目の使用でusedの値は255 + 1 = 256となりますが、uint8の最大値は255なので、オーバーフローして0になります。

コンパイラの他の部分では、このusedフィールドの値が0である場合、その変数は未使用であると判断し、最適化の対象としていました。未使用と判断された変数は、スタック上のオフセット0(SP)に配置されることがありました。これは、通常、関数の引数や戻り値が配置される領域と重なる可能性があり、特にゼロ初期化の際に、その領域のデータが意図せず上書きされてしまうという問題を引き起こしました。

このコミットの修正は、usedフィールドをインクリメントする代わりに、単に1を代入するように変更することで、このオーバーフローの問題を根本的に解決しています。

// 変更後 (概念的な表現)
p->from.node->used = 1;
p->to.node->used = 1;

これにより、変数が一度でも使用されればusedフィールドは1となり、それ以降何度参照されても1のままなので、オーバーフローすることなく、常に「使用済み」として正しく認識されるようになります。この変更は、変数の正確な使用回数を追跡する必要がない場合に有効なアプローチです。

コアとなるコードの変更箇所

このコミットでは、以下の3つのファイルが変更されています。これらはGoコンパイラの異なるアーキテクチャ(5g: ARM, 6g: x86-64, 8g: ARM64)向けのコード生成部分です。

  • src/cmd/5g/ggen.c
  • src/cmd/6g/ggen.c
  • src/cmd/8g/ggen.c

それぞれのファイルで、markautoused関数内のp->from.node->used++p->to.node->used++という行が、p->from.node->used = 1;p->to.node->used = 1;に変更されています。

また、このバグを再現し、修正が正しく機能することを確認するための新しいテストファイルtest/fixedbugs/bug423.goが追加されています。このテストファイルでは、Xという変数を意図的に256回以上参照することで、修正前のコンパイラであればバグが露呈するような状況を作り出しています。

src/cmd/5g/ggen.c の変更点

--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -29,10 +29,10 @@ markautoused(Prog* p)
 {
 	for (; p; p = p->link) {
 		if (p->from.name == D_AUTO && p->from.node)
-			p->from.node->used++;
+			p->from.node->used = 1;
 
 		if (p->to.name == D_AUTO && p->to.node)
-			p->to.node->used++;
+			p->to.node->used = 1;
 	}
 }

src/cmd/6g/ggen.c の変更点

--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -26,10 +26,10 @@ markautoused(Prog* p)
 {
 	for (; p; p = p->link) {
 		if (p->from.type == D_AUTO && p->from.node)
-			p->from.node->used++;
+			p->from.node->used = 1;
 
 		if (p->to.type == D_AUTO && p->to.node)
-			p->to.node->used++;
+			p->to.node->used = 1;
 	}
 }

src/cmd/8g/ggen.c の変更点

--- a/src/cmd/8g/ggen.c
+++ b/src/cmd/8g/ggen.c
@@ -28,10 +28,10 @@ markautoused(Prog* p)
 {
 	for (; p; p = p->link) {
 		if (p->from.type == D_AUTO && p->from.node)
-			p->from.node->used++;
+			p->from.node->used = 1;
 
 		if (p->to.type == D_AUTO && p->to.node)
-			p->to.node->used++;
+			p->to.node->used = 1;
 	}
 }

test/fixedbugs/bug423.go の追加

このファイルは、Xというint64型の変数を256回以上(正確には257回)X = 0という代入文で参照することで、修正前のコンパイラでバグが再現することを確認するためのテストケースです。

// 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.

// gc used to overflow a counter when a variable was
// mentioned 256 times, and generate stack corruption.

package main

func main() {
	F(1)
}

func F(arg int) {
	var X int64
	_ = X // used once
	X = 0
	// ... (X = 0 が多数続く)
	X = 0 // used 256 times
	if arg != 0 {
		panic("argument was changed")
	}
}

このテストのif arg != 0 { panic("argument was changed") }という部分は、arg変数がスタック上の0(SP)に配置され、Xのゼロ初期化によって上書きされてしまう場合に、argの値が変更されてパニックを引き起こすことを期待しています。修正後は、argの値は変更されず、テストは正常に終了します。

コアとなるコードの解説

markautoused関数は、Goコンパイラのコード生成フェーズにおいて、自動変数(ローカル変数)が実際に使用されているかどうかをマークする役割を担っています。この関数は、生成されたアセンブリ命令のリスト(Prog)を走査し、命令のfromオペランドやtoオペランドに変数が含まれている場合に、その変数を「使用済み」としてマークします。

変更前は、p->from.node->used++p->to.node->used++のように、変数が参照されるたびにusedカウンターをインクリメントしていました。このusedフィールドは、変数の使用回数を正確に追跡することを意図していた可能性がありますが、uint8型であったため、255を超えるとオーバーフローして0に戻るという問題がありました。

変更後のp->from.node->used = 1;p->to.node->used = 1;というコードは、変数が一度でも参照されたら、そのusedフィールドの値を1に設定します。これにより、変数の正確な使用回数を追跡するのではなく、「使用されたことがある」という状態をシンプルにマークするだけになります。このアプローチは、usedフィールドがuint8型である限り、オーバーフローの問題を完全に回避します。コンパイラが変数を未使用と判断するのはused0の場合のみであるため、1が設定されていれば常に「使用済み」と認識され、誤って0(SP)に配置されることはなくなります。

この修正は、変数の使用回数を厳密に数える必要がなく、単に「使用済みか否か」を判断できれば良いというGoコンパイラの設計思想に合致しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード
  • Go言語のIssueトラッカー
  • Go言語のコードレビューシステム (Gerrit)
  • Goコンパイラの内部構造に関する一般的な情報源 (例: "Go Compiler Internals" などのキーワードでの検索結果)
  • スタックフレーム、スタックポインタ、自動変数に関するコンピュータアーキテクチャの基本概念