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

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

このコミットは、Goランタイムのマップイテレータ (Hiter) の初期化に関するバグ修正です。具体的には、mapiterinit 関数内で Hiter 構造体のポインタフィールドがガベージコレクタ (GC) によってスキャンされる前に確実に nil で初期化されるようにすることで、GCの正確性と安定性を向上させています。

コミット

  • Author: Russ Cox rsc@golang.org
  • Date: Wed Mar 26 21:52:29 2014 -0400
  • Commit Message: runtime: initialize complete Hiter during mapiterinit

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

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

元コミット内容

runtime: initialize complete Hiter during mapiterinit

The garbage collector will scan these pointers,
so make sure they are initialized.

LGTM=bradfitz, khr
R=khr, bradfitz
CC=golang-codereviews
https://golang.org/cl/80960047

変更の背景

Goのランタイムは、プログラムのメモリ管理を自動的に行うガベージコレクタ (GC) を内蔵しています。GCは、ヒープ上に割り当てられたオブジェクトの中から、プログラムがまだ参照している(到達可能な)オブジェクトを特定し、それ以外の(到達不可能な)オブジェクトが占めるメモリを解放します。このプロセスにおいて、GCはオブジェクト内のポインタフィールドをスキャンし、それらが指す他のオブジェクトを追跡します。

Goのマップ(map型)をイテレートする際、ランタイムは内部的に runtime.Hiter という構造体を使用します。この Hiter 構造体は、イテレーションの状態を保持するために、マップのデータや現在のバケットなどを指す複数のポインタフィールドを含んでいます。

mapiterinit 関数は、マップイテレーションを開始する際に Hiter 構造体を初期化する役割を担っています。しかし、特定のシナリオ(例えば、イテレート対象のマップが nil である場合や、要素が一つも含まれていない場合など)では、mapiterinit 関数が Hiter の一部のポインタフィールドを明示的に初期化しないまま処理を終えてしまう可能性がありました。

未初期化のポインタフィールドは、メモリ上に不定な値(以前使用されていたデータの残骸など)を含んでいます。ガベージコレクタがこのような不定な値を持つポインタをスキャンしようとすると、GCは不正なメモリアドレスを参照しようとしたり、予期せぬ動作を引き起こしたり、最悪の場合、プログラムのクラッシュにつながる可能性がありました。

このコミットは、このようなGCの不安定性や誤動作を防ぐために導入されました。mapiterinit 関数が呼び出された際に、Hiter 構造体内のすべてのポインタフィールドがGCによってスキャンされる前に、確実に nil で初期化されるようにすることで、GCの正確性と堅牢性を保証することが目的です。

前提知識の解説

Goのガベージコレクタ (GC)

GoのGCは、並行マーク&スイープ方式をベースとしたトレース型GCです。プログラムの実行と並行して動作し、以下の主要なフェーズで構成されます。

  1. マークフェーズ: プログラムのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。この際、オブジェクト内のポインタをたどって、参照されている他のオブジェクトもマークしていきます。
  2. マークターミネーションフェーズ: マークフェーズの終了処理を行います。
  3. スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが占めるメモリを解放し、再利用可能な状態にします。

GCがポインタをスキャンする際、そのポインタが有効なメモリを指しているか、あるいは nil であるかを正確に判断する必要があります。未初期化のポインタは、GCにとって「どこを指しているか分からない」状態であり、これが問題の根源となります。

ポインタスキャンとGCの安全性

GCは、ヒープ上のメモリ領域を「ポインタを含む領域」と「ポインタを含まない領域」に区別して扱います。ポインタを含む領域をスキャンする際には、その領域内の各ワードがポインタであるかどうかを判断し、もしポインタであればその指す先を追跡します。このとき、もしポインタが未初期化で不定な値を保持していると、GCは存在しないメモリ領域をスキャンしようとしたり、誤って有効なオブジェクトを解放してしまったりする可能性があります。これは「GCの安全性 (GC safety)」と呼ばれる重要な概念であり、GCが正しく動作するための前提条件です。

Goのマップ (runtime.Hmap)

Goの組み込み型である map は、内部的にはハッシュテーブルとして実装されています。ランタイムレベルでは、このハッシュテーブルは runtime.Hmap 構造体によって表現されます。Hmap は、キーと値のペアを格納するためのバケット(配列)や、マップの現在の状態(要素数、ハッシュシードなど)を管理します。

マップイテレータ (runtime.Hiter)

Goで for key, value := range myMap のようにマップをイテレートする際、コンパイラは内部的に runtime.Hiter 構造体と、それを操作するランタイム関数を生成します。Hiter 構造体は、イテレーションの現在の状態を保持するために設計されており、以下のようなポインタフィールドを含んでいます(Goのバージョンによってフィールドは異なる場合がありますが、概念は同じです):

  • key: 現在のイテレーションで取得されるキーへのポインタ。
  • value: 現在のイテレーションで取得される値へのポインタ。
  • t: マップの型情報 (runtime._type へのポインタ)。
  • h: イテレート対象のマップ (runtime.Hmap へのポインタ)。
  • buckets: マップのバケット配列へのポインタ。
  • bptr: 現在処理中のバケット内の位置を示すポインタ。

これらのポインタフィールドは、GCがマップイテレータの状態を正確に理解し、イテレータが参照しているメモリが解放されないようにするために、GCによってスキャンされる必要があります。

