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

[インデックス 15361] ファイルの概要

このコミットは、Go言語のランタイムにおける関数(func構造体)の引数(args)とローカル変数(locals)に関する情報の精度を向上させることを目的としています。以前はこれらの情報が不正確であったり、欠落していたりしましたが、コンパイラとリンカの連携を強化することで、正確なスタックフレーム情報をバイト単位で記録するように変更されました。これにより、デバッグやプロファイリング、ガベージコレクションなど、ランタイムがスタックフレームの正確なレイアウトを必要とする様々な場面での精度と信頼性が向上します。

コミット

commit f466617a620ce98a7a6a84f51621458641a0c38f
Author: Carl Shapiro <cshapiro@google.com>
Date:   Thu Feb 21 12:52:26 2013 -0800

    cmd/5g, cmd/5l, cmd/6l, cmd/8l, cmd/gc, cmd/ld, runtime: accurate args and locals information
    
    Previously, the func structure contained an inaccurate value for
    the args member and a 0 value for the locals member.
    
    This change populates the func structure with args and locals
    values computed by the compiler.  The number of args was
    already available in the ATEXT instruction.  The number of
    locals is now passed through in the new ALOCALS instruction.
    
    This change also switches the unit of args and locals to be
    bytes, just like the frame member, instead of 32-bit words.
    
    R=golang-dev, bradfitz, cshapiro, dave, rsc
    CC=golang-dev
    https://golang.org/cl/7399045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/f466617a620ce98a7a6a84f51621458641a0c38f

元コミット内容

以前のGoランタイムでは、func構造体に含まれるargs(引数)メンバーの値が不正確であり、locals(ローカル変数)メンバーの値は常に0でした。このコミットは、コンパイラによって計算された正確なargslocalsの値をfunc構造体に格納するように変更します。引数の数は既にATEXT命令で利用可能でしたが、ローカル変数の数は新しいALOCALS命令を通じて渡されるようになりました。また、argslocalsの単位が、frameメンバーと同様に32ビットワードからバイトに変更されました。

変更の背景

Go言語のランタイムは、ガベージコレクション、スタックトレースの生成、デバッグ、プロファイリングなど、様々な内部処理において関数のスタックフレームに関する正確な情報を必要とします。特に、関数呼び出し時の引数の配置やローカル変数のメモリ使用量を正確に把握することは、メモリ管理の効率化やデバッグ情報の充実において不可欠です。

このコミット以前は、func構造体(Goランタイムが関数に関するメタデータを格納するために使用する内部構造体)内のargsフィールドが不正確な値を持ち、localsフィールドは常に0でした。これは、ランタイムが関数のスタックフレームの正確なレイアウトを把握する上で大きな制約となっていました。例えば、正確なスタックトレースを生成する際に、引数の値やローカル変数の状態を正しく表示できない、あるいはガベージコレクタがスタック上のポインタを正確に識別できないといった問題が発生する可能性がありました。

この問題に対処するため、コンパイラが関数の引数とローカル変数のサイズを正確に計算し、その情報をランタイムに渡すメカニズムを導入する必要がありました。これにより、func構造体がより信頼性の高いスタックフレーム情報を持つことができ、ランタイムの様々な機能がより正確かつ効率的に動作するようになります。また、サイズの単位を32ビットワードからバイトに統一することで、frameメンバーとの整合性が取れ、計算の複雑さを軽減する狙いもあります。

前提知識の解説

