[インデックス 14721] ファイルの概要
このコミットは、Go言語のリンカであるcmd/6l
(64-bit x86アーキテクチャ向け) と cmd/8l
(32-bit x86アーキテクチャ向け) に、スタックフレームをゼロ初期化するための-Z
フラグを追加するものです。これにより、関数エントリ時にスタックフレームの内容がゼロで埋められ、ガベージコレクションにおける誤検出(false positives)の可能性を低減します。
コミット
commit b9da27bed238b5bc55f0f92ed60dc691bd4691f5
Author: Russ Cox <rsc@golang.org>
Date: Sat Dec 22 11:20:17 2012 -0500
cmd/6l, cmd/8l: add -Z flag to zero stack frame on entry
Replacement for GOEXPERIMENT=zerostack, easier to use.
Does not require a separate toolchain.
R=ken2
CC=golang-dev
https://golang.org/cl/6996051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b9da27bed238b5bc55f0f92ed60dc691bd4691f5
元コミット内容
cmd/6l, cmd/8l: add -Z flag to zero stack frame on entry
このコミットは、Go言語のリンカである6l
(amd64) および 8l
(x86) に、関数エントリ時にスタックフレームをゼロ初期化する-Z
フラグを追加します。これは、以前のGOEXPERIMENT=zerostack
の代替であり、より使いやすく、個別のツールチェインを必要としません。
変更の背景
Go言語のガベージコレクタは、プログラムのメモリ使用量を効率的に管理するために設計されています。しかし、スタック上に残された古い(無効な)ポインタ値が、ガベージコレクタによって有効なポインタとして誤って認識される「誤検出(false positives)」の問題が発生する可能性がありました。これにより、実際には到達不能なオブジェクトが解放されずにメモリリークを引き起こしたり、ガベージコレクションの効率が低下したりすることがありました。
この問題を緩和するための一つのアプローチが、関数が呼び出された際にそのスタックフレームをゼロで初期化することです。これにより、スタック上の古いデータがクリアされ、ガベージコレクタが誤ってそれらをポインタとして解釈するリスクが減少します。
以前はGOEXPERIMENT=zerostack
という環境変数を通じてこの機能が実験的に提供されていましたが、これは特定のツールチェインを必要とするなど、開発者にとって使い勝手が良いものではありませんでした。このコミットは、より標準的なリンカフラグ-Z
としてこの機能を提供することで、利用の敷居を下げ、より多くの開発者がガベージコレクションの誤検出問題のデバッグや回避に利用できるようにすることを目的としています。
前提知識の解説
スタックフレーム
関数が呼び出されるたびに、その関数に必要なローカル変数、引数、戻りアドレスなどを格納するために、メモリの「スタック」領域に一時的なデータ構造が割り当てられます。これを「スタックフレーム」と呼びます。関数が終了すると、そのスタックフレームは解放され、スタックポインタが移動して、その領域は次の関数のために再利用可能になります。
ガベージコレクション (GC)
ガベージコレクションは、プログラムが動的に割り当てたメモリのうち、もはやプログラムから到達できない(参照されていない)領域を自動的に特定し、解放するプロセスです。Go言語のGCは、到達可能なオブジェクトをマークし、マークされなかったオブジェクトをスイープ(解放)する「マーク&スイープ」アルゴリズムを基本としています。GCが正しく機能するためには、プログラム内のすべてのポインタを正確に識別できる必要があります。
ガベージコレクションの誤検出 (False Positives)
スタックフレームが再利用される際、以前の関数の実行によって残されたデータ(古いポインタ値など)がそのまま残っていることがあります。もしこれらの古いデータが、たまたま有効なメモリアドレスを指すように見え、かつそれがGCによってポインタとして解釈されてしまうと、実際には到達不能なオブジェクトが「到達可能」と誤って判断されてしまいます。これがガベージコレクションの誤検出です。誤検出は、メモリリークやGCの非効率性の原因となる可能性があります。
リンカ (Linker)
リンカは、コンパイラによって生成されたオブジェクトファイル(機械語コード)を結合し、実行可能なプログラムを生成するツールです。このプロセスには、外部参照の解決(他のファイルで定義された関数や変数への参照を解決する)や、最終的な実行ファイルのメモリレイアウトの決定などが含まれます。Go言語のツールチェインでは、6l
や8l
といったリンカが特定のアーキテクチャ向けに存在します。
技術的詳細
このコミットは、Go言語のリンカであるsrc/cmd/6l/pass.c
とsrc/cmd/8l/pass.c
のdostkoff
関数に変更を加えます。dostkoff
関数は、スタックフレームのオフセットを処理し、関数プロローグ(関数が開始される際の初期処理)に関連するコードを生成する役割を担っています。
変更の核心は、debug['Z']
フラグが設定されており、かつautoffset
(スタックフレームのサイズ)がゼロでなく、かつNOSPLIT
属性(スタックの拡張を許可しない関数)が設定されていない場合に、スタックフレームをゼロ初期化するアセンブリ命令を挿入することです。
具体的には、以下の操作が行われます(64-bit 6l
の場合):
AMOVQ D_SP, D_DI
: スタックポインタ(SP
)の値をDI
レジスタに移動します。DI
は通常、REP STOSQ
命令の宛先アドレスとして使用されます。これにより、スタックフレームの開始アドレスがDI
に設定されます。AMOVQ D_CONST, D_CX
:autoffset/8
(スタックフレームのサイズを8バイト単位に変換したもの)をCX
レジスタに移動します。CX
はREP
命令のリピートカウントとして使用されます。これにより、ゼロ初期化するクワッドワード(8バイト)の数が設定されます。AMOVQ D_CONST, D_AX
: 定数0
をAX
レジスタに移動します。AX
はSTOSQ
命令のソースデータとして使用されます。これにより、スタックフレームをゼロで埋めるための値が設定されます。AREP
: 次の命令をCX
レジスタの値の回数だけ繰り返すことを指示します。ASTOSQ
:AX
レジスタの内容(ゼロ)をDI
レジスタが指すメモリ位置にストアし、DI
レジスタを8バイト(クワッドワードのサイズ)だけインクリメントします。
これらの命令の組み合わせ(MOV DI, SP; MOV CX, autoffset/8; MOV AX, 0; REP STOSQ
)は、x86/x64アーキテクチャにおいて、指定されたメモリ範囲を効率的にゼロで埋めるための標準的な手法です。
32-bit 8l
の場合も同様のロジックですが、レジスタや命令のサイズが異なります(例: AMOVL
、autoffset/4
、ASTOSL
)。
この変更は、リンカが最終的な実行ファイルを生成する際に、特定の条件を満たす関数のプロローグにこれらの命令を自動的に挿入するようにします。これにより、コンパイル時にスタックゼロ初期化のロジックが組み込まれ、実行時にスタックフレームが自動的にゼロで埋められるようになります。
コアとなるコードの変更箇所
src/cmd/6l/pass.c
(64-bit x86リンカ)
@@ -653,6 +653,34 @@ dostkoff(void)
q1->pcond = p;
}
+ if(debug['Z'] && autoffset && !(cursym->text->from.scale&NOSPLIT)) {
+ // 6l -Z means zero the stack frame on entry.
+ // This slows down function calls but can help avoid
+ // false positives in garbage collection.
+ p = appendp(p);
+ p->as = AMOVQ;
+ p->from.type = D_SP;
+ p->to.type = D_DI;
+
+ p = appendp(p);
+ p->as = AMOVQ;
+ p->from.type = D_CONST;
+ p->from.offset = autoffset/8;
+ p->to.type = D_CX;
+
+ p = appendp(p);
+ p->as = AMOVQ;
+ p->from.type = D_CONST;
+ p->from.offset = 0;
+ p->to.type = D_AX;
+
+ p = appendp(p);
+ p->as = AREP;
+
+ p = appendp(p);
+ p->as = ASTOSQ;
+ }
+
for(; p != P; p = p->link) {
pcsize = p->mode/8;
a = p->from.type;
src/cmd/8l/pass.c
(32-bit x86リンカ)
@@ -593,6 +593,34 @@ dostkoff(void)
}
deltasp = autoffset;
+ if(debug['Z'] && autoffset && !(cursym->text->from.scale&NOSPLIT)) {
+ // 8l -Z means zero the stack frame on entry.
+ // This slows down function calls but can help avoid
+ // false positives in garbage collection.
+ p = appendp(p);
+ p->as = AMOVL;
+ p->from.type = D_SP;
+ p->to.type = D_DI;
+
+ p = appendp(p);
+ p->as = AMOVL;
+ p->from.type = D_CONST;
+ p->from.offset = autoffset/4;
+ p->to.type = D_CX;
+
+ p = appendp(p);
+ p->as = AMOVL;
+ p->from.type = D_CONST;
+ p->from.offset = 0;
+ p->to.type = D_AX;
+
+ p = appendp(p);
+ p->as = AREP;
+
+ p = appendp(p);
+ p->as = ASTOSL;
+ }
+
for(; p != P; p = p->link) {
a = p->from.type;
if(a == D_AUTO)
コアとなるコードの解説
両ファイルにおける変更は、dostkoff
関数内に新しい条件分岐を追加するものです。
if(debug['Z'] && autoffset && !(cursym->text->from.scale&NOSPLIT))
この条件式は、以下の3つの条件がすべて真である場合に、スタックゼロ初期化コードを挿入することを示しています。
debug['Z']
: リンカに-Z
フラグが渡されたかどうかをチェックします。debug
配列は、リンカのデバッグフラグを管理するために使用されます。autoffset
: 現在の関数のスタックフレームサイズがゼロより大きいかどうかをチェックします。スタックフレームがない関数(例: 引数もローカル変数もない関数)では、ゼロ初期化は不要です。!(cursym->text->from.scale&NOSPLIT)
: 現在の関数がNOSPLIT
属性を持たないことをチェックします。NOSPLIT
属性を持つ関数は、スタックの拡張を許可しないため、特殊なケースであり、このゼロ初期化の対象外とされています。
条件が満たされた場合、一連のアセンブリ命令が生成され、現在の命令リストp
に追加されます。これらの命令は、前述の「技術的詳細」セクションで説明したように、REP STOSQ
(64-bit) または REP STOSL
(32-bit) 命令を使用して、スタックフレーム全体をゼロで埋めるためのものです。
コメントにもあるように、この処理は関数呼び出しを遅くする可能性がありますが、ガベージコレクションの誤検出を防ぐのに役立ちます。これは、パフォーマンスとメモリ安全性のトレードオフを示しています。
関連リンク
- Go言語のガベージコレクションに関する公式ドキュメントやブログ記事
- x86/x64アセンブリ言語における
REP STOS
命令に関する資料 - Go言語のリンカの内部構造に関する資料
参考にした情報源リンク
- https://github.com/golang/go/commit/b9da27bed238b5bc55f0f92ed60dc691bd4691f5
- Go言語のソースコード(
src/cmd/6l/pass.c
,src/cmd/8l/pass.c
) - Go言語のIssueトラッカーやメーリングリストでの関連議論(
GOEXPERIMENT=zerostack
など) - 一般的なガベージコレクションの概念に関する情報
- x86/x64アセンブリ命令セットリファレンス