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

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

このコミットは、Go言語のコンパイラ(cmd/5c, cmd/6c, cmd/8c)に対して、すべての関数呼び出しにおける引数のサイズを記録する機能を追加するものです。この情報は、Goランタイムがガベージコレクション、スタックトレース、デバッグなどの目的で、実行時に正確なスタックフレーム情報を利用するために不可欠です。

コミット

commit 4e141145b731b8adfc5e8ba44334ae63d6da80a2
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jul 16 16:24:43 2013 -0400

    cmd/5c, cmd/6c, cmd/8c: record arg size for every call
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/11364043

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

https://github.com/golang/go/commit/4e141145b731b8adfc5e8ba44334ae63d6da80a2

元コミット内容

cmd/5c, cmd/6c, cmd/8c: record arg size for every call

R=ken2
CC=golang-dev
https://golang.org/cl/11364043

変更の背景

Goランタイムは、プログラムの実行中に様々な低レベルの操作を行います。これには、ガベージコレクション(GC)による不要なメモリの解放、スタックトレースの生成(エラー発生時やデバッグ時)、goroutineのスケジューリングなどが含まれます。これらの操作を正確かつ効率的に行うためには、各関数のスタックフレームの構造、特に引数のサイズに関する正確なメタデータが必要となります。

特に、Go言語では可変長引数(...)を持つ関数が存在し、また異なるCPUアーキテクチャ(x86, x86-64, ARMなど)間で関数呼び出し規約が異なる場合があります。これらの複雑性に対応し、ランタイムが常に正しいスタック情報を取得できるようにするためには、コンパイル時に引数サイズを明示的に記録するメカニズムが不可欠でした。

このコミットは、Goコンパイラが関数呼び出しの際に引数サイズをPCDATA(Program Counter Data)として埋め込むことで、ランタイムがこの情報を利用できるようにするための基盤を強化するものです。これにより、ランタイムの堅牢性と正確性が向上し、将来的な機能拡張(例えば、より高度なデバッグツールやプロファイリングツール)への道が開かれます。

前提知識の解説

Goコンパイラ (5c, 6c, 8c)

Go言語の初期のコンパイラ群は、ターゲットとするCPUアーキテクチャに応じて異なる名前が付けられていました。

  • 5c: ARMアーキテクチャ(例: Raspberry Pi, スマートフォン)をターゲットとするコンパイラ。
  • 6c: x86-64アーキテクチャ(例: 現代のほとんどのデスクトップPCやサーバー)をターゲットとするコンパイラ。
  • 8c: x86アーキテクチャ(例: 32ビットシステム)をターゲットとするコンパイラ。

これらのコンパイラは、Goのソースコードを対応するアーキテクチャのアセンブリコードに変換する役割を担っていました。これらは、Plan 9オペレーティングシステムのCコンパイラツールチェーンをベースに開発されました。

PCDATA (Program Counter Data)

PCDATAは、Goランタイムがプログラムの実行時に必要とするメタデータの一種です。これは、コンパイラによって生成され、最終的な実行可能ファイル内の特定のセクションに格納されます。ランタイムは、プログラムカウンタ(PC、つまり実行中のコードのアドレス)に基づいてこれらのPCDATAをルックアップし、様々な情報を取得します。

PCDATAには、以下のような情報が含まれることがあります。

  • ガベージコレクションのためのポインタ情報: スタックやヒープ上のどの位置にポインタが存在するかをランタイムに教え、GCが正確に到達可能なオブジェクトを特定できるようにします。
  • スタックフレームのサイズ: 各関数のスタックフレームの総サイズ。
  • 関数の引数や戻り値のサイズ: 特にこのコミットで焦点が当てられている情報です。

PCDATAは、Goの効率的なガベージコレクション、正確なスタックトレース、そして動的なデバッグ機能を実現するための重要な要素です。

PCDATA_ArgSize

PCDATA_ArgSizeは、PCDATAの一種で、特定のプログラムカウンタ(PC)値における関数の引数サイズを示します。関数呼び出しの直前や直後など、特定のコードポイントでこのPCDATAを記録することで、Goランタイムは関数呼び出し時のスタック上の引数領域のサイズを正確に把握できます。これにより、ランタイムはスタックの巻き戻し(unwinding)や、スタック上のポインタの走査を正確に行うことができます。

NOSPLIT

NOSPLITは、Goの関数に適用できるコンパイラディレクティブ(または属性)の一つです。この属性が付けられた関数は、実行時にスタックの拡張を行いません。通常、Go関数は必要に応じてスタックを自動的に拡張しますが、NOSPLIT関数はスタック拡張を行わないため、スタックフレームのサイズが固定されている必要があります。これは、非常に短い関数や、ランタイムのコア部分など、スタック拡張のオーバーヘッドを避けたい場合に利用されます。

