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

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

このコミットは、Goのcmd/cgoツールにおける重要な修正を導入しています。具体的には、GoのポインタがCの関数呼び出しに渡された際に、そのポインタが呼び出しの期間中、ガベージコレクタによって回収されないように保持するメカニズムを改善しています。これにより、GoとCの相互運用におけるポインタの安全性が向上し、未定義の動作やクラッシュを防ぎます。

コミット

commit 5639d2754b1c9e33bc4440e23d21726d2cc3454b
Author: Russ Cox <rsc@golang.org>
Date:   Tue Sep 24 15:52:48 2013 -0400

    cmd/cgo: retain Go pointer passed to C call for duration of call
    
    Fixes #6397.
    
    R=golang-dev, bradfitz, iant
    CC=golang-dev
    https://golang.org/cl/13858043

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

https://github.com/golang/go/commit/5639d2754b1c9e33bc4440e23d21726d2cc3454b

元コミット内容

cmd/cgo: retain Go pointer passed to C call for duration of call

このコミットは、GoのポインタがCの関数呼び出しに渡された際に、そのポインタがCの呼び出しが完了するまでガベージコレクタによって保持されるように修正します。これは、Goのガベージコレクタがポインタを早期に回収してしまうことによって発生する可能性のある問題(Issue #6397)を解決するためのものです。

変更の背景

GoとCのコードを連携させるCgoを使用する際、Goのメモリ管理(ガベージコレクション)とCのメモリ管理(手動管理)の間には根本的な違いがあります。Goのガベージコレクタは、参照されなくなったメモリを自動的に解放します。しかし、GoのポインタがCの関数に渡されると、GoのランタイムはそのポインタがCコードによってまだ使用されていることを認識できません。このため、Cコードがそのポインタを使用している最中に、Goのガベージコレクタがそのポインタが指すメモリを解放してしまう可能性がありました。これは「use-after-free」バグとして知られる深刻な問題を引き起こし、プログラムのクラッシュや予期せぬ動作につながります。

このコミットは、この問題を解決するために、Cgoが生成するコードにおいて、GoのポインタがCの関数呼び出しの期間中、ガベージコレクタによって確実に保持されるように変更を加えています。これにより、GoとCの相互運用におけるメモリ安全性が向上します。

前提知識の解説

  • Cgo: Go言語からC言語の関数を呼び出したり、C言語からGo言語の関数を呼び出したりするためのGoのツールです。GoとCの間のインターフェースを生成します。
  • ガベージコレクション (GC): Go言語の主要な特徴の一つで、不要になったメモリ領域を自動的に解放する仕組みです。プログラマが手動でメモリを管理する手間を省き、メモリリークなどのバグを減らします。
  • ポインタ: メモリ上の特定のアドレスを指し示す変数です。GoとCの両方でポインタが使用されますが、その管理方法は異なります。
  • GoのポインタとCのポインタ: Goのポインタはガベージコレクタによって管理されますが、Cのポインタは通常、プログラマが手動でmallocfreeなどを使って管理します。この管理方法の違いが、GoとCの相互運用における課題の一つとなります。
  • runtime·cgocall: Cgoが生成するコード内で使用されるGoランタイムの内部関数で、GoからCの関数を呼び出す際に使用されます。この関数は、Cの関数呼び出しの前後でGoのランタイムの状態を適切に管理する役割を担います。
  • GCマップ (GC bitvector): Goのガベージコレクタがメモリをスキャンする際に、どのメモリ領域にポインタが含まれているかを識別するために使用される情報です。これにより、ガベージコレクタはポインタが指すオブジェクトを追跡し、誤って回収してしまうことを防ぎます。

技術的詳細

このコミットは、主にsrc/cmd/cc/pgen.csrc/cmd/cgo/out.goの2つのファイルに変更を加えています。

src/cmd/cc/pgen.cの変更

このファイルは、CgoがCのコードをコンパイルする際に、GoのガベージコレクタがCのスタックフレーム内のポインタを認識できるようにするためのGCマップ情報を生成する部分です。

  • walktype1関数の変更:
    • walktype1関数は、与えられた型tとオフセットoffsetに基づいて、GCビットベクトルbvを更新します。このビットベクトルは、ガベージコレクタがポインタを識別するために使用されます。
    • 変更前は、TARRAY(配列型)の場合、Goとは異なりCでは配列が参照渡しされるため、TIND(ポインタ型)と同じ処理(goto pointer)をしていました。
    • 変更後、walktype1関数にparamという新しい引数が追加されました。このparamは、その型が関数のパラメータとして渡されているかどうかを示します。
    • TARRAYの場合、paramが真(つまり関数のパラメータとして配列が渡されている)であれば、以前と同様にgoto pointerでポインタとして扱います。
    • しかし、paramが偽(構造体や共用体内の配列など)であれば、配列は実際の配列として扱われ、その要素を再帰的にwalktype1で処理するように変更されました。これにより、構造体内の配列のGCマップがより正確に生成されるようになります。
    • また、TSTRUCTTUNIONの場合のwalktype1の再帰呼び出しにも、param引数として0(偽)が渡されるようになりました。これは、構造体や共用体のメンバーは関数のパラメータではないため、配列の特殊な扱いを適用しないことを意味します。
  • dumpgcargs関数の変更:
    • dumpgcargs関数は、Cの関数に渡される引数のGCマップ情報をダンプします。
    • この関数内でwalktype1を呼び出す際に、新しいparam引数として1(真)が渡されるようになりました。これは、Cの関数に渡される引数は常にパラメータとして扱われるため、配列がポインタとして扱われるべきであることを示しています。

これらの変更により、Cの関数に渡されるGoのポインタ(特に配列がポインタとして扱われる場合)が、Goのガベージコレクタによって正しく認識され、呼び出し期間中に保持されるためのGCマップ情報がより正確に生成されるようになります。

src/cmd/cgo/out.goの変更

このファイルは、CgoがGoのコードからCの関数を呼び出すためのラッパー関数を生成する部分です。

  • writeDefsFunc関数の変更:
    • この関数は、Cgoが生成する_cgo_export.cファイル内のCのラッパー関数の定義を書き込みます。
    • 変更前は、Cのラッパー関数の引数として、uint8 x[argSize]という単一のバイト配列を持つ構造体を定義していました。これは、引数全体のサイズを確保するための一般的な方法でした。
    • 変更後、引数として渡される構造体の定義がより詳細になりました。
      • argSize / p.PtrSizeで計算されるポインタの数だけvoid *y[n]というポインタ配列を定義します。p.PtrSizeはポインタのサイズ(通常4バイトまたは8バイト)です。
      • 残りのバイト数argSize % p.PtrSizeがあれば、uint8 x[n]というバイト配列を定義します。
    • この変更のコメントには、「TODO(rsc): The struct here should declare pointers only where there are pointers in the actual argument frame. This is a workaround for golang.org/issue/6397.」とあります。これは、この変更がIssue #6397に対する暫定的な解決策であり、将来的には引数フレーム内の実際のポインタの位置に基づいてより正確な構造体を宣言する必要があることを示唆しています。
    • この変更の目的は、Cの関数に渡される引数フレーム内にGoのポインタが存在する場合、そのポインタがCgoによって生成されるラッパー関数の引数構造体内で明示的にvoid *として宣言されるようにすることです。これにより、Goのランタイムがruntime·cgocallを介してCの関数を呼び出す際に、引数フレーム内のポインタをより正確に識別し、ガベージコレクタがそれらを適切に保持できるようになります。

これらの変更は、GoのポインタがCの関数呼び出し中にガベージコレクタによって誤って回収されることを防ぐための、Cgoの内部的なメカニズムの改善です。特に、Cの関数に渡される引数フレーム内のポインタの存在をGoのランタイムに正確に伝えることで、ポインタのライフタイム管理を強化しています。

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

src/cmd/cc/pgen.c

--- a/src/cmd/cc/pgen.c
+++ b/src/cmd/cc/pgen.c
@@ -651,9 +651,10 @@ bcomplex(Node *n, Node *c)
 // Updates the bitvector with a set bit for each pointer containing
 // value in the type description starting at offset.
 static void
-walktype1(Type *t, int32 offset, Bvec *bv)
+walktype1(Type *t, int32 offset, Bvec *bv, int param)
 {
 	Type *t1;
+	int32 o;
 
 	switch(t->etype) {
 	case TCHAR:
@@ -672,21 +673,29 @@ walktype1(Type *t, int32 offset, Bvec *bv)
 		break;
 
 	case TIND:
-	case TARRAY: // unlike Go, C passes arrays by reference
+	pointer:
 		// pointer types
 		if((offset + t->offset) % ewidth[TIND] != 0)
 			yyerror("unaligned pointer");
 		bvset(bv, ((offset + t->offset) / ewidth[TIND])*BitsPerPointer);
 		break;
 
+	case TARRAY:
+		if(param)	// unlike Go, C passes arrays by reference
+			goto pointer;
+		// array in struct or union is an actual array
+		for(o = 0; o < t->width; o += t->link->width)
+			walktype1(t->link, offset+o, bv, 0);
+		break;
+
 	case TSTRUCT:
 		// build map recursively
 		for(t1 = t->link; t1 != T; t1 = t1->down)
-			walktype1(t1, offset, bv);
+			walktype1(t1, offset, bv, 0);
 		break;
 
 	case TUNION:
-		walktype1(t->link, offset, bv);
+		walktype1(t->link, offset, bv, 0);
 		break;
 
 	default:
@@ -728,7 +737,7 @@ dumpgcargs(Type *fn, Sym *sym)
 			if(t->etype == TVOID)
 				continue;
 			argoffset = align(argoffset, t, Aarg1, nil);
-			walktype1(t, argoffset, bv);
+			walktype1(t, argoffset, bv, 1);
 			argoffset = align(argoffset, t, Aarg2, nil);
 		}
 		gextern(sym, nodconst(bv->n), 0, 4);

src/cmd/cgo/out.go

--- a/src/cmd/cgo/out.go
+++ b/src/cmd/cgo/out.go
@@ -413,7 +413,17 @@ func (p *Package) writeDefsFunc(fc, fgo2 *os.File, n *Name) {
 	if argSize == 0 {
 		argSize++
 	}
-	fmt.Fprintf(fc, "·%s(struct{uint8 x[%d];}p)\\n", n.Mangle, argSize)
+	// TODO(rsc): The struct here should declare pointers only where
+	// there are pointers in the actual argument frame.
+	// This is a workaround for golang.org/issue/6397.
+	fmt.Fprintf(fc, "·%s(struct{", n.Mangle)
+	if n := argSize / p.PtrSize; n > 0 {
+		fmt.Fprintf(fc, "void *y[%d];", n)
+	}
+	if n := argSize % p.PtrSize; n > 0 {
+		fmt.Fprintf(fc, "uint8 x[%d];", n)
+	}
+	fmt.Fprintf(fc, "}p)\\n")
 	fmt.Fprintf(fc, "{\\n")
 	fmt.Fprintf(fc, "\\truntime·cgocall(_cgo%s%s, &p);\\n", cPrefix, n.Mangle)
 	if n.AddError {

コアとなるコードの解説

src/cmd/cc/pgen.cの変更点

  • walktype1関数のシグネチャ変更とparam引数の導入:
    • static void walktype1(Type *t, int32 offset, Bvec *bv) から static void walktype1(Type *t, int32 offset, Bvec *bv, int param) へ変更されました。
    • param引数は、現在処理している型が関数のパラメータであるかどうかを示すフラグです。
  • TARRAYの処理の分岐:
    • 以前はTARRAY型は常にgoto pointer;でポインタとして扱われていました。
    • 変更後は、if(param)という条件が追加され、paramが真(関数のパラメータとして配列が渡されている)の場合のみgoto pointer;でポインタとして扱われます。
    • paramが偽の場合(構造体や共用体内の配列など)、配列は実際の配列として扱われ、その要素をforループで再帰的にwalktype1を呼び出して処理するようになりました。これにより、構造体内の配列のGCマップがより正確に生成されます。
  • TSTRUCTTUNIONの再帰呼び出し:
    • walktype1(t1, offset, bv); から walktype1(t1, offset, bv, 0); へ変更されました。
    • walktype1(t->link, offset, bv); から walktype1(t->link, offset, bv, 0); へ変更されました。
    • 構造体や共用体のメンバーは関数のパラメータではないため、param引数に0を渡すことで、配列の特殊な扱いを適用しないようにしています。
  • dumpgcargsからのwalktype1呼び出し:
    • walktype1(t, argoffset, bv); から walktype1(t, argoffset, bv, 1); へ変更されました。
    • dumpgcargsはCの関数に渡される引数のGCマップを生成するため、引数は常にパラメータとして扱われるべきであり、param1を渡すことでこれを明示しています。

これらの変更により、CgoはCの関数に渡されるGoのポインタ(特に配列がポインタとして扱われる場合)のGCマップ情報をより正確に生成できるようになり、ガベージコレクタがそれらを適切に保持するための情報を提供します。

src/cmd/cgo/out.goの変更点

  • Cラッパー関数の引数構造体の変更:
    • 以前は、Cgoが生成するCのラッパー関数の引数として、struct{uint8 x[argSize];}pという単一のバイト配列を持つ構造体を定義していました。
    • 変更後は、引数構造体の定義がより詳細になりました。
      • if n := argSize / p.PtrSize; n > 0 { fmt.Fprintf(fc, "void *y[%d];", n) }
        • これは、引数全体のサイズargSizeをポインタのサイズp.PtrSizeで割ることで、引数フレーム内に含まれる可能性のあるポインタの数を計算し、その数だけvoid *型の配列yを宣言します。
      • if n := argSize % p.PtrSize; n > 0 { fmt.Fprintf(fc, "uint8 x[%d];", n) }
        • ポインタのサイズで割り切れない残りのバイト数があれば、それをuint8型の配列xとして宣言します。
    • この変更の目的は、Cの関数に渡される引数フレーム内にGoのポインタが存在する場合、そのポインタがCgoによって生成されるラッパー関数の引数構造体内で明示的にvoid *として宣言されるようにすることです。これにより、Goのランタイムがruntime·cgocallを介してCの関数を呼び出す際に、引数フレーム内のポインタをより正確に識別し、ガベージコレクタがそれらを適切に保持できるようになります。コメントにあるように、これはIssue #6397に対する暫定的な解決策であり、将来的にはより洗練されたポインタ宣言が必要となる可能性が示唆されています。

これらの変更は、GoのポインタがCの関数呼び出し中にガベージコレクタによって誤って回収されることを防ぐための、Cgoの内部的なメカニズムの改善です。特に、Cの関数に渡される引数フレーム内のポインタの存在をGoのランタイムに正確に伝えることで、ポインタのライフタイム管理を強化しています。

関連リンク

参考にした情報源リンク

  • コミットメッセージ (./commit_data/17693.txt)
  • 変更されたソースコード (src/cmd/cc/pgen.c, src/cmd/cgo/out.go)
  • Go言語のCgoに関する一般的な知識
  • ガベージコレクションに関する一般的な知識
  • ポインタに関する一般的な知識
  • Go言語のIssueトラッカー (ただし、Issue #6397は直接見つからなかったため、コミットメッセージとコード変更から推測)