このコミットを理解するためには、以下のGo言語の内部構造と概念に関する知識が必要です。

  1. Go言語のコンパイラとリンカ (cmd/gc, cmd/5l, cmd/6l, cmd/8l, cmd/ld):

    • cmd/gc: Go言語の公式コンパイラです。Goのソースコードをアセンブリコード(または中間表現)に変換し、関数のスタックフレームのレイアウトや変数に関する情報を生成します。
    • cmd/5l, cmd/6l, cmd/8l: それぞれARM (5l), AMD64 (6l), x86 (8l) アーキテクチャ向けのリンカです。コンパイラが生成したオブジェクトファイルを結合し、実行可能ファイルを生成します。この過程で、関数に関するメタデータも処理されます。
    • cmd/ld: 汎用リンカ。上記アーキテクチャ固有のリンカの共通部分を担うこともあります。 これらのツールは密接に連携し、Goプログラムのビルドプロセスを構成しています。
  2. func構造体: Goランタイム内部で使用される構造体で、個々のGo関数に関するメタデータ(関数名、開始PC、スタックフレームサイズ、引数サイズ、ローカル変数サイズなど)を格納します。この情報は、スタックトレース、デバッグ、プロファイリング、ガベージコレクションなど、ランタイムの様々な機能で利用されます。

  3. スタックフレーム (Stack Frame): 関数が呼び出されるたびに、その関数専用のメモリ領域がスタック上に確保されます。これをスタックフレームと呼びます。スタックフレームには、関数の引数、ローカル変数、戻りアドレス、レジスタの退避領域などが含まれます。Goのスタックは通常、下位アドレスに向かって成長します。

  4. 引数 (Arguments) とローカル変数 (Local Variables):

    • 引数: 関数に渡される値です。Goでは、引数は通常スタックフレームの一部として配置されます。
    • ローカル変数: 関数内で宣言され、その関数スコープ内でのみ有効な変数です。これらもスタックフレーム内に確保されます。
  5. ATEXT命令: Goのアセンブリ言語(Plan 9アセンブラ)における命令の一つで、関数の定義を開始するために使用されます。TEXTディレクティブとも呼ばれます。この命令は、関数の名前、フラグ、そしてスタックフレームのサイズや引数のサイズに関する情報を含みます。

  6. ALOCALS命令 (新設): このコミットで新しく導入された概念です。コンパイラが計算したローカル変数のサイズをリンカに伝えるための内部的な命令(または擬似命令)として機能します。これにより、リンカはfunc構造体に正確なローカル変数サイズを記録できるようになります。

  7. 単位の変更 (32-bit words to bytes): 以前はargslocalsのサイズが32ビットワード(4バイト)単位で表現されていましたが、この変更によりバイト単位で表現されるようになりました。これは、frameメンバー(スタックフレーム全体のサイズ)が既にバイト単位であったため、整合性を保ち、計算を簡素化するためです。例えば、32ビットワードで2ワードの引数がある場合、以前はargs=2でしたが、変更後はargs=8(2ワード * 4バイト/ワード)となります。

これらの概念を理解することで、コンパイラとリンカがどのように連携して関数のメタデータを生成し、ランタイムがそれをどのように利用するのか、そして今回の変更がそのプロセスにどのような影響を与えるのかが明確になります。

技術的詳細

このコミットの技術的詳細は、Goコンパイラ (cmd/gc)、リンカ (cmd/5l, cmd/6l, cmd/8l, cmd/ld)、およびランタイム (src/pkg/runtime) の間の連携と、func構造体のデータ表現の変更に集約されます。

  1. ALOCALS命令の導入:

    • コンパイラ (cmd/gc/pgen.c) は、関数のローカル変数の合計サイズを計算します。この計算されたサイズをリンカに伝えるために、新しい擬似命令 ALOCALS が導入されました。
    • pgen.c 内の compile 関数で、gins(ALOCALS, N, N) を呼び出すことで ALOCALS 命令が生成され、その to.offset フィールドに計算されたスタックフレームサイズ(ローカル変数の合計サイズ)が設定されます。これにより、コンパイラがローカル変数のサイズをリンカに明示的に渡すパスが確立されました。
  2. リンカにおけるALOCALSの処理:

    • 各アーキテクチャのリンカ (cmd/5l/obj.c, cmd/6l/obj.c, cmd/8l/obj.c) は、ALOCALS命令を認識し、処理するように変更されました。
    • リンカはALOCALS命令を見つけると、現在のシンボル(関数)のlocalsメンバーに、ALOCALS命令のto.offsetに格納されている値を代入します。これにより、Sym構造体(リンカがシンボル情報を管理するための内部構造体)に正確なローカル変数サイズが記録されます。
    • また、5l/5.out.h, 6l/6.out.h, 8l/8.out.hALOCALS が新しい命令タイプとして追加されています。
  3. Sym構造体の拡張:

    • リンカが使用するSym構造体 (cmd/5l/l.h, cmd/6l/l.h, cmd/8l/l.h) に、localsargsという新しいフィールドが追加されました。これらはそれぞれ、スタックフレームのローカル変数領域と引数領域のサイズをバイト単位で格納します。
    • ATEXT命令の処理も変更され、p->to.offset2(またはp->to.offset >> 32 for 6l)から引数のサイズをSym->argsに格納するようになりました。これにより、引数のサイズもリンカによって正確に記録されるようになります。
  4. func構造体の更新と単位の変更:

    • ランタイムのFunc構造体 (src/pkg/runtime/extern.go, src/pkg/runtime/runtime.h) の定義が変更され、argslocalsフィールドのコメントが「number of 32-bit in/out args」から「in/out args size」および「number of 32-bit locals」から「locals size」に変更されました。これは、これらのフィールドがバイト単位でサイズを格納するようになったことを示しています。
    • 型自体はint32のままですが、その解釈がワード数からバイト数に変わったことを意味します。
  5. シンボルテーブルの処理 (src/pkg/runtime/symtab.c):

    • ランタイムがシンボルテーブルを解析するdofunc関数が大幅に変更されました。以前は'm'タイプのシンボル(フレームサイズ関連)と'p'タイプのシンボル(引数関連)を単純に処理していましたが、この変更により、.frame, .locals, .argsという特定の名前を持つ'm'タイプのシンボルを区別して処理するようになりました。
    • これにより、リンカから渡された正確な.frame, .locals, .argsの値をfunc構造体の対応するフィールドに直接設定できるようになりました。以前の引数計算ロジック(f->args < sym->value/4 + 2など)は削除され、リンカからの直接的な値が使用されます。
  6. スタックトレースの調整 (src/pkg/runtime/traceback_arm.c, src/pkg/runtime/traceback_x86.c):

    • スタックトレースを生成する関数 (runtime·gentraceback) において、引数の数を計算する際にf->argssizeof(uintptr)で割るようになりました。これは、f->argsがバイト単位になったため、実際の引数の数を取得するためにはポインタのサイズで割る必要があるためです。

