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

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

コミット

このコミットは、Goランタイムのガベージコレクション(GC)コードにおける特定のコンパイラバグ(通称「5cバグ」)を回避するための修正を導入しています。具体的には、*p++ = struct_literal という形式のコードがGoコンパイラ 5c によって誤ってコンパイルされる問題に対応し、ポインタのインクリメントと構造体リテラルの代入を2つの独立した操作に分割することで、このバグによる誤動作を防いでいます。

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

https://github.com/golang/go/commit/6e981c181ce1b8dd54ad83107cfddc954eea668f

元コミット内容

runtime: work around 5c bug in GC code.

5c miscompiles *p++ = struct_literal.

R=dave, golang-dev
CC=golang-dev
https://golang.org/cl/7065069

変更の背景

この変更の背景には、当時のGoコンパイラ 5c に存在した特定のバグがあります。5c は、Goのソースコードを機械語に変換するコンパイラの一つで、特にARMアーキテクチャ向けのコンパイラを指すことが多いです。このバグは、*p++ = struct_literal という構文、つまり「ポインタ p が指すメモリ位置に構造体リテラルを代入し、その後ポインタ p をインクリメントする」という操作を単一の式で行う場合に発生しました。

ガベージコレクション(GC)のコードは、メモリ管理の根幹をなす部分であり、その正確性はシステムの安定性とパフォーマンスに直結します。もしGCコードがコンパイラのバグによって誤動作すれば、メモリリーク、クラッシュ、データの破損など、深刻な問題を引き起こす可能性があります。このため、コンパイラのバグが修正されるまでの間、GCコードの堅牢性を確保するために、バグを回避するコード変更が必要となりました。

このコミットは、コンパイラ自体の修正を待つのではなく、影響を受けるコードパターンを安全な代替パターンに書き換えることで、緊急の回避策を講じたものです。これにより、GoランタイムのGCが正しく機能し続けることが保証されました。

前提知識の解説

Goのガベージコレクション (GC)

Go言語は、自動メモリ管理のためにガベージコレクタ(GC)を採用しています。開発者は手動でメモリを解放する必要がなく、GCが不要になったメモリ領域を自動的に回収します。GoのGCは、並行マーク&スイープ方式をベースにしており、プログラムの実行と並行して動作することで、アプリケーションの一時停止(ストップ・ザ・ワールド)時間を最小限に抑えるように設計されています。

mgc0.c

src/pkg/runtime/mgc0.c は、Goランタイムのガベージコレクションの中核部分を実装しているC言語のファイルです。Goランタイムの一部は、パフォーマンスや低レベルのメモリ操作のためにC言語で書かれています。このファイルには、オブジェクトのスキャン、マーク、スイープといったGCの主要なロジックが含まれています。

PtrTargetBitTarget

これらはGoランタイムのGC内部で使用される構造体です。

  • PtrTarget: GCがスキャンすべきポインタとその型情報(または関連情報)を保持するために使用されます。例えば、オブジェクト内の他のオブジェクトへの参照を追跡するために使われます。
  • BitTarget: GCがマークビットを操作するために使用されます。オブジェクトがマークされたかどうか、または割り当てられたかどうかを示すビット情報を管理します。

これらの構造体は、GCがメモリグラフを走査し、到達可能なオブジェクトを特定する際に、一時的なバッファに格納されます。

*p++ = struct_literal

C言語(およびGoランタイムのCコード)におけるこの構文は、以下のような操作を単一の式で行います。

  1. p が指すメモリ位置に struct_literal の内容をコピー(代入)します。
  2. その後、ポインタ p をその型が指すサイズ分だけインクリメントします。

これは非常に簡潔な記述ですが、当時の 5c コンパイラはこの特定のパターンを正しく機械語に変換できないバグを抱えていました。

Goコンパイラ 5c

Goの初期のコンパイラツールチェーンは、6g (amd64), 8g (386), 5g (arm) のように、ターゲットアーキテクチャを示す数字と g (Goコンパイラ) の組み合わせで命名されていました。5c は、GoのソースコードをC言語に変換し、その後Cコンパイラでコンパイルする、あるいは直接ARMアーキテクチャ向けの機械語を生成するコンパイラの一部を指す可能性があります。この文脈では、特にARMアーキテクチャ向けのコンパイラがこのバグを抱えていたことを示唆しています。

技術的詳細

このコミットが修正しているのは、GoランタイムのGCコード内で頻繁に発生する、ポインタのインクリメントと構造体リテラルの代入を組み合わせたパターンです。

元のコードは以下の形式でした:

*some_pointer_variable++ = (SomeStructType){field1_value, field2_value, ...};

