[インデックス 14675] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)に実験的な機能としてGOEXPERIMENT=zerostack
を追加するものです。この機能は、関数エントリ時にスタックフレームをゼロクリアすることで、ガベージコレクション(GC)における誤検知(false positives)を削減することを目的としています。これはCPU時間を消費する高コストな操作ですが、GCの誤検知に悩まされている場合に、その解消のためにCPU時間を犠牲にする選択肢を提供します。ただし、この機能は他の関数呼び出しによって引き起こされる誤検知のみを排除し、現在の関数呼び出し内に格納されているデッドな一時変数によって引き起こされる誤検知には効果がありません。
コミット
commit b7603cfc2cf8ffa261aca63dd59fb1e7d58180ff
Author: Russ Cox <rsc@golang.org>
Date: Mon Dec 17 14:32:26 2012 -0500
cmd/gc: add GOEXPERIMENT=zerostack to clear stack on function entry
This is expensive but it might be useful in cases where
people are suffering from false positives during garbage
collection and are willing to trade the CPU time for getting
rid of the false positives.
On the other hand it only eliminates false positives caused
by other function calls, not false positives caused by dead
temporaries stored in the current function call.
The 5g/6g/8g changes were pulled out of the history, from
the last time we needed to do this (to work around a goto bug).
The code in go.h, lex.c, pgen.c is new but tiny.
R=ken2
CC=golang-dev
https://golang.org/cl/6938073
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b7603cfc2cf8ffa261aca63dd59fb1e7d58180ff
元コミット内容
cmd/gc
: 関数エントリ時にスタックをクリアする GOEXPERIMENT=zerostack
を追加。
これは高コストな処理ですが、ガベージコレクション中の誤検知に悩まされており、その誤検知を排除するためにCPU時間を犠牲にしても構わない場合に役立つ可能性があります。
一方で、これは他の関数呼び出しによって引き起こされる誤検知のみを排除し、現在の関数呼び出し内に格納されているデッドな一時変数によって引き起こされる誤検知には効果がありません。
5g/6g/8g
の変更は、以前(gotoバグの回避策として)これを行う必要があった時の履歴から取り出されました。go.h
、lex.c
、pgen.c
のコードは新規ですが、ごくわずかです。
変更の背景
Go言語のガベージコレクション(GC)は、プログラムが使用しなくなったメモリを自動的に解放する重要な機能です。しかし、GCが正しく動作するためには、どのメモリがまだ参照されているかを正確に識別する必要があります。ここで問題となるのが「誤検知(false positives)」です。
誤検知とは、実際にはもう使用されていないメモリ領域(特にスタック上の古い値)が、GCによって誤って「まだ参照されている」と判断されてしまう現象を指します。これは、スタック上に以前の関数呼び出しや一時変数によって書き込まれた古いポインタ値が残っており、その値がたまたま有効なヒープ上のオブジェクトを指しているように見える場合に発生します。GCはこれらの古いポインタを「生きている」と判断し、その結果、本来解放されるべきメモリが解放されずに残り、メモリリークのように見える状況や、メモリ使用量の増加を引き起こす可能性があります。
このコミットは、このGCの誤検知問題に対処するための実験的なアプローチを導入します。関数が呼び出された直後に、その関数のスタックフレーム全体をゼロで埋める(ゼロクリアする)ことで、スタック上に残っている可能性のある古いポインタ値を無効化します。これにより、GCがそれらの古い値を有効な参照として誤って認識する可能性を減らし、誤検知の発生を抑制します。
ただし、スタックのゼロクリアは、関数呼び出しごとに余分なCPU時間を消費する高コストな操作です。そのため、この機能はデフォルトでは無効化されており、GOEXPERIMENT=zerostack
という環境変数を設定した場合にのみ有効になります。これは、GCの誤検知が深刻な問題となっている特定のケースにおいて、開発者がパフォーマンスのオーバーヘッドと引き換えにGCの正確性を向上させる選択肢を提供するためのものです。
コミットメッセージにもあるように、このアプローチは「他の関数呼び出しによって引き起こされる誤検知」には効果がありますが、「現在の関数呼び出し内に格納されているデッドな一時変数によって引き起こされる誤検知」には効果がありません。これは、現在の関数内で一時変数が使用され、その後にその変数が不要になっても、そのメモリがゼロクリアされる前にGCが走る可能性があるためです。
また、5g/6g/8g
(Goのコンパイラバックエンド、それぞれARM、x86-64、x86に対応)の変更が「履歴から取り出された」とあるのは、スタックのゼロクリアという技術自体は、過去に別の目的(例えば、セキュリティ上の理由や特定のバグ回避のため)で検討または実装されたことがあったことを示唆しています。
前提知識の解説
Goのガベージコレクション (GC) の基本
GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。これは、以下のステップで動作します。
- マークフェーズ: GCは、プログラムが現在使用しているオブジェクト(ルートセット、例:グローバル変数、実行中の関数のスタック上の変数)から開始し、それらから到達可能なすべてのオブジェクトを「生きている(live)」としてマークします。
- スイープフェーズ: マークされなかったすべてのオブジェクトは「死んでいる(dead)」と判断され、それらが占めていたメモリは再利用のために解放されます。
GoのGCは、プログラムの実行と並行して動作する「コンカレントGC」を採用しており、プログラムの一時停止(ストップ・ザ・ワールド)時間を最小限に抑えるように設計されています。
GCにおける「誤検知 (False Positives)」とは何か、なぜ発生するか
GCにおける誤検知とは、前述の通り、実際にはプログラムから到達不可能で解放されるべきメモリが、GCによって誤って「到達可能」と判断されてしまう現象です。これは主に以下の理由で発生します。
- スタック上の古い値: 関数が呼び出され、その中でローカル変数や一時変数が使用されます。関数が終了しても、そのスタックフレームのメモリはすぐにゼロクリアされるわけではありません。そのため、以前の関数呼び出しや現在の関数の実行中に、たまたまヒープ上の有効なオブジェクトのアドレスと同じ値がスタック上のどこかに残ってしまうことがあります。GCがスタックをスキャンする際、この古い値を有効なポインタとして誤認識し、そのポインタが指す先のオブジェクト(およびそこから到達可能なオブジェクト)を「生きている」とマークしてしまいます。
- 未初期化メモリ: 新しく確保されたメモリ領域が初期化される前にGCが走った場合、そのメモリ領域にたまたま古いポインタ値が残っていると、それが誤検知の原因となることがあります。
誤検知が発生すると、本来解放されるべきメモリが解放されず、メモリ使用量が増加したり、メモリリークのように見えたりする問題が発生します。これは特に、メモリ使用量が厳しく管理される環境や、長期間稼働するサービスにおいて問題となります。
スタックとヒープの違い
- スタック (Stack): 関数呼び出しやローカル変数、関数の引数などを格納するために使用されるメモリ領域です。LIFO(Last-In, First-Out)の構造を持ち、関数が呼び出されるとスタックフレームが積まれ、関数が終了するとそのスタックフレームが破棄されます。メモリの割り当てと解放が非常に高速です。
- ヒープ (Heap): プログラムの実行中に動的にメモリを割り当てるために使用されるメモリ領域です。
new
やmake
などのGoの組み込み関数によって割り当てられるオブジェクト(例:スライス、マップ、チャネル、構造体のインスタンスなど)はヒープに格納されます。ヒープメモリの管理はGCによって行われます。
GCは主にヒープメモリを管理しますが、スタック上のポインタがヒープ上のオブジェクトを指しているため、GCはスタックもスキャンして「生きている」オブジェクトを特定します。
GOEXPERIMENT
環境変数
GOEXPERIMENT
は、Goツールチェインにおける実験的な機能を有効にするための環境変数です。Goの開発チームは、新しい機能や最適化を導入する前に、このメカニズムを通じてコミュニティからのフィードバックを収集することがあります。GOEXPERIMENT
で有効化された機能は、将来的に正式な機能としてGoに組み込まれるか、あるいは破棄される可能性があります。このコミットでは、GOEXPERIMENT=zerostack
を設定することで、スタックゼロクリア機能を有効にできます。
Goコンパイラの構造 (cmd/gc
, 5g
, 6g
, 8g
の役割)
Goコンパイラは、Goのソースコードを機械語に変換する役割を担っています。Goのコンパイラは、複数のステージとコンポーネントから構成されています。
cmd/gc
: これはGoコンパイラの主要なフロントエンドおよび共通バックエンド部分を指します。Goのソースコードの字句解析、構文解析、型チェック、中間表現(IR)の生成、最適化、およびアーキテクチャ非依存のコード生成を担当します。5g
,6g
,8g
: これらはGoコンパイラのバックエンドの一部であり、特定のCPUアーキテクチャ向けのコード生成を担当します。5g
: ARMアーキテクチャ(例:Raspberry Pi、一部のモバイルデバイス)向けのコード生成。6g
: x86-64アーキテクチャ(一般的な64ビットデスクトップ/サーバーCPU)向けのコード生成。8g
: x86アーキテクチャ(一般的な32ビットデスクトップCPU)向けのコード生成。
これらのバックエンドは、cmd/gc
によって生成された中間表現を受け取り、それぞれのアーキテクチャに特化したアセンブリコードを生成します。このコミットでは、スタックゼロクリアの処理がアーキテクチャ固有のアセンブリ命令に依存するため、5g/gsubr.c
、6g/gsubr.c
、8g/gsubr.c
の各ファイルにそれぞれの実装が追加されています。gsubr.c
ファイルは、Goコンパイラのバックエンドにおける共通サブルーチンやヘルパー関数を格納する場所です。
技術的詳細
このコミットにおける技術的な変更は、主に以下の3つの側面から構成されます。
-
GOEXPERIMENT=zerostack
の認識と有効化:src/cmd/gc/go.h
にzerostack_enabled
というグローバル変数が宣言されます。これは、スタックゼロクリア機能が有効になっているかどうかを示すフラグとして機能します。src/cmd/gc/lex.c
は、Goコンパイラの字句解析器の一部であり、GOEXPERIMENT
環境変数を解析するロジックを含んでいます。このコミットでは、exper
配列に{"zerostack", &zerostack_enabled}
というエントリが追加されます。これにより、GOEXPERIMENT=zerostack
が設定されている場合、zerostack_enabled
変数がtrue
(非ゼロ)に設定されます。
-
関数エントリポイントでのスタックゼロクリアの挿入:
src/cmd/gc/pgen.c
は、Goコンパイラのコード生成フェーズの一部です。compile
関数は、個々のGo関数をコンパイルする主要なエントリポイントです。- このコミットでは、
compile
関数内にif(zerostack_enabled) clearstk();
という条件分岐が追加されます。これにより、zerostack_enabled
フラグが有効な場合、各Go関数のコンパイルされたコードの冒頭にclearstk()
関数が呼び出されるように、アセンブリ命令が挿入されます。
-
アーキテクチャ固有のスタックゼロクリア実装 (
clearstk
関数):src/cmd/5g/gsubr.c
(ARM),src/cmd/6g/gsubr.c
(x86-64),src/cmd/8g/gsubr.c
(x86) の各ファイルに、clearstk
関数が追加されます。この関数は、それぞれのアーキテクチャの特性に合わせて、スタックフレームを効率的にゼロクリアするアセンブリ命令を生成します。clearstk
関数は、現在の関数のスタックフレームのサイズ(plast->firstpc->to.offset
で取得される)に基づいて、ゼロクリアするメモリ範囲を決定します。plast->firstpc
は、現在の関数の最初のプログラムカウンタ(命令)を指し、to.offset
はその関数のスタックフレームのサイズ(バイト単位)を表します。- 5g (ARM):
nodreg
,nodconst
,gins
,gmove
,raddr
,patch
,gbranch
などのGoコンパイラ内部のヘルパー関数を使用して、ARMアセンブリ命令を生成します。- 具体的には、スタックポインタ(
SP
)からのオフセットを計算し、ループを使ってMOVW
(ワード移動)命令でスタック上のメモリをゼロで埋めます。C_PBIT
はプリインデックスアドレッシングモードを示し、scond
は条件付き実行フラグに関連します。 MOVW $4(SP), R1
: スタックポインタから4バイトオフセットしたアドレスをR1にロード(スタックフレームの開始アドレス)。MOVW $n(R1), R2
: スタックフレームの終了アドレスをR2にロード。n
はスタックフレームのサイズ。MOVW $0, R3
: R3レジスタに0をロード。- ループ (
L:
):MOVW.P R3, 0(R1) +4
(R1が指すアドレスにR3の値を書き込み、R1を4バイト進める)、CMP R1, R2
(R1とR2を比較)、BNE L
(R1がR2と等しくなければLへ分岐)。これにより、スタックフレーム全体がゼロで埋められます。
- 6g (x86-64):
- x86-64アーキテクチャでは、
REP STOSQ
(Repeat Store String Quadword)命令がメモリブロックを効率的にゼロクリアするために使用されます。 nodreg(&sp, types[tptr], D_SP);
: スタックポインタ(RSP
)を表現するノードを作成。nodreg(&di, types[tptr], D_DI);
: 宛先インデックスレジスタ(RDI
)を表現するノードを作成。nodreg(&cx, types[TUINT64], D_CX);
: カウントレジスタ(RCX
)を表現するノードを作成。nodconst(&con, types[TUINT64], (uint32)p1->to.offset / widthptr);
: スタックフレームのサイズをポインタのサイズ(widthptr
)で割って、クワッドワード(8バイト)単位のカウントを計算し、RCX
にロード。gins(ACLD, N, N);
: 方向フラグをクリア(STOS
命令が前方へ進むように設定)。gins(AMOVQ, &sp, &di);
: スタックポインタの値をRDI
に移動(ゼロクリアの開始アドレス)。gins(AMOVQ, &con, &cx);
: 計算したカウントをRCX
に移動。nodconst(&con, types[TUINT64], 0); nodreg(&ax, types[TUINT64], D_AX); gins(AMOVQ, &con, &ax);
:RAX
レジスタに0をロード(STOSQ
が書き込む値)。gins(AREP, N, N); gins(ASTOSQ, N, N);
:REP STOSQ
命令を生成。RCX
が0になるまでRAX
の値をRDI
が指すメモリに書き込み、RDI
を8バイトずつ進めます。
- x86-64アーキテクチャでは、
- 8g (x86):
- x86アーキテクチャでは、
REP STOSL
(Repeat Store String Longword)命令が使用されます。基本的なロジックは6gと同じですが、32ビットレジスタ(EAX
,ECX
,EDI
)と32ビット操作(MOVL
,STOSL
)を使用します。widthptr
も4バイトになります。
- x86アーキテクチャでは、
これらの変更により、GOEXPERIMENT=zerostack
が有効な場合、Goコンパイラは生成される各関数のアセンブリコードの冒頭に、その関数のスタックフレームをゼロクリアする命令を挿入するようになります。
コアとなるコードの変更箇所
src/cmd/gc/go.h
@@ -937,6 +937,7 @@ EXTERN int funcdepth;
EXTERN int typecheckok;
EXTERN int compiling_runtime;
EXTERN int compiling_wrappers;
+EXTERN int zerostack_enabled;
EXTERN int nointerface;
EXTERN int fieldtrack_enabled;
@@ -1092,6 +1093,7 @@ void genlist(NodeList *l);
Node* sysfunc(char *name);
void tempname(Node *n, Type *t);
Node* temp(Type*);
+void clearstk(void);
/*
* init.c
src/cmd/gc/lex.c
@@ -41,6 +41,7 @@ static struct {
} exper[] = {
// {"rune32", &rune32},
{"fieldtrack", &fieldtrack_enabled},
+\t{"zerostack", &zerostack_enabled},\n\t{nil, nil},
};
src/cmd/gc/pgen.c
@@ -141,6 +141,9 @@ compile(Node *fn)
if(0)
frame(0);
+\tif(zerostack_enabled)
+\t\tclearstk();
+\
ret:
lineno = lno;
}
src/cmd/5g/gsubr.c
(ARM)
@@ -174,6 +174,64 @@ newplist(void)
return pl;
}
+void
+clearstk(void)
+{
+ Plist *pl;
+ Prog *p, *p1, *p2, *p3;
+ Node dst, end, zero, con;
+
+ if(plast->firstpc->to.offset <= 0)
+ return;
+
+ // reestablish context for inserting code
+ // at beginning of function.
+ pl = plast;
+ p1 = pl->firstpc;
+ p2 = p1->link;
+ pc = mal(sizeof(*pc));
+ clearp(pc);
+ p1->link = pc;
+
+ // zero stack frame
+
+ // MOVW $4(SP), R1
+ nodreg(&dst, types[tptr], 1);
+ p = gins(AMOVW, N, &dst);
+ p->from.type = D_CONST;
+ p->from.reg = REGSP;
+ p->from.offset = 4;
+
+ // MOVW $n(R1), R2
+ nodreg(&end, types[tptr], 2);
+ p = gins(AMOVW, N, &end);
+ p->from.type = D_CONST;
+ p->from.reg = 1;
+ p->from.offset = p1->to.offset;
+
+ // MOVW $0, R3
+ nodreg(&zero, types[TUINT32], 3);
+ nodconst(&con, types[TUINT32], 0);
+ gmove(&con, &zero);
+
+ // L:
+ // MOVW.P R3, 0(R1) +4
+ // CMP R1, R2
+ // BNE L
+ p = gins(AMOVW, &zero, &dst);
+ p->to.type = D_OREG;
+ p->to.offset = 4;
+ p->scond |= C_PBIT;
+ p3 = p;
+ p = gins(ACMP, &dst, N);
+ raddr(&end, p);
+ patch(gbranch(ABNE, T, 0), p3);
+
+ // continue with original code.
+ gins(ANOP, N, N)->link = p2;
+ pc = P;
+}
+
void
gused(Node *n)
{
src/cmd/6g/gsubr.c
(x86-64)
@@ -172,6 +172,44 @@ newplist(void)
return pl;
}
+void
+clearstk(void)
+{
+ Plist *pl;
+ Prog *p1, *p2;
+ Node sp, di, cx, con, ax;
+
+ if((uint32)plast->firstpc->to.offset <= 0)
+ return;
+
+ // reestablish context for inserting code
+ // at beginning of function.
+ pl = plast;
+ p1 = pl->firstpc;
+ p2 = p1->link;
+ pc = mal(sizeof(*pc));
+ clearp(pc);
+ p1->link = pc;
+
+ // zero stack frame
+ nodreg(&sp, types[tptr], D_SP);
+ nodreg(&di, types[tptr], D_DI);
+ nodreg(&cx, types[TUINT64], D_CX);
+ nodconst(&con, types[TUINT64], (uint32)p1->to.offset / widthptr);
+ gins(ACLD, N, N);
+ gins(AMOVQ, &sp, &di);
+ gins(AMOVQ, &con, &cx);
+ nodconst(&con, types[TUINT64], 0);
+ nodreg(&ax, types[TUINT64], D_AX);
+ gins(AMOVQ, &con, &ax);
+ gins(AREP, N, N);
+ gins(ASTOSQ, N, N);
+
+ // continue with original code.
+ gins(ANOP, N, N)->link = p2;
+ pc = P;
+}
+
void
gused(Node *n)
{
src/cmd/8g/gsubr.c
(x86)
@@ -173,6 +173,44 @@ newplist(void)
return pl;
}
+void
+clearstk(void)
+{
+ Plist *pl;
+ Prog *p1, *p2;
+ Node sp, di, cx, con, ax;
+
+ if(plast->firstpc->to.offset <= 0)
+ return;
+
+ // reestablish context for inserting code
+ // at beginning of function.
+ pl = plast;
+ p1 = pl->firstpc;
+ p2 = p1->link;
+ pc = mal(sizeof(*pc));
+ clearp(pc);
+ p1->link = pc;
+
+ // zero stack frame
+ nodreg(&sp, types[tptr], D_SP);
+ nodreg(&di, types[tptr], D_DI);
+ nodreg(&cx, types[TUINT32], D_CX);
+ nodconst(&con, types[TUINT32], p1->to.offset / widthptr);
+ gins(ACLD, N, N);
+ gins(AMOVL, &sp, &di);
+ gins(AMOVL, &con, &cx);
+ nodconst(&con, types[TUINT32], 0);
+ nodreg(&ax, types[TUINT32], D_AX);
+ gins(AMOVL, &con, &ax);
+ gins(AREP, N, N);
+ gins(ASTOSL, N, N);
+
+ // continue with original code.
+ gins(ANOP, N, N)->link = p2;
+ pc = P;
+}
+
void
gused(Node *n)
{
コアとなるコードの解説
src/cmd/gc/go.h
の変更
EXTERN int zerostack_enabled;
:zerostack_enabled
という名前の整数型グローバル変数を宣言しています。EXTERN
キーワードは、この変数が他のファイルで定義されていることを示します。この変数は、GOEXPERIMENT=zerostack
が有効かどうかをコンパイラ全体で共有するためのフラグとして機能します。void clearstk(void);
:clearstk
関数のプロトタイプ宣言です。この関数は、スタックをゼロクリアする処理をカプセル化しており、各アーキテクチャ固有のgsubr.c
ファイルで実装されます。
src/cmd/gc/lex.c
の変更
exper[]
配列に{"zerostack", &zerostack_enabled}
というエントリが追加されています。lex.c
はコンパイラの字句解析部分であり、GOEXPERIMENT
環境変数の値を読み取り、対応する実験的機能を有効にする役割を担っています。この追加により、GOEXPERIMENT=zerostack
が設定された場合、zerostack_enabled
変数が非ゼロの値に設定され、スタックゼロクリア機能が有効になります。
src/cmd/gc/pgen.c
の変更
compile(Node *fn)
関数内にif(zerostack_enabled) clearstk();
という行が追加されています。compile
関数は、Goの各関数をコンパイルする際に呼び出される主要な関数です。この条件分岐により、zerostack_enabled
フラグが有効な場合にのみ、clearstk()
関数が呼び出されるアセンブリ命令が、コンパイルされる関数のエントリポイント(関数の開始部分)に挿入されます。これにより、関数が実行される直前にスタックフレームがゼロクリアされるようになります。
src/cmd/5g/gsubr.c
(ARM) の clearstk
関数
この関数はARMアーキテクチャ向けにスタックをゼロクリアするアセンブリ命令を生成します。
if(plast->firstpc->to.offset <= 0) return;
: スタックフレームのサイズが0以下の場合、ゼロクリアは不要なので早期リターンします。plast->firstpc->to.offset
は現在の関数のスタックフレームのサイズ(バイト単位)を示します。// reestablish context for inserting code ...
: コード挿入のためのコンテキストを再確立します。p1
は現在の関数の最初の命令、p2
はその次の命令を指します。新しい命令はp1
とp2
の間に挿入されます。// zero stack frame
: ここからスタックゼロクリアのアセンブリ命令生成が始まります。nodreg(&dst, types[tptr], 1);
: レジスタR1を宛先として設定します。types[tptr]
はポインタ型を示します。p = gins(AMOVW, N, &dst); p->from.type = D_CONST; p->from.reg = REGSP; p->from.offset = 4;
:MOVW $4(SP), R1
という命令を生成します。これは、スタックポインタ(SP
)から4バイトオフセットしたアドレス(通常、Goの関数スタックフレームの開始点)をR1レジスタにロードします。nodreg(&end, types[tptr], 2); p = gins(AMOVW, N, &end); p->from.type = D_CONST; p->from.reg = 1; p->from.offset = p1->to.offset;
:MOVW $n(R1), R2
という命令を生成します。これは、R1が指すアドレス(スタックフレームの開始点)にスタックフレームのサイズp1->to.offset
を加えたアドレス(スタックフレームの終了点)をR2レジスタにロードします。nodreg(&zero, types[TUINT32], 3); nodconst(&con, types[TUINT32], 0); gmove(&con, &zero);
: レジスタR3に0をロードします。この0がスタックに書き込まれる値になります。// L: MOVW.P R3, 0(R1) +4 ... BNE L
: ここでループを生成し、スタックフレームをゼロクリアします。p = gins(AMOVW, &zero, &dst); p->to.type = D_OREG; p->to.offset = 4; p->scond |= C_PBIT;
:MOVW.P R3, 0(R1) +4
という命令を生成します。これは、R1が指すアドレスにR3(0)の値を書き込み、その後R1を4バイト(ワードサイズ)進めます。C_PBIT
はプリインデックスアドレッシングモードを示し、書き込み後にアドレスレジスタを更新します。p3 = p; p = gins(ACMP, &dst, N); raddr(&end, p);
:CMP R1, R2
という命令を生成します。R1(現在の書き込み位置)とR2(スタックフレームの終了位置)を比較します。patch(gbranch(ABNE, T, 0), p3);
:BNE L
という条件分岐命令を生成します。R1とR2が等しくない場合(まだゼロクリアが完了していない場合)、ループの先頭(p3
が指す命令)に戻ります。
gins(ANOP, N, N)->link = p2; pc = P;
: ゼロクリアコードの後に、元の関数のコード(p2
が指す命令)にジャンプするように命令をリンクし、コンテキストをリセットします。
src/cmd/6g/gsubr.c
(x86-64) の clearstk
関数
この関数はx86-64アーキテクチャ向けにスタックをゼロクリアするアセンブリ命令を生成します。x86-64では、REP STOSQ
命令がメモリブロックの効率的なゼロクリアによく使われます。
if((uint32)plast->firstpc->to.offset <= 0) return;
: ARM版と同様に、スタックフレームサイズが0以下の場合はリターン。// reestablish context ...
: コード挿入のためのコンテキストを再確立。// zero stack frame
:nodreg(&sp, types[tptr], D_SP); nodreg(&di, types[tptr], D_DI);
: スタックポインタ(RSP
)と宛先インデックスレジスタ(RDI
)のノードを作成。nodreg(&cx, types[TUINT64], D_CX);
: カウントレジスタ(RCX
)のノードを作成。nodconst(&con, types[TUINT64], (uint32)p1->to.offset / widthptr);
: スタックフレームのサイズをポインタのサイズ(widthptr
、x86-64では8バイト)で割って、8バイト単位の繰り返し回数を計算し、con
に格納。gins(ACLD, N, N);
:CLD
命令を生成。これは方向フラグをクリアし、STOS
命令がメモリを前方(アドレスが増加する方向)に書き込むように設定します。gins(AMOVQ, &sp, &di);
:MOVQ RSP, RDI
命令を生成。スタックポインタの現在値(スタックフレームの開始アドレス)をRDI
に移動します。RDI
はSTOSQ
命令の宛先アドレスとして使用されます。gins(AMOVQ, &con, &cx);
:MOVQ con, RCX
命令を生成。計算した繰り返し回数をRCX
に移動します。RCX
はREP
プレフィックスの繰り返しカウンタとして使用されます。nodconst(&con, types[TUINT64], 0); nodreg(&ax, types[TUINT64], D_AX); gins(AMOVQ, &con, &ax);
:MOVQ $0, RAX
命令を生成。RAX
レジスタに0をロードします。RAX
はSTOSQ
命令がメモリに書き込む値を提供します。gins(AREP, N, N); gins(ASTOSQ, N, N);
:REP STOSQ
命令を生成。この命令は、RCX
が0になるまでRAX
の内容をRDI
が指すメモリ位置に書き込み、RDI
を8バイトずつインクリメントします。これにより、スタックフレーム全体が効率的にゼロで埋められます。
gins(ANOP, N, N)->link = p2; pc = P;
: ゼロクリアコードの後に、元の関数のコードにジャンプするように命令をリンクし、コンテキストをリセットします。
src/cmd/8g/gsubr.c
(x86) の clearstk
関数
この関数はx86アーキテクチャ向けにスタックをゼロクリアするアセンブリ命令を生成します。基本的なロジックはx86-64版と非常に似ていますが、32ビットレジスタと命令を使用します。
if(plast->firstpc->to.offset <= 0) return;
: 同上。// reestablish context ...
: 同上。// zero stack frame
:nodreg(&sp, types[tptr], D_SP); nodreg(&di, types[tptr], D_DI);
: スタックポインタ(ESP
)と宛先インデックスレジスタ(EDI
)のノードを作成。nodreg(&cx, types[TUINT32], D_CX);
: カウントレジスタ(ECX
)のノードを作成。nodconst(&con, types[TUINT32], p1->to.offset / widthptr);
: スタックフレームのサイズをポインタのサイズ(widthptr
、x86では4バイト)で割って、4バイト単位の繰り返し回数を計算し、con
に格納。gins(ACLD, N, N);
:CLD
命令を生成。gins(AMOVL, &sp, &di);
:MOVL ESP, EDI
命令を生成。スタックポインタの現在値(スタックフレームの開始アドレス)をEDI
に移動します。gins(AMOVL, &con, &cx);
:MOVL con, ECX
命令を生成。計算した繰り返し回数をECX
に移動します。nodconst(&con, types[TUINT32], 0); nodreg(&ax, types[TUINT32], D_AX); gins(AMOVL, &con, &ax);
:MOVL $0, EAX
命令を生成。EAX
レジスタに0をロードします。gins(AREP, N, N); gins(ASTOSL, N, N);
:REP STOSL
命令を生成。この命令は、ECX
が0になるまでEAX
の内容をEDI
が指すメモリ位置に書き込み、EDI
を4バイトずつインクリメントします。
gins(ANOP, N, N)->link = p2; pc = P;
: 同上。
これらのアーキテクチャ固有の実装により、Goコンパイラは、GOEXPERIMENT=zerostack
が有効な場合に、各関数の開始時にそのスタックフレームを効率的にゼロクリアする機械語コードを生成します。
関連リンク
- Go言語のガベージコレクションに関する公式ドキュメントやブログ記事:
- Go's Garbage Collector: A Comprehensive Guide (Go 1.5のGCに関する詳細な解説)
- Go's runtime package documentation (GC関連の関数も含む)
- Goのコンパイラに関する情報:
- Go Compiler Design (Goコンパイラの設計に関する古い記事だが、基本的な構造は参考になる)
- スタックゼロクリアとGCの誤検知に関する議論:
- GoのIssueトラッカーやメーリングリストで「false positives garbage collection stack」や「stack zeroing」といったキーワードで検索すると、関連する議論が見つかる可能性があります。
参考にした情報源リンク
- コミットメッセージと変更されたソースコード
- Go言語の公式ドキュメント
- Go言語のランタイムおよびコンパイラのソースコード
- x86/x86-64アセンブリ命令セットリファレンス (特に
REP STOS
命令について) - ARMアセンブリ命令セットリファレンス
- ガベージコレクションに関する一般的な知識