mapiterinit 関数

mapiterinit は、Goランタイムの内部関数であり、マップイテレーションを開始する際に呼び出されます。この関数は、与えられたマップ (Hmap) とマップの型情報 (MapType) を基に、Hiter 構造体を適切に初期化し、イテレーションの準備を整えます。

技術的詳細

このコミットの変更は、Goランタイムの src/pkg/runtime/hashmap.goc ファイルにあります。このファイルは、Goのマップの実装に関するCGoコードを含んでいます。

変更の核心は、mapiterinit 関数の冒頭に、Hiter 構造体 it のすべてのポインタフィールドを明示的に nil に初期化するコードが追加された点です。

// Clear pointer fields so garbage collector does not complain.
it->key = nil;
it->value = nil;
it->t = nil;
it->h = nil;
it->buckets = nil;
it->bptr = nil;

この変更が重要である理由は以下の通りです。

  1. GCの正確性の保証: GoのGCは、ポインタが指すメモリ領域を追跡することで、到達可能なオブジェクトを特定します。もし Hiter のポインタフィールドが未初期化のままであると、それらは不定なメモリアドレスを指している可能性があります。GCがこのような不定なポインタをスキャンしようとすると、存在しないメモリ領域にアクセスしたり、誤ったオブジェクトを「生きている」と判断したり、あるいはその逆で、まだ使用されているオブジェクトを誤って解放してしまったりする可能性があります。nil で初期化することで、GCはこれらのフィールドが有効なポインタを指していない(つまり、何も参照していない)ことを確実に認識できます。

  2. 早期リターンパスの安全性: mapiterinit 関数は、イテレート対象のマップが nil であったり、要素が一つも含まれていなかったりする場合に、早期にリターンするパスを持っています。このような場合、イテレータは実際にマップの要素を指す必要がないため、一部のポインタフィールドが初期化されないままになることがありました。しかし、GCは関数の実行パスに関わらず、スタック上の変数(この場合は Hiter 構造体)をスキャンする可能性があります。このコミットにより、どのような実行パスを通っても、GCがスキャンする前に Hiter のポインタフィールドが常に既知の安全な状態(nil)にあることが保証されます。

  3. デバッグの容易性: 未初期化のポインタによるGC関連のバグは、再現が困難でデバッグが非常に難しい場合があります。ポインタを明示的に nil に初期化することで、このような潜在的なバグの発生を防ぎ、ランタイムの堅牢性を高めます。

#pragma textflag NOSPLIT は、この関数がスタックを分割しないことを示すコンパイラディレクティブです。これはGoランタイムの低レベルな部分でよく使用され、スタックの成長によるオーバーヘッドを避けるために、関数が非常に小さい場合や、GCの動作に密接に関わる場合に適用されます。

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

--- a/src/pkg/runtime/hashmap.goc
+++ b/src/pkg/runtime/hashmap.goc
@@ -1004,6 +1004,14 @@ func reflect·mapdelete(t *MapType, h *Hmap, key *byte) {
 
 #pragma textflag NOSPLIT
 func mapiterinit(t *MapType, h *Hmap, it *Hiter) {
+	// Clear pointer fields so garbage collector does not complain.
+	it->key = nil;
+	it->value = nil;
+	it->t = nil;
+	it->h = nil;
+	it->buckets = nil;
+	it->bptr = nil;
+
  	if(h == nil || h->count == 0) {
  		it->key = nil;
  		return;

コアとなるコードの解説

上記の差分は、src/pkg/runtime/hashmap.goc ファイル内の mapiterinit 関数に対する変更を示しています。

変更点は、mapiterinit 関数の本体の冒頭に、以下の8行が追加されたことです。

	// Clear pointer fields so garbage collector does not complain.
	it->key = nil;
	it->value = nil;
	it->t = nil;
	it->h = nil;
	it->buckets = nil;
	it->bptr = nil;
  • it は、mapiterinit 関数に渡される Hiter 構造体へのポインタです。
  • 追加された各行は、Hiter 構造体の個々のポインタフィールド (key, value, t, h, buckets, bptr) を明示的に nil に設定しています。
  • この初期化は、関数がどのような実行パスをたどるかに関わらず、mapiterinit が呼び出された直後に実行されます。
  • これにより、GCがこれらのポインタフィールドをスキャンする際に、常に既知の安全な値(nil)が保証され、未初期化のポインタによるGCの誤動作やクラッシュが効果的に防止されます。
  • 特に、if(h == nil || h->count == 0) のような早期リターン条件の前にこれらの初期化が行われるため、マップが空である場合でも、イテレータのポインタフィールドは適切に初期化されます。

関連リンク

  • Go issue tracker (CL 80960047): https://golang.org/cl/80960047 (このCL番号は古いか内部的な参照である可能性があり、公開されているGoのGerritやIssueトラッカーでは直接詳細が見つからない場合がありますが、コミットメッセージに記載されているためここに含めます。)

参考にした情報源リンク

  • Go言語の公式ドキュメント (Goのガベージコレクション、マップの実装に関する一般的な情報)
  • Goのソースコード (特に runtime パッケージ内の hashmap.goc および関連ファイル)
  • Goのガベージコレクションに関する技術記事や論文 (一般的なGCの概念理解のため)
  • Goのマップ実装に関する技術記事 (一般的なマップの内部構造理解のため)