この単一の式は、some_pointer_variable が指すメモリ位置に {field1_value, field2_value, ...} という構造体リテラルを代入し、その後 some_pointer_variable をインクリメントするという動作を期待していました。しかし、Goコンパイラ 5c はこの複合的な操作を正しく機械語に変換できず、結果としてGCの動作に誤りをもたらしていました。

このコミットによる修正は、この単一の式を2つの独立した操作に分割することで、コンパイラのバグを回避しています。

修正後のコードは以下の形式になります:

*some_pointer_variable = (SomeStructType){field1_value, field2_value, ...};
some_pointer_variable++;

この変更により、まず some_pointer_variable が指すメモリ位置に構造体リテラルが代入され、その後に some_pointer_variable が明示的にインクリメントされます。この2段階の操作は、5c コンパイラが正しく処理できるパターンであり、元の意図された動作を正確に再現します。

この修正は、GCのロジック自体を変更するものではなく、GCが使用するデータ構造(PtrTargetBitTarget)をバッファに格納する際の低レベルなC言語の記述方法を調整するものです。これにより、コンパイラのバグによるGCの誤動作を防ぎ、Goランタイムの安定性を確保しています。

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

変更は src/pkg/runtime/mgc0.c ファイルに集中しています。具体的には、flushptrbuf 関数と scanblock 関数内の4箇所で、*ptrbufpos++ = (PtrTarget){...} または *bitbufpos++ = (BitTarget){...} の形式の行が修正されています。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -338,7 +338,8 @@ flushptrbuf(PtrTarget *ptrbuf, PtrTarget **ptrbufpos, Obj **_wp, Workbuf **_wbuf
 			if((bits & (bitAllocated|bitMarked)) != bitAllocated)
 				continue;
 
-			*bitbufpos++ = (BitTarget){obj, ti, bitp, shift};
+			*bitbufpos = (BitTarget){obj, ti, bitp, shift};
+			bitbufpos++;
 		}
 
 		runtime·lock(&lock);
@@ -541,7 +542,8 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n 			
 			// iface->tab
 			if((void*)iface->tab >= arena_start && (void*)iface->tab < arena_used) {
-				*ptrbufpos++ = (PtrTarget){iface->tab, (uintptr)itabtype->gc};\n+				*ptrbufpos = (PtrTarget){iface->tab, (uintptr)itabtype->gc};\n+				ptrbufpos++;
 				if(ptrbufpos == ptrbuf_end)
 					flushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj, bitbuf);
 			}
@@ -568,7 +570,8 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n 				stack_top.b += PtrSize;
 				obj = *(byte**)i;
 				if(obj >= arena_start && obj < arena_used) {
-					*ptrbufpos++ = (PtrTarget){obj, 0};\n+					*ptrbufpos = (PtrTarget){obj, 0};\n+					ptrbufpos++;
 					if(ptrbufpos == ptrbuf_end)
 						flushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj, bitbuf);
 				}
@@ -654,7 +657,8 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n 		}
 
 		if(obj >= arena_start && obj < arena_used) {
-			*ptrbufpos++ = (PtrTarget){obj, objti};\n+			*ptrbufpos = (PtrTarget){obj, objti};\n+			ptrbufpos++;
 			if(ptrbufpos == ptrbuf_end)
 				flushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj, bitbuf);
 		}

コアとなるコードの解説

上記の差分が示すように、すべての変更箇所で共通のパターンが適用されています。

例えば、最初の変更箇所である flushptrbuf 関数内では、以下の行が変更されています。

変更前:

*bitbufpos++ = (BitTarget){obj, ti, bitp, shift};

この行は、bitbufpos が指すメモリ位置に BitTarget 構造体のリテラルを代入し、その後 bitbufpos ポインタをインクリメントしていました。

変更後:

*bitbufpos = (BitTarget){obj, ti, bitp, shift};
bitbufpos++;

この変更では、まず bitbufpos が指すメモリ位置に BitTarget 構造体のリテラルを代入します。そして、次の行で bitbufpos ポインタを明示的にインクリメントしています。これにより、*p++ = struct_literal という単一の式が持つ複合的な動作が、コンパイラが正しく解釈できる2つの独立した操作に分解されました。

同様の修正が、scanblock 関数内の3つの箇所でも行われています。これらの箇所では、PtrTarget 構造体のリテラルを ptrbufpos が指すメモリ位置に代入し、その後 ptrbufpos をインクリメントする操作が、同様に2つの独立した行に分割されています。

これらの変更は、GoランタイムのGCがオブジェクトをスキャンし、マークする際に使用する内部バッファ(bitbufptrbuf)への書き込み方法を調整するものです。GCのロジック自体には影響を与えず、コンパイラのバグを回避するための純粋な構文上の修正となっています。これにより、GCの正確性と安定性が確保されます。

関連リンク

参考にした情報源リンク