hasdotdotdot()

hasdotdotdot()は、Goコンパイラの内部で利用されるヘルパー関数で、現在の関数が可変長引数(...、Goでは...typeとして表現される)を持つかどうかを判定します。可変長引数を持つ関数は、引数の処理方法が通常の関数とは異なるため、コンパイラは特別な処理を行う必要があります。このコミットでは、引数サイズの記録ロジックにおいて、可変長引数を持つ関数に対する特別な考慮が必要となるため、この関数が利用されています。

技術的詳細

このコミットの主要な技術的変更点は、Goコンパイラが関数呼び出しの際に、その呼び出しの引数サイズをPCDATAとして実行可能ファイルに埋め込むことです。これにより、Goランタイムは実行時にこの情報を利用して、より正確なスタック管理とガベージコレクションを実現します。

具体的な変更は以下の通りです。

  1. gpcdata関数の導入: src/cmd/{5c,6c,8c}/txt.cgpcdataという新しい関数が追加されました。この関数は、指定されたPCDATAインデックス(例: PCDATA_ArgSize)とそれに対応する値(例: 引数サイズ)を受け取り、APCDATAというアセンブリ命令を生成します。APCDATA命令は、特定のPC値にPCDATAを関連付けるための命令です。

  2. 関数呼び出し箇所でのPCDATA_ArgSizeの記録: src/cmd/{5c,6c,8c}/cgen.c内のコード生成ロジックが変更され、関数呼び出しの直前でgpcdata(PCDATA_ArgSize, curarg)が呼び出されるようになりました。curargは、その時点での関数呼び出しの引数サイズを保持している変数です。これにより、ランタイムは関数が呼び出される直前のスタック上の引数領域のサイズを知ることができます。 さらに、関数呼び出しの直後にはgpcdata(PCDATA_ArgSize, -1)が呼び出されます。これは、そのPC値以降は、直前の関数呼び出しの引数サイズ情報がもはや有効ではないことをランタイムに伝えるためのマーカーとして機能します。-1は、PCDATAの「終了」または「無効化」を示す慣習的な値です。

  3. gtext関数における引数サイズの処理: src/cmd/{5c,6c,8c}/sgen.c内のgtext関数(関数のプロローグ、つまり関数の開始部分のアセンブリコードを生成する関数)が変更されました。

    • NOSPLIT属性を持たない関数、または可変長引数(...)を持つ関数に対して、argsize()関数で計算された引数サイズが取得されます。
    • 重要な変更点として、argsize()0を返す場合、その値を1に変換するロジックが追加されました。これは、引数サイズが実際に0である場合でも、その情報が「存在する」ことをランタイムに明示的に伝えるための慣習的な処理です。これにより、ランタイムは「引数サイズが0である」という情報と、「引数サイズの情報が全く存在しない」という状態を区別できるようになります。
    • この引数サイズは、最終的に関数のメタデータの一部として実行可能ファイルに埋め込まれ、ランタイムが利用します。6cコンパイラの場合、この引数サイズは関数のスタックフレーム情報の上位32ビットに格納されます。
  4. hasdotdotdot()関数の追加: src/cmd/cc/pgen.chasdotdotdot()という新しいヘルパー関数が追加されました。この関数は、現在の関数が可変長引数を持つかどうかを判定します。この関数は、sgen.cgtext関数内で、可変長引数を持つ関数に対する引数サイズ記録の条件分岐に利用されます。

これらの変更により、Goコンパイラはより詳細で正確なスタック関連のメタデータを生成できるようになり、Goランタイムの堅牢性と効率性が向上します。

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

このコミットによって変更された主要なファイルと、その役割は以下の通りです。

  • src/cmd/{5c,6c,8c}/cgen.c:

    • GoのAST(抽象構文木)からアセンブリコードを生成する主要なファイル。
    • 関数呼び出しのコード生成ロジックに、gpcdata(PCDATA_ArgSize, curarg)gpcdata(PCDATA_ArgSize, -1)の呼び出しが追加されました。
  • src/cmd/{5c,6c,8c}/gc.h:

    • 各コンパイラの共通ヘッダーファイル。
    • 新しく追加されたgpcdata関数のプロトタイプ宣言が追加されました。
  • src/cmd/{5c,6c,8c}/sgen.c:

    • シンボルテーブルの生成や、関数のテキストセクション(アセンブリコード本体)の生成に関連するファイル。
    • gtext関数内で、NOSPLITでない関数や可変長引数を持つ関数に対して、argsize()で取得した引数サイズをPCDATAとして記録するロジックが追加されました。
    • argsize()0を返す場合に1に変換する処理もここに含まれます。
  • src/cmd/{5c,6c,8c}/txt.c:

    • アセンブリコードの出力や、PCDATAなどの特殊な命令の生成に関連するファイル。
    • gpcdata関数の具体的な実装が追加されました。この関数はAPCDATAアセンブリ命令を生成します。
  • src/cmd/cc/cc.h:

    • Goコンパイラ全体で共有される共通ヘッダーファイル。
    • hasdotdotdot()関数のプロトタイプ宣言が追加されました。
  • src/cmd/cc/pgen.c:

    • Goのパーサーが生成した中間表現から、コード生成に必要な情報を準備するファイル。
    • hasdotdotdot()関数の具体的な実装が追加されました。

