[インデックス 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のポインタは通常、プログラマが手動で
malloc
やfree
などを使って管理します。この管理方法の違いが、GoとCの相互運用における課題の一つとなります。 runtime·cgocall
: Cgoが生成するコード内で使用されるGoランタイムの内部関数で、GoからCの関数を呼び出す際に使用されます。この関数は、Cの関数呼び出しの前後でGoのランタイムの状態を適切に管理する役割を担います。- GCマップ (GC bitvector): Goのガベージコレクタがメモリをスキャンする際に、どのメモリ領域にポインタが含まれているかを識別するために使用される情報です。これにより、ガベージコレクタはポインタが指すオブジェクトを追跡し、誤って回収してしまうことを防ぎます。
技術的詳細
このコミットは、主にsrc/cmd/cc/pgen.c
とsrc/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マップがより正確に生成されるようになります。 - また、
TSTRUCT
とTUNION
の場合の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の関数を呼び出す際に、引数フレーム内のポインタをより正確に識別し、ガベージコレクタがそれらを適切に保持できるようになります。
- この関数は、Cgoが生成する
これらの変更は、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マップがより正確に生成されます。
- 以前は
TSTRUCT
とTUNION
の再帰呼び出し: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マップを生成するため、引数は常にパラメータとして扱われるべきであり、param
に1
を渡すことでこれを明示しています。
これらの変更により、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に対する暫定的な解決策であり、将来的にはより洗練されたポインタ宣言が必要となる可能性が示唆されています。
- 以前は、Cgoが生成するCのラッパー関数の引数として、
これらの変更は、GoのポインタがCの関数呼び出し中にガベージコレクタによって誤って回収されることを防ぐための、Cgoの内部的なメカニズムの改善です。特に、Cの関数に渡される引数フレーム内のポインタの存在をGoのランタイムに正確に伝えることで、ポインタのライフタイム管理を強化しています。
関連リンク
- Go Change List: https://golang.org/cl/13858043
参考にした情報源リンク
- コミットメッセージ (
./commit_data/17693.txt
) - 変更されたソースコード (
src/cmd/cc/pgen.c
,src/cmd/cgo/out.go
) - Go言語のCgoに関する一般的な知識
- ガベージコレクションに関する一般的な知識
- ポインタに関する一般的な知識
- Go言語のIssueトラッカー (ただし、Issue #6397は直接見つからなかったため、コミットメッセージとコード変更から推測)