[インデックス 14491] ファイルの概要
このコミットは、Go言語のリンカ(cmd/6l
および cmd/8l
)におけるスタックオーバーフローチェッカーの挙動を修正するものです。具体的には、スタック調整量がゼロバイトの関数がnosplit
関数として誤って扱われ、リンカが不正確なエラーを報告する問題を解決します。また、8l
リンカでもスタックチェッカーを有効にします。
コミット
commit 2e73453acabd5827383ae97cdcafff814ce09a64
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Nov 26 21:51:48 2012 +0100
cmd/6l, cmd/8l: emit no-ops to separate zero-stack funcs from nosplits.
The stack overflow checker in the linker uses the spadj field
to determine whether stack space will be large enough or not.
When spadj=0, the checker treats the function as a nosplit
and emits an error although the program is correct.
Also enable the stack checker in 8l.
Fixes #4316.
R=rsc, golang-dev
CC=golang-dev
https://golang.org/cl/6855088
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2e73453acabd5827383ae97cdcafff814ce09a64
元コミット内容
cmd/6l
および cmd/8l
リンカにおいて、スタック調整量がゼロの関数をnosplit
関数と区別するために、no-op命令(何もしない命令)を発行するように変更します。これにより、リンカのスタックオーバーフローチェッカーが、スタック調整量がゼロの関数を誤ってnosplit
関数として扱い、誤ったエラーを報告する問題を修正します。さらに、8l
リンカでもスタックチェッカーを有効にします。
変更の背景
Go言語のランタイムは、ゴルーチン(goroutine)のスタックを動的に管理します。関数が呼び出される際、その関数が必要とするスタック空間が現在のスタックに十分にあるかをチェックする「スタックオーバーフローチェッカー」が動作します。もしスタックが不足している場合、ランタイムはスタックを拡張(スタック分割)し、既存のスタック内容を新しい、より大きなスタックセグメントにコピーします。
このコミットが修正する問題は、Goリンカのスタックオーバーフローチェッカーが、関数のスタック調整量(spadj
フィールド)に基づいてスタック空間の十分さを判断する際に発生していました。特定の関数、特にスタックを全く使用しない(スタック調整量がゼロバイト、spadj=0
)関数において、チェッカーがその関数を//go:nosplit
ディレクティブでマークされた関数(スタックチェックを行わない関数)として誤認し、実際には問題がないにもかかわらずエラーを報告していました。これは、リンカがspadj=0
をnosplit
関数の特徴と誤解していたためです。
この問題は、Go Issue #4316として報告されており、このコミットはその修正を目的としています。また、8l
リンカ(x86アーキテクチャ向け)ではスタックチェッカーが有効になっていなかったため、このコミットで有効化されています。
前提知識の解説
- Goのスタック管理とスタック分割 (Stack Splitting): Goのゴルーチンは、比較的小さなスタック(通常は数KB)で開始し、必要に応じて動的に拡張されます。関数呼び出しのプロローグ(関数の冒頭部分)には、スタックオーバーフローチェックのコードが含まれており、スタックが不足している場合はランタイムに制御を移し、より大きなスタックを割り当てて既存のスタック内容をコピーします。このプロセスをスタック分割と呼びます。
spadj
(Stack Pointer Adjustment):spadj
は、関数がスタックポインタをどれだけ調整するか、つまり関数がスタック上でどれだけの空間を必要とするかを示す値です。これは、ローカル変数や引数、呼び出し規約によって使用されるスタック空間の合計を表します。リンカのスタックチェッカーは、このspadj
の値を見て、スタックが十分であるかを判断します。//go:nosplit
ディレクティブ: これはGoコンパイラに対するディレクティブで、特定の関数に対して標準のスタックオーバーフローチェックと動的なスタック拡張(スタック分割)を省略するよう指示します。nosplit
は、ゴルーチンのプリエンプション(横取り)が安全でない低レベルのランタイムコードや、スタックチェックのオーバーヘッドが許容できないパフォーマンスが重要な関数で主に使用されます。しかし、nosplit
関数がスタック空間を使い果たすと、スタック拡張が行われずにプログラムがクラッシュする(セグメンテーション違反など)危険性があるため、慎重に使用する必要があります。- リンカの役割: Goのリンカ(
cmd/6l
はAMD64向け、cmd/8l
はx86向け)は、コンパイルされたコードとデータを結合して実行可能ファイルを生成します。この過程で、スタック管理メカニズムが最終的なバイナリにどのように組み込まれるかを決定し、nosplit
とマークされた関数が実際にスタックチェックプロローグをバイパスするようにします。 ANOP
(Assembly No-Operation): アセンブリ言語におけるNo-Operation(何もしない)命令です。CPUサイクルを消費しますが、レジスタやメモリの状態を変更しません。このコミットでは、リンカがスタックチェックの境界を正しく認識させるために、特定の状況でこの命令を挿入します。
技術的詳細
リンカのスタックオーバーフローチェッカーは、各関数のスタックフレームサイズを計算し、その情報(spadj
)を利用してスタックが十分かを判断します。問題は、スタックを全く使用しない関数(spadj=0
)の場合に発生しました。リンカは、spadj=0
の関数をnosplit
関数(スタックチェックが不要な関数)と誤認し、本来は正しいプログラムであるにもかかわらず、スタックオーバーフローのエラーを報告していました。これは、nosplit
関数もまたスタック調整量がゼロである場合があるため、リンカが両者を区別できなかったためです。
このコミットの解決策は、スタック調整量がゼロバイトの関数に対して、リンカがダミーの非ゼロスタック調整を伴うno-op命令(ANOP
)を挿入することです。具体的には、-PtrSize
(ポインタサイズの負の値)と+PtrSize
(ポインタサイズの正の値)のspadj
を持つ2つのANOP
命令を連続して挿入します。
// zero-byte stack adjustment.
// Insert a fake non-zero adjustment so that stkcheck can
// recognize the end of the stack-splitting prolog.
p = appendp(p);
p->as = ANOP;
p->spadj = -PtrSize;
p = appendp(p);
p->as = ANOP;
p->spadj = PtrSize;
このシーケンスは、全体としてはスタックポインタに影響を与えません(-PtrSize + PtrSize = 0
)が、リンカのスタックチェッカーにとっては、スタック分割プロローグの終わりを認識するための「非ゼロの調整」として機能します。これにより、spadj=0
の関数がnosplit
関数と誤認されることを防ぎ、リンカが正しいスタックチェックを実行できるようになります。
また、src/cmd/8l/obj.c
において、dostkcheck()
関数がmain
関数内で呼び出されるように変更され、8l
リンカでもスタックチェッカーが有効になりました。
コアとなるコードの変更箇所
このコミットでは、以下のファイルが変更されています。
src/cmd/6l/pass.c
: AMD64リンカのスタックオフセット処理 (dostkoff
) に、ゼロバイトスタック調整のno-op挿入ロジックが追加されました。src/cmd/8l/obj.c
: x86リンカのメイン関数に、スタックチェッカーを有効にするdostkcheck()
の呼び出しが追加されました。src/cmd/8l/pass.c
: x86リンカのスタックオフセット処理 (dostkoff
) に、ゼロバイトスタック調整のno-op挿入ロジックが追加されました。test/fixedbugs/issue4316.go
: この問題(Issue 4316)を再現し、修正が正しく機能することを確認するための新しいテストファイルが追加されました。このテストは、再帰的な関数呼び出しや、スタックをほとんど使用しない関数を含むコードパスを検証します。
コアとなるコードの解説
src/cmd/6l/pass.c
および src/cmd/8l/pass.c
の変更
これらのファイルは、それぞれAMD64およびx86アーキテクチャ向けのリンカのパス処理を担当しています。dostkoff
関数は、関数のスタックフレームオフセットを計算し、スタックポインタの調整を行います。
変更点として、既存のスタック調整ロジックに以下のelse
ブロックが追加されました。
} else {
// zero-byte stack adjustment.
// Insert a fake non-zero adjustment so that stkcheck can
// recognize the end of the stack-splitting prolog.
p = appendp(p);
p->as = ANOP;
p->spadj = -PtrSize;
p = appendp(p);
p->as = ANOP;
p->spadj = PtrSize;
}
このコードは、autoffset
(自動的に計算されたスタックオフセット)がゼロの場合に実行されます。
p = appendp(p);
: 新しい命令を現在の命令リストの末尾に追加し、その命令へのポインタp
を更新します。p->as = ANOP;
: 追加された命令のオペレーションコードをANOP
(No-Operation)に設定します。p->spadj = -PtrSize;
: 最初のANOP
命令に、ポインタサイズの負の値のスタック調整量を与えます。p = appendp(p);
: 再び新しい命令を追加します。p->as = ANOP;
: 2番目の命令もANOP
に設定します。p->spadj = PtrSize;
: 2番目のANOP
命令に、ポインタサイズの正の値のスタック調整量を与えます。
この2つのANOP
命令の組み合わせにより、スタックポインタの最終的な調整量はゼロのままですが、リンカのスタックチェッカーは-PtrSize
と+PtrSize
という非ゼロの調整を検出し、スタック分割プロローグの終わりを正しく認識できるようになります。これにより、spadj=0
の関数がnosplit
関数と誤認される問題が解消されます。
src/cmd/8l/obj.c
の変更
このファイルは、x86リンカのメイン処理を担当しています。
dostkoff();
dostkcheck(); // <-- この行が追加された
if(debug['p'])
dostkcheck()
関数は、スタックオーバーフローチェッカーを実際に実行する関数です。以前は8l
リンカではこの関数が呼び出されていなかったため、スタックチェックが機能していませんでした。このコミットにより、dostkcheck()
が明示的に呼び出されるようになり、8l
リンカでもスタックオーバーフローチェックが有効になります。
test/fixedbugs/issue4316.go
の追加
このテストファイルは、spadj=0
の関数がリンカによって正しく処理されることを検証します。
makePeano
関数は再帰的にPeano
型のポインタを作成し、countPeano
関数は再帰的にPeano
リストの要素をカウントします。これらの関数は、スタックをほとんど消費しないか、スタック調整量がゼロになる可能性のあるコードパスを含んでいます。
また、p()
関数は括弧のバランスをチェックする再帰的なパーサーの例で、これもスタック消費が少ない可能性があります。
これらのテストケースは、リンカがこれらの関数をnosplit
と誤認してエラーを出すことなく、正しくリンクし、実行できることを確認します。
関連リンク
- Go Issue #4316: https://golang.org/issue/4316
- Go CL 6855088: https://golang.org/cl/6855088
参考にした情報源リンク
- Go言語のスタック管理と
nosplit
ディレクティブに関する情報: - Go Issue 4316に関する情報:
- Google検索結果:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEfDDkC1SyqaIo4qPVDeJqZKer_xWcx91r68GoPybhozFjN3jhTte0lAqDrpB17N7L4wnlvC15cmFI9eh_pgk6b1KXp5hfCYG6tdhNni3EE1m80WFVF0zdnrmHnijB_0wcvaw-UvKr-mrAuc4kQOVT5BWT69FpVIeBSoebXcA==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFa77Hua_P9ilAl5IDNSHsx-MPIFqBbWEwcD55IG_qraNqRAu3PJQZkMZ3ftRml91uhdDFLTJwkutKErZ51LuSHdhK8xIeRc2xs6NveYEFN50cbqfo1mV57dYs94puIeoIy
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFZTHqk5kgZTT6rpQ1zAtmvKN9i5uHCGe7UE1HZTnkIMZIi0V54U3sqZqubkhktnyvPC5sYBrkWqc_WaacI8R3w7gb4pXepmBMQHAIHANSfazIyJayNFnKltTp4i0_tDkHvu7v1g30L2qT0QxJ6kHE=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHGgF67VvyFUvpyKDt4KlvoFxNat5vGzHsA8qnzgV7PjVjhgr2snga5XzX0zm_vIy3vI3RQUNZ655RErKPdRKBgj9FF7Q3iQo8Gh1WaZU2kK7Ve3y4o9U3lcQwneiu8BIc19uTxo-Tvk628czm9q9Ipz8Rod7xTsX4Khbm26hMkPnV1
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF_SCtbHCCxOea93B3TJSLKMBVU07GEqiSTaxz3dzIWySLE1px6l3HOhFcgyhJjNcZWdh5iwxFvC3rrOB8sbet8aLIEfwnsFLOo587hy1qSZhZAl7vNXK6sblHjFBy2HMw003qzU6c0MkHRwAVfIEH_9E3_ZmJ5QUVSRD9bZeA=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQENirGHmAxuDyQysSMjxyAWTb8-FVICOsgJCJJCq6FOmJIaHxpj0MS6G0UhuZU2-xNNIHWS_6Ei2XmDFc38cAbCbpT_nHAz7Ob-X86u7122qOhf2CbgT8XceNwCJAY4PXcSVjnW9ID7S8DYE9PcMqedvFZUGGYF
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGldbbBkQ3xoF6nAT9vRytk_Kma0BTf9g_eGTMjKQB2M2riChOvYDJ58WvGuoqzvHY56GlVgPdpqqYbD0J2X80lq1Ov8kYtGF3kT1yOO-LulQ191YrdAaz6VTv5qzp1l3ycXOI26K7TX_-lWowjjJbUPKJKkmjnttQj-_-iNpSfAg4BysOndX-dzkmE9yFeyWRIs1GZs0J6fLFGTMQr89HP