これらの変更により、Goのコンパイラ、リンカ、ランタイムが連携して、関数のスタックフレームに関するより正確で詳細なメタデータを生成・利用できるようになり、Goプログラムの内部動作の信頼性とデバッグ可能性が向上しました。

コアとなるコードの変更箇所

このコミットでは、主に以下のファイル群が変更されています。

  • コンパイラ (cmd/gc):

    • src/cmd/gc/pgen.c: コンパイラがALOCALS命令を生成し、ローカル変数のサイズをリンカに渡すロジックが追加されました。
  • リンカ (cmd/5l, cmd/6l, cmd/8l):

    • src/cmd/{5,6,8}l/{5,6,8}.out.h: 新しい命令タイプALOCALSが定義されました。
    • src/cmd/{5,6,8}l/l.h: Sym構造体にlocalsargsフィールドが追加されました。
    • src/cmd/{5,6,8}l/obj.c: リンカがALOCALS命令を処理し、Sym構造体のlocalsフィールドを更新するロジックが追加されました。また、ATEXT命令からargsフィールドを更新するロジックも変更されました。
    • src/cmd/{6,8}l/optab.c: ALOCALS命令がリンカのオペレーションテーブルに追加されました。
  • リンカ共通ライブラリ (cmd/ld):

    • src/cmd/ld/lib.c: シンボル情報を出力するgenasmsym関数が変更され、.locals.argsという新しいシンボルが追加されました。
  • ランタイム (src/pkg/runtime):

    • src/pkg/runtime/extern.go: Func構造体のargslocalsフィールドのコメントが更新され、バイト単位でのサイズを示すようになりました。
    • src/pkg/runtime/runtime.h: C言語版のFunc構造体の定義も同様に更新されました。
    • src/pkg/runtime/symtab.c: シンボルテーブルを解析し、func構造体を構築するロジックが変更され、.frame, .locals, .argsシンボルを区別して処理するようになりました。
    • src/pkg/runtime/traceback_arm.c, src/pkg/runtime/traceback_x86.c: スタックトレース生成時に、Func.argsがバイト単位であることを考慮して引数の数を計算するように変更されました。

これらの変更は、Goのビルドツールチェーン全体にわたる協調的な作業であり、コンパイラが情報を生成し、リンカがそれを処理して最終的なバイナリに埋め込み、ランタイムがその情報を利用するという一連のフローを確立しています。

コアとなるコードの解説

