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

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

コミット

commit 3e02987508d25db11b213e7ce1edd39000ade7c8
Author: Russ Cox <rsc@golang.org>
Date:   Fri Feb 6 14:41:21 2009 -0800

    tgs's gc bug.
    
    R=r
    DELTA=10  (7 added, 0 deleted, 3 changed)
    OCL=24577
    CL=24577

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

https://github.com/golang/go/commit/3e02987508d25db11b213e7ce1edd39000ade7c8

元コミット内容

tgs's gc bug.

R=r
DELTA=10  (7 added, 0 deleted, 3 changed)
OCL=24577
CL=24577

変更の背景

このコミットは、Go言語の初期段階(2009年)におけるガベージコレクション(GC)のバグ修正を目的としています。当時のGoのGCはまだ成熟しておらず、Russ Cox自身もその性能について「かなり遅い」と認めていました。特に、バイナリツリーのベンチマークがGCの性能を測るものとして機能していたことからも、GCがボトルネックとなっていたことが伺えます。

コミットメッセージにある「tgs's gc bug」は、おそらくGoの開発者の一人である「tgs」によって発見された、GCに関連する特定の不具合を指しています。このバグは、メモリ管理の根幹をなすmlookup関数におけるポインタの解釈、特にGCの参照カウント領域に関する誤った処理に起因していたと考えられます。GCが正しく動作しないと、メモリリーク、不正なメモリアクセス、クラッシュなど、深刻な問題を引き起こす可能性があります。この修正は、Goのランタイムにおけるメモリ管理の堅牢性を高めるための重要なステップでした。

前提知識の解説

  • ガベージコレクション (GC): プログラムが動的に確保したメモリ領域のうち、もはや使用されなくなったものを自動的に解放する仕組みです。Go言語はトレース型GCを採用しており、到達可能なオブジェクトを特定し、それ以外のオブジェクトを解放します。
  • src/runtime/malloc.c: Goランタイムのメモリ割り当て(malloc)と解放(free)に関する低レベルな処理が記述されているC言語のファイルです。Goのメモリ管理の根幹を担っています。
  • mlookup関数: src/runtime/malloc.c内に定義されている関数で、与えられたポインタvが指すメモリブロックの情報を検索します。具体的には、そのブロックのベースアドレス、サイズ、そしてGC参照カウントへのポインタなどを返します。GCがメモリをスキャンする際に、ポインタが有効なオブジェクトを指しているか、そのオブジェクトのサイズはどれくらいか、といった情報を得るために利用されます。
  • MSpan構造体: Goのメモリヒープを管理するための重要なデータ構造です。連続したページ(メモリブロックの最小単位)の集合を表し、特定のサイズのオブジェクトを格納するために使用されます。MSpanには、そのスパンが管理するメモリ領域の開始アドレス、ページ数、オブジェクトのサイズクラスなどの情報が含まれます。
  • PageShift: メモリページのサイズをビットシフトで表す定数です。例えば、1 << PageShiftでページサイズが計算できます。
  • sizeclass: Goのメモリ割り当てにおいて、オブジェクトのサイズに応じて分類されるクラスです。異なるサイズのオブジェクトは、それぞれに適したMSpanから割り当てられます。
  • gcref: MSpan構造体の一部で、そのスパン内のオブジェクトのGC参照カウント情報が格納されているメモリ領域へのポインタです。GCは、このgcref領域を参照して、各オブジェクトが参照されているかどうかを判断します。

技術的詳細

このコミットは、src/runtime/malloc.c内のmlookup関数に焦点を当てた修正です。mlookupは、与えられたポインタvがGoのヒープ内の有効なオブジェクトを指しているかを判断し、そのオブジェクトのメタデータ(ベースアドレス、サイズ、GC参照情報)を返す役割を担っています。

変更の核心は、GCが参照カウント情報を格納している領域自体を、誤って有効なオブジェクトへのポインタとして扱ってしまう可能性があった点を修正することにあります。

