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

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

このコミットは、Go言語のCコンパイラ(cc)において、C関数呼び出し時の引数に対するポインタマップを生成する機能を追加するものです。これにより、Goランタイムのガベージコレクタが、Goから呼び出されたC関数のスタックフレーム上のポインタを正確に識別し、メモリ管理を適切に行えるようになります。

コミット

commit 9b1f1833de07763b8c07f838a44d7e44a8837b18
Author: Keith Randall <khr@golang.org>
Date:   Wed Jul 24 09:41:06 2013 -0700

    cc: generate argument pointer maps for C functions.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/11683043

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

https://github.com/golang/go/commit/9b1f1833de07763b8c07f838a44d7e44a8837b18

元コミット内容

cc: generate argument pointer maps for C functions.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/11683043

変更の背景

Go言語は、C言語との相互運用性を提供するためにcgoというメカニズムを持っています。cgoを使用すると、GoコードからC関数を呼び出したり、CコードからGo関数を呼び出したりすることができます。Goのガベージコレクタ(GC)は、プログラムが使用しているメモリを自動的に管理し、不要になったメモリを解放する役割を担っています。GCが正しく機能するためには、ヒープ上に存在するすべてのポインタを正確に識別できる必要があります。

しかし、GoからC関数を呼び出す場合、C関数のスタックフレーム上にはGoのヒープオブジェクトへのポインタが存在する可能性があります。GoのGCは通常、Goのスタックフレームをスキャンしてポインタを識別しますが、C関数のスタックフレームの構造はGoのGCには直接理解できません。このため、C関数のスタックフレーム上に存在するGoヒープへのポインタがGCによって見落とされ、誤って解放されてしまう(Use-After-Free)などの問題が発生する可能性がありました。

このコミットの目的は、cc(GoのCコンパイラ)がC関数をコンパイルする際に、その関数の引数リスト内に存在するポインタの位置を示す「ポインタマップ」を生成することです。このポインタマップは、GoランタイムがC関数を呼び出す際に利用され、GCがC関数のスタックフレームをスキャンする際に、どこにポインタがあるかを正確に把握できるようにします。これにより、GoとCの相互運用環境におけるメモリ安全性が向上します。

前提知識の解説

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

Goのガベージコレクタは、並行マーク&スイープ方式を採用しています。GCは、プログラムが実行中に到達可能なすべてのオブジェクト(ライブオブジェクト)を識別し、到達不可能なオブジェクト(ガベージ)を解放します。このプロセスにおいて、GCはスタック、レジスタ、グローバル変数など、プログラムのルートセットからポインタをたどってライブオブジェクトをマークします。ポインタを正確に識別することは、GCがメモリを安全に管理するための最も重要な要素です。

Goのcgoメカニズム

cgoは、GoプログラムがCライブラリを呼び出すためのGoの機能です。import "C"という特別な構文を使用することで、Goコード内でCの関数や型を宣言し、Goの関数のように呼び出すことができます。cgoは、GoとCの間の呼び出し規約の変換、スタックフレームの管理、およびメモリの受け渡しを処理します。

ポインタマップ

ポインタマップは、ガベージコレクタがメモリ領域(特にスタックフレームやヒープオブジェクト)内のどこにポインタが存在するかを識別するために使用するメタデータです。これは通常、ビットマップ形式で表現され、各ビットが特定のメモリワードがポインタであるかどうかを示します。GCは、このマップを参照することで、メモリ領域を効率的かつ正確にスキャンし、ポインタを識別します。

funcdata

Goランタイムでは、各関数に関するメタデータがfuncdataというセクションに格納されます。このメタデータには、スタックフレームのサイズ、引数の情報、そしてガベージコレクションに関連する情報(ポインタマップなど)が含まれます。FUNCDATA_GCは、特定の関数に関連付けられたGCポインタマップのデータを指す定数です。

Cの呼び出し規約