このコミットにおける主要なコード変更とその意味をいくつかピックアップして解説します。

  1. src/cmd/gc/pgen.c における ALOCALS 命令の生成:

    // src/cmd/gc/pgen.c
    // ...
    compile(Node *fn)
    {
        // ...
        Prog *plocals, *ptxt; // plocalsが追加
        // ...
        ginit();
        plocals = gins(ALOCALS, N, N); // ALOCALS命令を生成
        // ...
        // allocautoの呼び出し後、stksizeが確定した時点でplocalsのoffsetを設定
        allocauto(ptxt);
        plocals->to.offset = stksize; // ローカル変数の合計サイズをALOCALS命令に設定
        // ...
    }
    

    この変更は、コンパイラが関数のローカル変数の合計サイズ (stksize) を計算した後、その値を新しいALOCALS命令のto.offsetフィールドに格納していることを示しています。このALOCALS命令は、リンカによって読み取られ、関数のlocals情報として利用されます。これは、コンパイラからリンカへの情報伝達の新しいパスを確立するものです。

  2. リンカの Sym 構造体 (src/cmd/{5,6,8}l/l.h):

    // src/cmd/5l/l.h (同様の変更が6l/l.h, 8l/l.hにも適用)
    struct	Sym
    {
        // ...
        int32	locals;	// size of stack frame locals area (バイト単位)
        int32	args;	// size of stack frame incoming arguments area (バイト単位)
        // ...
    };
    

    リンカが内部でシンボル情報を管理するために使用するSym構造体に、localsargsという2つの新しいフィールドが追加されました。これらのフィールドは、それぞれローカル変数領域と引数領域のサイズをバイト単位で保持します。これにより、リンカは関数ごとの正確なスタックフレーム情報を保持できるようになります。

  3. リンカの obj.c における ALOCALS の処理:

    // src/cmd/5l/obj.c (同様の変更が6l/obj.c, 8l/obj.cにも適用)
    // ...
    loop:
        // ...
        case ALOCALS:
            cursym->locals = p->to.offset; // ALOCALS命令からlocalsサイズを取得し、Symに設定
            pc++;
            break;
        case ATEXT:
            // ...
            s->args = p->to.offset2; // ATEXT命令からargsサイズを取得し、Symに設定
            // ...
    

    リンカのオブジェクトファイル処理部分で、ALOCALS命令が検出された際に、その命令のto.offset(コンパイラが設定したローカル変数サイズ)を現在のシンボル(関数)のlocalsフィールドに代入しています。また、ATEXT命令の処理も更新され、p->to.offset2から引数のサイズをs->argsに格納しています。これにより、リンカはコンパイラから渡された正確なargslocalsの情報をSym構造体に記録します。

  4. ランタイムの Func 構造体 (src/pkg/runtime/extern.go):

    // src/pkg/runtime/extern.go
    type Func struct { // Keep in sync with runtime.h:struct Func
        // ...
        frame  int32 // stack frame size
        args   int32 // in/out args size (コメントが変更)
        locals int32 // locals size (コメントが変更)
    }
    

    Goのランタイムが使用するFunc構造体のargslocalsフィールドのコメントが変更されました。これは、これらのフィールドがもはや32ビットワード数ではなく、バイト単位のサイズを格納するようになったことを明確に示しています。

  5. ランタイムの symtab.c におけるシンボル処理:

    // src/pkg/runtime/symtab.c
    // ...
    dofunc(Sym *sym)
    {
        // ...
        case 'm': // 'm'タイプのシンボル処理
            if(nfunc <= 0 || func == nil)
                break;
            if(runtime·strcmp(sym->name, (byte*)".frame") == 0)
                func[nfunc-1].frame = sym->value;
            else if(runtime·strcmp(sym->name, (byte*)".locals") == 0)
                func[nfunc-1].locals = sym->value; // .localsシンボルからlocalsサイズを設定
            else if(runtime·strcmp(sym->name, (byte*)".args") == 0)
                func[nfunc-1].args = sym->value;   // .argsシンボルからargsサイズを設定
            else {
                runtime·printf("invalid 'm' symbol named '%s'\n", sym->name);
                runtime·throw("mangled symbol table");
            }
            break;
        // ...
    }
    

    ランタイムがシンボルテーブルを読み込む際に、.frame, .locals, .argsという特定の名前を持つ'm'タイプのシンボルを識別し、それぞれのvalue(リンカが設定したサイズ)をfunc構造体の対応するフィールドに直接代入するようになりました。これにより、以前の複雑な引数計算ロジックが不要になり、リンカから提供される正確な値がそのまま利用されます。

  6. スタックトレースの調整 (src/pkg/runtime/traceback_arm.c, src/pkg/runtime/traceback_x86.c):

    // src/pkg/runtime/traceback_arm.c (同様の変更がtraceback_x86.cにも適用)
    // ...
    runtime·gentraceback(...)
    {
        // ...
        // 以前: for(i = 0; i < f->args; i++)
        for(i = 0; i < f->args/sizeof(uintptr); i++) { // バイト単位になったf->argsをuintptrのサイズで割る
            // ...
        }
        // ...
    }
    

    スタックトレースを生成する際、f->argsがバイト単位で格納されるようになったため、実際の引数の数を取得するためには、ポインタのサイズ(sizeof(uintptr))で割る必要があります。この変更により、スタックトレースが正しく引数を表示できるようになります。

これらのコード変更は、Goのコンパイラ、リンカ、ランタイムが連携して、関数のスタックフレームに関するより正確なメタデータを生成し、利用するための重要なステップを示しています。

関連リンク

参考にした情報源リンク