コアとなるコードの解説

gpcdata関数の実装 (src/cmd/{5c,6c,8c}/txt.c)

void
gpcdata(int index, int value)
{
    Node n1;

    n1 = *nodconst(index);
    gins(APCDATA, &n1, nodconst(value));
}

この関数は、PCDATAをアセンブリコードに埋め込むためのヘルパーです。

  • index: 記録するPCDATAの種類(例: PCDATA_ArgSize)。
  • value: そのPCDATAに関連付けられる値(例: 引数サイズ)。
  • nodconst(index): indexを定数ノードに変換します。
  • gins(APCDATA, &n1, nodconst(value)): APCDATAというアセンブリ命令を生成し、現在のコードストリームに挿入します。この命令は、indexvalueを関連付け、ランタイムがPC値に基づいてこの情報を取得できるようにします。

cgen.cにおけるgpcdataの呼び出し例

// 関数呼び出しの引数を準備する処理の後
gargs(r, &nod, &nod1);
gpcdata(PCDATA_ArgSize, curarg); // 関数呼び出し直前の引数サイズを記録

// ... ここで実際の関数呼び出しのアセンブリコードが生成される ...

gpcdata(PCDATA_ArgSize, -1);    // 関数呼び出し後の引数サイズ情報の終了をマーク

cgen.cでは、関数呼び出しのコードを生成する際に、その前後にgpcdataを呼び出しています。

  • curarg: 現在の関数呼び出しの引数サイズを保持している変数です。この値がPCDATA_ArgSizeとして記録されます。
  • gpcdata(PCDATA_ArgSize, -1): 関数呼び出しが完了した後、このPCDATAがもはや有効ではないことをランタイムに伝えます。これにより、ランタイムはPCDATAの有効範囲を正確に把握できます。

sgen.cにおけるgtextの変更例

Prog*
gtext(Sym *s, int32 stkoff)
{
    int32 a; // または vlong v; (6cの場合)

    a = 0;
    if(!(textflag & NOSPLIT) || !hasdotdotdot()) {
        a = argsize();
        // Change argsize 0 to 1 to be mark that
        // the argument size is present.
        if(a == 0)
            a = 1;
    }
    // ...
    // この 'a' の値が関数のメタデータの一部として利用される
    // 6cの場合: v |= a << 32; のように上位32ビットに格納される
    // ...
}

gtext関数は、Go関数のアセンブリコードの開始部分(プロローグ)を生成します。

  • !(textflag & NOSPLIT): 関数がNOSPLIT属性を持たない場合(つまり、スタック拡張が許可されている場合)。
  • !hasdotdotdot(): 関数が可変長引数を持たない場合。
  • 上記の条件が満たされる場合、argsize()関数が呼び出され、その関数の引数サイズが計算されます。
  • if(a == 0) a = 1;: ここが重要なロジックです。argsize()0を返した場合(引数がない場合など)、その値を1に強制的に変更しています。これは、ランタイムに対して「引数サイズの情報は存在するが、その値は0である」ということを明示的に伝えるための慣習です。これにより、「情報が存在しない」状態と区別できます。
  • このa(またはv)の値は、最終的にGo実行可能ファイルの関数情報セクションに埋め込まれ、ランタイムがスタックフレームの情報を取得する際に利用されます。

pgen.cにおけるhasdotdotdot関数の実装

int
hasdotdotdot(void)
{
    Type *t;

    for(t=thisfn->down; t!=T; t=t->down)
        if(t->etype == TDOT)
            return 1;
    return 0;
}

この関数は、現在の関数(thisfn)の引数リストを走査します。

  • thisfn->down: 関数の最初の引数へのポインタ。
  • t->etype == TDOT: 引数の型がTDOTであるかどうかをチェックします。TDOTはGoコンパイラの内部で可変長引数(...)を示すために使用される特殊な型です。
  • 可変長引数が見つかれば1を返し、そうでなければ0を返します。この結果は、sgen.cgtext関数で利用され、可変長引数を持つ関数に対する引数サイズ記録の条件分岐に影響を与えます。

関連リンク

参考にした情報源リンク

  • (特になし。コミット内容と差分から直接解析しました。)