C言語の関数呼び出し規約は、引数がどのようにスタックにプッシュされるか、戻り値がどのように返されるかなどを定義します。特に、構造体を値で返す場合、多くのCコンパイラは、呼び出し元が提供する隠れたポインタ引数を介して構造体を返すという慣習があります。この隠れたポインタも、GoのGCにとっては重要なポインタとして認識される必要があります。

技術的詳細

このコミットは、src/cmd/cc/pgen.cファイルに以下の主要な変更を加えています。

  1. pointermap_type関数の追加:

    • この関数は、Cの型(Type *t)と、その型が引数リスト内で開始するバイトオフセット(offset)、およびビットマップの基準となるインデックス(baseidx)を受け取ります。
    • 再帰的に型を走査し、ポインタ型(TIND)や配列型(TARRAY、Cでは参照渡しされるためポインタとして扱われる)を見つけると、そのポインタがbaseidxからbaseidx+31の範囲内にある場合に、対応するビットをセットした整数値を返します。
    • 構造体(TSTRUCT)の場合、そのメンバーを再帰的に走査してポインタマップを構築します。
    • 共用体(TUNION)の場合、すべての共用体メンバーが同じポインタマップを持つことを要求します。これは、共用体内のどのメンバーがアクティブであるかをランタイムが知ることができないため、最も保守的なアプローチとして、すべてのメンバーが同じポインタマップを持つことを強制することで、GCが安全に動作するようにするためです。
    • 非ポインタ型(TCHAR, TINT, TFLOATなど)の場合は0を返します。
    • アラインメントのチェックも行い、ポインタが適切にアラインされていることを確認します。
  2. pointermap関数の追加:

    • この関数は、ガベージコレクションシンボル(gcsym)と、ポインタマップデータを書き込むオフセット(off)を受け取ります。
    • Cの可変長引数関数(hasdotdotdot())の場合、ポインタマップの生成を諦め、nptrs=0として処理します。これは、可変長引数の型情報をコンパイル時に完全に特定することが困難なためです。
    • 関数の引数リスト全体のポインタ数を計算します(nptrs)。
    • nptrsgcsymに書き込み、その後にポインタマップのビットベクトルを書き込みます。
    • 引数リストをイテレートし、pointermap_typeを呼び出して各引数のポインタマップビットを収集します。
    • 特に、Cの呼び出し規約で構造体がポインタ経由で返される場合(隠れた最初の引数としてポインタが渡される場合)を考慮し、そのポインタもマップに含めます。
    • 生成されたビットベクトルは、32ビット単位でgcsymに書き込まれます。
  3. codgen関数の変更:

    • codgen関数は、C関数のコードを生成するGoのCコンパイラの主要な部分です。
    • 以前は、FUNCDATA_GCシンボルを生成し、ローカル変数のスタックオフセットとnptrs(ポインタ数)を書き込んでいましたが、このコミットにより、nptrsの書き込み部分がpointermap関数の呼び出しに置き換えられました。
    • これにより、C関数の引数リストに対する詳細なポインタマップがFUNCDATA_GCシンボルに格納されるようになります。
    • gcsym->type->width = off; の行は、gcsymに格納されるGCデータの総サイズを更新しています。

これらの変更により、GoのCコンパイラは、GoランタイムがC関数を呼び出す際に必要とする、引数リスト内のポインタに関する正確なメタデータを生成できるようになります。これにより、GoのGCはC関数が使用するメモリ領域も安全にスキャンし、Goヒープオブジェクトへのポインタを正しく識別できるようになります。

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

src/cmd/cc/pgen.c

--- a/src/cmd/cc/pgen.c
+++ b/src/cmd/cc/pgen.c
@@ -31,6 +31,8 @@
 #include "gc.h"
 #include "../../pkg/runtime/funcdata.h"
 
