[インデックス 16437] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)、リンカ(cmd/ld
)、およびランタイム(src/pkg/runtime
)における、nosplit
関数(スタックを拡張しない関数)のポインタマップ生成と引数情報処理に関する改善です。特に、リンカによって識別されるnosplit
関数が、これまで適切に扱われていなかったポインタマップと引数情報を持つ場合に対応するための変更が含まれています。
コミット
commit 037a1a9f3183e92a930e25ce66dba99df3786fb0
Author: Carl Shapiro <cshapiro@google.com>
Date: Wed May 29 17:16:57 2013 -0700
cmd/ld, runtime: emit pointer maps for nosplits identified by the linker
A nosplits was assumed to have no argument information and no
pointer map. However, nosplits created by the linker often
have both. This change uses the pointer map size as an
alternate source of argument size when processing a nosplit.
In addition, the symbol table construction pointer map size
and argument size consistency check is strengthened. If a
nptrs is greater than 0 it must be equal to the number of
argument words.
R=golang-dev, khr, khr
CC=golang-dev
https://golang.org/cl/9666047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/037a1a9f3183e92a930e25ce66dba99df3786fb0
元コミット内容
リンカによって識別されるnosplit
関数に対してポインタマップを出力する。
nosplit
関数は、これまで引数情報もポインタマップも持たないと仮定されていました。しかし、リンカによって生成されるnosplit
関数は、しばしばその両方を持っています。この変更は、nosplit
関数を処理する際に、ポインタマップのサイズを引数サイズの代替ソースとして使用します。
さらに、シンボルテーブル構築におけるポインタマップサイズと引数サイズの一貫性チェックが強化されます。nptrs
(ポインタの数)が0より大きい場合、それは引数ワードの数と等しくなければなりません。
変更の背景
Goのランタイム、特にガベージコレクタ(GC)は、スタック上のポインタを正確に識別する必要があります。これにより、GCは到達可能なオブジェクトを正しくマークし、不要なメモリを解放できます。関数が呼び出される際、そのスタックフレームには引数、ローカル変数、および戻り値が格納されます。これらのうち、ポインタを含む可能性のある領域については、GCが参照を追跡できるように「ポインタマップ」という情報が必要です。
nosplit
関数は、スタックの拡張を伴わない(つまり、スタックチェックを行わない)関数です。これは、非常に短い関数や、ランタイムの低レベルな部分(例えば、スケジューラやGC自体)で使われることが多く、スタックチェックのオーバーヘッドを避けるために利用されます。
これまでのGoのコンパイラとリンカの挙動では、nosplit
関数は引数情報やポインタマップを持たないと仮定されていました。しかし、リンカが特定の最適化やコード生成を行う際に、nosplit
としてマークされた関数が実際には引数やポインタを持つ場合がありました。この仮定の不一致が、ガベージコレクションの正確性に問題を引き起こす可能性がありました。具体的には、リンカが生成したnosplit
関数内のポインタがGCによって認識されず、誤って回収されてしまう、あるいは逆に回収されるべきでないメモリが回収されないといった問題が発生し得ました。
このコミットは、この不一致を解消し、リンカによって生成されたnosplit
関数であっても、そのポインタマップと引数情報を正確にランタイムに伝えることを目的としています。これにより、Goのガベージコレクションの堅牢性と正確性が向上します。
前提知識の解説
- ガベージコレクション (GC): Goのランタイムが自動的にメモリを管理する仕組み。不要になったメモリ領域を自動的に解放し、メモリリークを防ぎます。GCは、プログラムが使用しているポインタを追跡し、到達可能なオブジェクトを特定することで機能します。
- ポインタマップ (Pointer Map): 関数のスタックフレームやヒープ上のオブジェクト内に、どのオフセットにポインタが存在するかを示すビットマップのような情報。GCはこれを利用して、メモリ内のポインタを正確に識別し、参照をたどります。
nosplit
関数: Goコンパイラによって生成される関数の一種で、スタックの拡張チェックを行わない関数。通常、非常に短い関数や、ランタイムのクリティカルパスで使用され、スタックチェックのオーバーヘッドを避けるために利用されます。go:nosplit
ディレクティブで指定されることもあります。- リンカ (
cmd/ld
): コンパイラによって生成されたオブジェクトファイル(機械語コード)を結合し、実行可能ファイルを生成するツール。この過程で、シンボル解決、アドレスの再配置、および特定のランタイム情報の埋め込みなどが行われます。 - コンパイラ (
cmd/gc
): Goのソースコードを機械語コードに変換するツール。この過程で、関数のスタックフレーム情報やポインタマップの生成も行われます。 - ランタイム (
src/pkg/runtime
): Goプログラムの実行をサポートするライブラリ。ガベージコレクタ、スケジューラ、メモリ管理など、低レベルな機能を提供します。 - シンボルテーブル (Symbol Table): プログラム内のシンボル(関数名、変数名など)とそのアドレスや型などの情報を格納するデータ構造。リンカやデバッガがプログラムを理解するために使用します。
nptrs
: ポインタマップにおけるポインタの数を示す値。ArgsSizeUnknown
: 引数のサイズが不明であることを示す定数。
技術的詳細
このコミットの核心は、nosplit
関数のポインタマップと引数情報の扱いを修正することにあります。
-
nosplit
関数のポインタマップ生成の統一: 以前は、pgen.c
(コンパイラのポインタマップ生成部分)において、ポインタマップが空の場合(bvisempty(bv)
)はANPTRS
命令のto.offset
を0に設定し、ポインタマップの生成をスキップしていました。しかし、この変更により、ポインタマップが空であるかどうかにかかわらず、常にANPTRS
命令とそれに続くAPTRS
命令が生成されるようになりました。これにより、リンカが生成するnosplit
関数であっても、ポインタマップのメタデータが常に存在することが保証されます。 -
リンカにおける
nosplit
関数の引数サイズ処理の改善:cmd/ld/lib.c
のgenasmsym
関数は、アセンブリシンボルを生成する際に、関数の引数サイズを決定します。以前は、NOSPLIT
フラグが設定されている関数に対しては、無条件に引数サイズをArgsSizeUnknown
としていました。 この変更では、NOSPLIT
フラグが設定されており、かつs->args == 0
(コンパイラが認識する引数サイズが0)であり、かつs->nptrs < 0
(ポインタの数が負、これはポインタマップ情報がまだ適切に設定されていないことを示唆する可能性)の場合にのみ、引数サイズをArgsSizeUnknown
とします。 この修正により、リンカが生成したnosplit
関数が、たとえコンパイラが引数サイズを0と認識していても、ポインタマップ情報(s->nptrs
)が正しく設定されていれば、そのポインタマップサイズを引数サイズの代替として利用できるようになります。これは、リンカが生成するコードが、コンパイラが直接生成するコードとは異なる方法で引数情報を表現する可能性があるため重要です。 -
ランタイムにおけるシンボルテーブルの一貫性チェックの強化:
src/pkg/runtime/symtab.c
のdofunc
関数は、ランタイムがシンボルテーブルを解析する際に、関数の引数サイズとポインタマップサイズの一貫性をチェックします。以前は、sym->value > func[nfunc-1].args/sizeof(uintptr)
(ポインタマップエントリが引数ワード数より多い)場合にエラーとしていました。 この変更では、チェックがsym->value != func[nfunc-1].args/sizeof(uintptr)
(ポインタマップサイズと引数サイズが一致しない)に強化されました。これは、ポインタマップの数が引数ワードの数と厳密に一致する必要があるという、より厳格な要件を課しています。これにより、シンボルテーブルの破損や不整合が早期に検出され、ランタイムの安定性が向上します。
これらの変更は、Goのコンパイラ、リンカ、ランタイム間の連携を強化し、特に低レベルなnosplit
関数のガベージコレクションの正確性を保証するために不可欠です。
コアとなるコードの変更箇所
このコミットでは、以下の3つのファイルが変更されています。
src/cmd/gc/pgen.c
: コンパイラのポインタマップ生成ロジック。src/cmd/ld/lib.c
: リンカのアセンブリシンボル生成ロジック。src/pkg/runtime/symtab.c
: ランタイムのシンボルテーブル解析ロジック。
コアとなるコードの解説
src/cmd/gc/pgen.c
--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -296,21 +296,15 @@ pointermap(Node *fn)
walktype(inargtype, bv);
if(outargtype != nil)
walktype(outargtype, bv);
- if(bvisempty(bv)) {
- prog = gins(ANPTRS, N, N);
+ prog = gins(ANPTRS, N, N);
+ prog->to.type = D_CONST;
+ prog->to.offset = bv->n;
+ for(i = 0; i < bv->n; i += 32) {
+ prog = gins(APTRS, N, N);
+ prog->from.type = D_CONST;
+ prog->from.offset = i / 32;
prog->to.type = D_CONST;
- prog->to.offset = 0;
- } else {
- prog = gins(ANPTRS, N, N);
- prog->to.type = D_CONST;
- prog->to.offset = bv->n;
- for(i = 0; i < bv->n; i += 32) {
- prog = gins(APTRS, N, N);
- prog->from.type = D_CONST;
- prog->from.offset = i / 32;
- prog->to.type = D_CONST;
- prog->to.offset = bv->b[i / 32];
- }
+ prog->to.offset = bv->b[i / 32];
}
free(bv);
}
この変更は、ポインタマップが空であるかどうかにかかわらず、常にANPTRS
(ポインタの総数を指定)とAPTRS
(ポインタマップのビット列を指定)命令を生成するように修正しています。以前は、ポインタマップが空の場合にこれらの命令の生成をスキップしていましたが、この修正により、リンカがnosplit
関数を処理する際に、ポインタマップのメタデータが常に利用可能になります。これにより、リンカが生成するnosplit
関数であっても、ランタイムがポインタ情報を正確に取得できるようになります。
src/cmd/ld/lib.c
--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -1914,7 +1914,11 @@ genasmsym(void (*put)(Sym*, char*, int, vlong, vlong, int, Sym*))
/* frame, locals, args, auto, param and pointers after */
put(nil, ".frame", 'm', (uint32)s->text->to.offset+PtrSize, 0, 0, 0);
put(nil, ".locals", 'm', s->locals, 0, 0, 0);
- if(s->text->textflag & NOSPLIT)
+ if((s->text->textflag & NOSPLIT) && (s->args == 0) && (s->nptrs < 0))
+ // This might be a vararg function and have no
+ // predetermined argument size. This check is
+ // approximate and will also match 0 argument
+ // nosplit functions compiled by 6c.
put(nil, ".args", 'm', ArgsSizeUnknown, 0, 0, 0);
else
put(nil, ".args", 'm', s->args, 0, 0, 0);
この変更は、リンカがnosplit
関数の引数サイズを決定するロジックを修正しています。以前は、NOSPLIT
フラグが設定されている関数に対しては無条件にArgsSizeUnknown
を設定していました。
修正後は、NOSPLIT
フラグが設定されており、かつコンパイラが認識する引数サイズが0 (s->args == 0
) であり、かつポインタの数が負 (s->nptrs < 0
) の場合にのみArgsSizeUnknown
を設定します。この条件は、リンカが可変引数関数や、コンパイラが引数サイズを特定できないnosplit
関数を処理する際に、ポインタマップのサイズを引数サイズの代替として利用できるようにするためのものです。これにより、リンカが生成するnosplit
関数であっても、正確な引数情報がランタイムに渡されるようになります。
src/pkg/runtime/symtab.c
--- a/src/pkg/runtime/symtab.c
+++ b/src/pkg/runtime/symtab.c
@@ -234,8 +234,8 @@ dofunc(Sym *sym)
func[nfunc-1].args = sym->value;
else if(runtime·strcmp(sym->name, (byte*)".nptrs") == 0) {
// TODO(cshapiro): use a dense representation for gc information
- if(sym->value > func[nfunc-1].args/sizeof(uintptr)) {
- runtime·printf("more pointer map entries than argument words\n");
+ if(sym->value != func[nfunc-1].args/sizeof(uintptr)) {
+ runtime·printf("pointer map size and argument size disagree\n");
runtime·throw("mangled symbol table");
}
cap = ROUND(sym->value, 32) / 32;
この変更は、ランタイムがシンボルテーブルを解析する際に行う、ポインタマップサイズと引数サイズの一貫性チェックを強化しています。以前は、ポインタマップエントリの数が引数ワード数より多い場合にエラーとしていましたが、修正後は、ポインタマップサイズと引数サイズが一致しない場合にエラーをスローするように変更されました。これは、ポインタマップの数が引数ワードの数と厳密に一致する必要があるという、より厳格な制約を課すものです。これにより、シンボルテーブルの不整合がより確実に検出され、ランタイムの堅牢性が向上します。
関連リンク
参考にした情報源リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事
- Goのコンパイラ、リンカ、ランタイムのソースコード(特に
src/cmd/gc
、src/cmd/ld
、src/pkg/runtime
ディレクトリ) - Goの
nosplit
関数に関する技術記事や議論 - Goのポインタマップに関する技術記事や議論
- Goのシンボルテーブルに関する情報