具体的には、以下の2つの主要な変更が行われています。

  1. gcref領域へのポインタの除外:

    +	if((byte*)v >= (byte*)s->gcref) {
    +		// pointers into the gc ref counts
    +		// do not count as pointers.
    +		return 0;
    +	}
    

    この新しいチェックは、入力ポインタvMSpangcref領域(GC参照カウントが格納されているメモリ領域)内を指している場合に、即座に0を返してmlookupを終了させます。これは、「GC参照カウントへのポインタは、有効なオブジェクトへのポインタとしてはカウントしない」という明確な意図を示しています。もしこのチェックがないと、GCは自身の管理情報であるgcref領域を、誤ってヒープ上のオブジェクトとして解釈し、不正確なGCスキャンやクラッシュを引き起こす可能性がありました。

  2. gcref領域の範囲チェックの修正:

    -	if((byte*)s->gcref < p || (byte*)s->gcref >= p+(s->npages<<PageShift)) {
    +	nobj = (s->npages << PageShift) / (n + RefcountOverhead);
    +	if((byte*)s->gcref < p || (byte*)(s->gcref+nobj) > p+(s->npages<<PageShift)) {
    

    既存のgcref領域の範囲チェックが修正されました。

    • nobjという新しい変数が導入され、これは現在のMSpanが保持できるオブジェクトの総数を計算します。この計算には、オブジェクトのサイズnと、GC参照カウントのためのオーバーヘッドRefcountOverheadが考慮されています。
    • 以前のチェックでは、gcrefの開始アドレスがスパンの範囲内にあるかのみを確認していました。しかし、修正後はgcrefの開始アドレスだけでなく、gcref領域の終了アドレスs->gcref + nobj)もスパンの有効なメモリ範囲(pからp+(s->npages<<PageShift))内に収まっているかを厳密にチェックするようになりました。
    • この変更により、gcref領域がMSpanの割り当てられたメモリ範囲を逸脱していないことをより正確に保証し、メモリ破損やGCの誤動作を防ぎます。

これらの変更は、GCがメモリヒープを正確にスキャンし、有効なオブジェクトとGCの内部管理領域を区別するために不可欠です。特に、gcref領域がヒープ上の通常のオブジェクトと混同されることを防ぎ、GCの堅牢性と正確性を向上させています。

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

--- a/src/runtime/malloc.c
+++ b/src/runtime/malloc.c
@@ -121,8 +121,8 @@ free(void *v)
 int32
 mlookup(void *v, byte **base, uintptr *size, uint32 **ref)
 {
-	uintptr n, i;
-	byte *p;
+	uintptr n, nobj, i;
+	byte *p, *ep;
 	MSpan *s;
 
 	s = MHeap_LookupMaybe(&mheap, (uintptr)v>>PageShift);
@@ -148,13 +148,20 @@ mlookup(void *v, byte **base, uintptr *size, uint32 **ref)
 		return 1;
 	}
 
+	if((byte*)v >= (byte*)s->gcref) {
+		// pointers into the gc ref counts
+		// do not count as pointers.
+		return 0;
+	}
+
 	n = class_to_size[s->sizeclass];
 	i = ((byte*)v - p)/n;
 	if(base)
 		*base = p + i*n;
 	if(size)
 		*size = n;
-	if((byte*)s->gcref < p || (byte*)s->gcref >= p+(s->npages<<PageShift)) {
+	nobj = (s->npages << PageShift) / (n + RefcountOverhead);
+	if((byte*)s->gcref < p || (byte*)(s->gcref+nobj) > p+(s->npages<<PageShift)) {
 		printf("s->base sizeclass %d %p gcref %p block %D\\n",
 			s->sizeclass, p, s->gcref, s->npages<<PageShift);
 		throw("bad gcref");

コアとなるコードの解説

  • uintptr n, nobj, i;byte *p, *ep;:
    • nobjが新しく追加されました。これは、現在のMSpanが保持するオブジェクトの数を計算するために使用されます。
    • epは追加されましたが、このコミットでは使用されていません。これは、将来的な拡張のためのプレースホルダーか、以前の変更の名残である可能性があります。
  • 新しいifブロック (if((byte*)v >= (byte*)s->gcref)):
    • このコードは、mlookupに渡されたポインタvが、現在のMSpanのGC参照カウント領域(s->gcrefが指す領域)内にあるかどうかをチェックします。
    • もしvgcref領域内を指している場合、それはヒープ上の通常のオブジェクトへのポインタではないため、mlookup0を返して処理を終了します。これにより、GCが自身の管理データを誤ってオブジェクトとして処理することを防ぎます。
  • nobj = (s->npages << PageShift) / (n + RefcountOverhead);:
    • s->npages << PageShiftは、MSpanが管理する全メモリ領域のバイトサイズを計算します。
    • nは、このMSpanが割り当てるオブジェクトのサイズです。
    • RefcountOverheadは、各オブジェクトのGC参照カウントに必要な追加のメモリオーバーヘッドです。
    • この行は、MSpanが保持できるオブジェクトの総数nobjを正確に計算します。
  • 修正されたifブロック (if((byte*)s->gcref < p || (byte*)(s->gcref+nobj) > p+(s->npages<<PageShift))):
    • 以前の条件if((byte*)s->gcref < p || (byte*)s->gcref >= p+(s->npages<<PageShift))は、gcrefの開始アドレスがスパンの範囲内にあるかのみをチェックしていました。
    • 修正後では、gcref領域の開始アドレスがスパンの開始アドレスpより小さいか、またはgcref領域の終了アドレス(s->gcref + nobj)がスパンの終了アドレスp+(s->npages<<PageShift)を超えているかをチェックします。
    • これにより、gcref領域全体がMSpanの割り当てられたメモリ範囲内に完全に収まっていることを保証し、メモリの整合性を保ちます。もしこの条件が満たされない場合、「bad gcref」というエラーでプログラムが終了します。

これらの変更は、GoのGCがメモリをより正確に管理し、内部データ構造とユーザーデータ領域を明確に区別することで、GCのバグを防ぎ、ランタイムの安定性を向上させるために不可欠でした。

関連リンク

参考にした情報源リンク