+static int32 pointermap(Sym *gcsym, int32 offset);
+
 int
 hasdotdotdot(void)
 {
@@ -101,7 +103,22 @@ codgen(Node *n, Node *nn)\n \tp = gtext(n1->sym, stkoff);\n \tsp = p;\n-\t\n+\n+\t/*
+\t * generate funcdata symbol for this function.
+\t * data is filled in at the end of codgen().
+\t */
+\tsnprint(namebuf, sizeof namebuf, "gc·%d", ngcsym++);
+\tgcsym = slookup(namebuf);\n+\tgcsym->class = CSTATIC;\n+\n+\tmemset(&nod, 0, sizeof nod);\n+\tnod.op = ONAME;\n+\tnod.sym = gcsym;\n+\tnod.class = CSTATIC;\n+\n+\tgins(AFUNCDATA, nodconst(FUNCDATA_GC), &nod);\n+\n \t/*
 \t * isolate first argument
 \t */
@@ -139,17 +156,6 @@ codgen(Node *n, Node *nn)\n \t\tmaxargsafe = xround(maxargsafe, 8);\n \tsp->to.offset += maxargsafe;\n \t\n-\tsnprint(namebuf, sizeof namebuf, "gc·%d", ngcsym++);\n-\tgcsym = slookup(namebuf);\n-\tgcsym->class = CSTATIC;\n-\n-\tmemset(&nod, 0, sizeof nod);\n-\tnod.op = ONAME;\n-\tnod.sym = gcsym;\n-\tnod.class = CSTATIC;\n-\n-\tgins(AFUNCDATA, nodconst(FUNCDATA_GC), &nod);\n-\n \t// TODO(rsc): "stkoff" is not right. It does not account for\n \t// the possibility of data stored in .safe variables.\n \t// Unfortunately those move up and down just like\n@@ -162,8 +168,7 @@ codgen(Node *n, Node *nn)\n \toff = 0;\n \tgextern(gcsym, nodconst(stkoff), off, 4); // locals\n \toff += 4;\n-\tgextern(gcsym, nodconst(0), off, 4); // nptrs\n-\toff += 4;\n+\toff = pointermap(gcsym, off); // nptrs and ptrs[...]\n \tgcsym->type = typ(0, T);\n \tgcsym->type->width = off;\n }\n@@ -633,3 +638,105 @@ bcomplex(Node *n, Node *c)\n \tboolgen(n, 1, Z);\n \treturn 0;\n }\n+\n+// Makes a bitmap marking the the pointers in t.  t starts at the given byte\n+// offset in the argument list.  The returned bitmap should be for pointer\n+// indexes (relative to offset 0) between baseidx and baseidx+32.\n+static int32\n+pointermap_type(Type *t, int32 offset, int32 baseidx)\n+{\n+\tType *t1;\n+\tint32 idx;\n+\tint32 m;\n+\n+\tswitch(t->etype) {\n+\tcase TCHAR:\n+\tcase TUCHAR:\n+\tcase TSHORT:\n+\tcase TUSHORT:\n+\tcase TINT:\n+\tcase TUINT:\n+\tcase TLONG:\n+\tcase TULONG:\n+\tcase TVLONG:\n+\tcase TUVLONG:\n+\tcase TFLOAT:\n+\tcase TDOUBLE:\n+\t\t// non-pointer types\n+\t\treturn 0;\n+\tcase TIND:\n+\tcase TARRAY: // unlike Go, C passes arrays by reference\n+\t\t// pointer types\n+\t\tif((offset + t->offset) % ewidth[TIND] != 0)\n+\t\t\tyyerror("unaligned pointer");\n+\t\tidx = (offset + t->offset) / ewidth[TIND];\n+\t\tif(idx >= baseidx && idx < baseidx + 32)\n+\t\t\treturn 1 << (idx - baseidx);\n+\t\treturn 0;\n+\tcase TSTRUCT:\n+\t\t// build map recursively\n+\t\tm = 0;\n+\t\tfor(t1=t->link; t1; t1=t1->down)\n+\t\t\tm |= pointermap_type(t1, offset, baseidx);\n+\t\treturn m;\n+\tcase TUNION:\n+\t\t// We require that all elements of the union have the same pointer map.\n+\t\tm = pointermap_type(t->link, offset, baseidx);\n+\t\tfor(t1=t->link->down; t1; t1=t1->down) {\n+\t\t\tif(pointermap_type(t1, offset, baseidx) != m)\n+\t\t\t\tyyerror("invalid union in argument list - pointer maps differ");\n+\t\t}\n+\t\treturn m;\n+\tdefault:\n+\t\tyyerror("can't handle arg type %s\\n", tnames[t->etype]);\n+\t\treturn 0;\n+\t}\n+}\n+\n+// Compute a bit vector to describe the pointer containing locations\n+// in the argument list.  Adds the data to gcsym and returns the offset\n+// of end of the bit vector.\n+static int32\n+pointermap(Sym *gcsym, int32 off)\n+{\n+\tint32 nptrs;\n+\tint32 i;\n+\tint32 s;     // offset in argument list (in bytes)\n+\tint32 m;     // current ptrs[i/32]\n+\tType *t;\n+\n+\tif(hasdotdotdot()) {\n+\t\t// give up for C vararg functions.\n+\t\t// TODO: maybe make a map just for the args we do know?\n+\t\tgextern(gcsym, nodconst(0), off, 4); // nptrs=0\n+\t\treturn off + 4;\n+\t}\n+\tnptrs = (argsize() + ewidth[TIND] - 1) / ewidth[TIND];\n+\tgextern(gcsym, nodconst(nptrs), off, 4);\n+\toff += 4;\n+\n+\tfor(i = 0; i < nptrs; i += 32) {\n+\t\t// generate mask for ptrs at offsets i ... i+31\n+\t\tm = 0;\n+\t\ts = align(0, thisfn->link, Aarg0, nil);\n+\t\tif(s > 0 && i == 0) {\n+\t\t\t// C Calling convention returns structs by copying\n+\t\t\t// them to a location pointed to by a hidden first\n+\t\t\t// argument.  This first argument is a pointer.\n+\t\t\tif(s != ewidth[TIND])\n+\t\t\t\tyyerror("passbyptr arg not the right size");\n+\t\t\tm = 1;\n+\t\t}\n+\t\tfor(t=thisfn->down; t!=T; t=t->down) {\n+\t\t\tif(t->etype == TVOID)\n+\t\t\t\tcontinue;\n+\t\t\ts = align(s, t, Aarg1, nil);\n+\t\t\tm |= pointermap_type(t, s, i);\n+\t\t\ts = align(s, t, Aarg2, nil);\n+\t\t}\n+\t\tgextern(gcsym, nodconst(m), off, 4);\n+\t\toff += 4;\n+\t}\n+\treturn off;\n+\t// TODO: needs a test for nptrs>32\n+}\n```

## コアとなるコードの解説

### `pointermap_type` 関数

この関数は、C言語の型定義を再帰的に解析し、その型がメモリ上でポインタを保持している可能性のある位置をビットマップとして表現します。

*   **`switch(t->etype)`**: Cの型の種類(`etype`)に基づいて処理を分岐します。
    *   **非ポインタ型**: `TCHAR`, `TINT`, `TFLOAT`などの基本型はポインタではないため、`0`を返します。
    *   **ポインタ型 (`TIND`) および配列型 (`TARRAY`)**: Cでは配列は参照渡しされるため、実質的にポインタとして扱われます。これらの型の場合、引数リスト内でのオフセット(`offset + t->offset`)をポインタのサイズ(`ewidth[TIND]`)で割ることで、ポインタのインデックス(`idx`)を計算します。このインデックスが現在のビットマップ範囲(`baseidx`から`baseidx+31`)内であれば、対応するビットをセットして返します(例: `1 << (idx - baseidx)`)。アラインメントのチェックも行われます。
    *   **構造体 (`TSTRUCT`)**: 構造体は複数のメンバーを持つため、`for`ループで各メンバー(`t1`)を走査し、`pointermap_type`を再帰的に呼び出して、すべてのメンバーのポインタマップをビットOR演算で結合します。
    *   **共用体 (`TUNION`)**: 共用体はメモリを共有するため、その中のどのメンバーが現在アクティブであるかをコンパイル時に判断できません。そのため、この実装では、共用体のすべてのメンバーが同じポインタマップを持つことを厳密に要求します。もし異なるポインタマップを持つメンバーが存在する場合、`yyerror`(コンパイルエラー)を発生させます。これは、GCが安全に動作するための保守的な設計判断です。
    *   **その他**: 未対応の型が来た場合はエラーを発生させます。

### `pointermap` 関数

この関数は、特定のC関数の引数リスト全体に対するポインタマップを生成し、それを`gcsym`(ガベージコレクションシンボル)に書き込みます。

*   **`if(hasdotdotdot())`**: Cの可変長引数関数(例: `printf`)の場合、引数の型情報がコンパイル時に完全に不明なため、ポインタマップの生成を諦めます。この場合、`nptrs=0`としてGCデータに書き込み、GCはこの関数呼び出しの引数リストをスキャンしないことになります。
*   **`nptrs = (argsize() + ewidth[TIND] - 1) / ewidth[TIND];`**: 引数リスト全体のサイズをポインタのサイズで割り、引数リストに含まれるポインタの総数(またはポインタワードの数)を計算します。
*   **`gextern(gcsym, nodconst(nptrs), off, 4);`**: 計算された`nptrs`の値を`gcsym`に書き込みます。これは、GCがこの関数呼び出しの引数リストをスキャンする際に、いくつのポインタワードを期待するかを示します。
*   **`for(i = 0; i < nptrs; i += 32)`**: ポインタマップを32ビットのチャンク(`m`)で生成します。これは、GoのGCがポインタマップを32ビットのビットベクトルとして扱うためです。
*   **隠れた第一引数の処理**: Cの呼び出し規約では、構造体を値で返す関数は、戻り値の構造体を格納するための隠れたポインタを第一引数として受け取ることがあります。このポインタもGoのヒープオブジェクトを指す可能性があるため、`if(s > 0 && i == 0)`のブロックで特別に処理され、ポインタマップに含められます。
*   **`for(t=thisfn->down; t!=T; t=t->down)`**: 関数の各引数(`t`)を走査します。
*   **`m |= pointermap_type(t, s, i);`**: 各引数に対して`pointermap_type`を呼び出し、その引数内のポインタマップビットを現在のチャンク`m`に結合します。
*   **`gextern(gcsym, nodconst(m), off, 4);`**: 生成された32ビットのポインタマップチャンク`m`を`gcsym`に書き込みます。
*   **`return off;`**: ポインタマップデータの書き込みが終了した後のオフセットを返します。

### `codgen` 関数の変更

*   以前は、`FUNCDATA_GC`シンボルを生成した後、ローカル変数のスタックオフセットと`nptrs`(ポインタ数)を直接`gcsym`に書き込んでいました。
*   このコミットでは、`nptrs`を直接書き込む代わりに、新しく追加された`pointermap(gcsym, off)`関数を呼び出すように変更されました。これにより、C関数の引数リストの型情報に基づいて、より詳細で正確なポインタマップが自動的に生成され、`FUNCDATA_GC`シンボルに格納されるようになります。
*   `gcsym->type->width = off;` の行は、`gcsym`に格納されるGCデータの総サイズを更新しています。

これらの変更により、GoのCコンパイラは、GoランタイムがC関数を呼び出す際に必要とする、引数リスト内のポインタに関する正確なメタデータを生成できるようになります。これにより、GoのGCはC関数が使用するメモリ領域も安全にスキャンし、Goヒープオブジェクトへのポインタを正しく識別できるようになります。

## 関連リンク

*   Go言語のガベージコレクションに関する公式ドキュメントやブログ記事
*   Go言語の`cgo`に関する公式ドキュメント
*   Goランタイムの`funcdata`に関する情報(Goのソースコードや関連する設計ドキュメント)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント: [https://golang.org/doc/](https://golang.org/doc/)
*   Goのガベージコレクションに関するブログ記事や論文 (例: "Go's new GC: less latency, more throughput")
*   Goのソースコード (`src/runtime/mgc.go`, `src/cmd/cgo/`)
*   GoのIssueトラッカーやデザインドキュメント (例: Go CL 11683043)