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

[インデックス 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=0nosplit関数の特徴と誤解していたためです。

この問題は、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(自動的に計算されたスタックオフセット)がゼロの場合に実行されます。

  1. p = appendp(p);: 新しい命令を現在の命令リストの末尾に追加し、その命令へのポインタpを更新します。
  2. p->as = ANOP;: 追加された命令のオペレーションコードをANOP(No-Operation)に設定します。
  3. p->spadj = -PtrSize;: 最初のANOP命令に、ポインタサイズの負の値のスタック調整量を与えます。
  4. p = appendp(p);: 再び新しい命令を追加します。
  5. p->as = ANOP;: 2番目の命令もANOPに設定します。
  6. 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と誤認してエラーを出すことなく、正しくリンクし、実行できることを確認します。

関連リンク

参考にした情報源リンク