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

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

このコミットは、Goコンパイラ(cmd/gc)において、間接関数呼び出し(indirect function calls)の引数サイズ(argument size)を記録するように変更を加えるものです。これにより、reflect.methodValueCallmakeFuncStubといったGoのreflectパッケージが生成する特殊な関数呼び出しのスタックアンワインド(stack unwind)が適切に行われるようになります。

コミット

commit 8679d5f2b5a621099af285587601d9f0c3f9b93b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Jul 31 20:00:33 2013 +0400

    cmd/gc: record argument size for all indirect function calls
    This is required to properly unwind reflect.methodValueCall/makeFuncStub.
    Fixes #5954.
    Stats for 'go install std':
    61849 total INSTCALL
    24655 currently have ArgSize metadata
    27278 have ArgSize metadata with this change
    godoc size before: 11351888, after: 11364288
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12163043

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

https://github.com/golang/go/commit/8679d5f2b5a621099af285587601d9f0c3f9b93b

元コミット内容

Goコンパイラ(cmd/gc)が、全ての間接関数呼び出しに対して引数サイズ(ArgSize)のメタデータを記録するように変更します。これは、reflect.methodValueCallおよびmakeFuncStubのスタックアンワインドを正しく行うために必要です。

この変更により、go install stdの統計では、ArgSizeメタデータを持つINSTCALL(間接呼び出し)の数が24655から27278に増加しました。godocのバイナリサイズはわずかに増加しています(11351888バイトから11364288バイトへ)。

変更の背景

Goのランタイムは、プログラムの実行中にスタックトレースを生成したり、ガベージコレクションのためにスタックをスキャンしたりする際に、各関数のスタックフレームの構造を正確に知る必要があります。特に、関数の引数がスタック上にどのように配置されているか、そのサイズはどれくらいかという情報は、スタックポインタを適切に進めたり、引数領域を正確に特定したりするために不可欠です。

通常の関数呼び出しでは、コンパイル時に引数の型と数が確定しているため、コンパイラは固定の引数サイズを決定し、そのメタデータを生成できます。しかし、Goのreflectパッケージは、実行時に動的に関数を呼び出す機能(reflect.Callなど)を提供します。この動的な呼び出しは、reflect.methodValueCallmakeFuncStubといった特殊なアセンブリコードによって実現されます。これらの関数は、呼び出される実際の関数の引数リストが実行時まで分からないため、可変長の引数を扱う必要があります。

従来のGoランタイムでは、これらの可変長引数を持つ間接呼び出しの引数サイズ情報が不足しており、スタックアンワインドが正しく行えないという問題(Issue #5954)がありました。スタックアンワインドが正しく行えないと、デバッガでのスタックトレース表示が不正確になったり、ガベージコレクタがスタック上のポインタを正しく識別できず、メモリリークやクラッシュの原因となる可能性がありました。

このコミットは、この問題を解決するために、コンパイラがすべて間接関数呼び出しに対して、呼び出しサイトで引数サイズ情報を記録するように拡張することで、reflectパッケージが生成する動的な呼び出しでも正確なスタックアンワインドを可能にすることを目的としています。

前提知識の解説

1. Goのスタックフレームとスタックアンワインド

Goプログラムが関数を呼び出す際、その関数に必要なローカル変数、引数、戻り値などを格納するためにスタック上に「スタックフレーム」が割り当てられます。スタックフレームのサイズは、コンパイル時に決定される固定部分と、可変長引数(...)やreflectパッケージによる動的呼び出しのように実行時に決定される可変部分から構成されます。

「スタックアンワインド」とは、現在実行中の関数から呼び出し元の関数へとスタックを遡り、各スタックフレームの情報を取得するプロセスです。これは、主に以下の目的で利用されます。

  • デバッグとスタックトレース: プログラムがパニックを起こしたり、デバッガで停止したりした際に、どの関数がどの順序で呼び出されたか(コールスタック)を表示するために必要です。
  • ガベージコレクション: Goのガベージコレクタは、スタック上のポインタを識別し、到達可能なオブジェクトをマークするためにスタックをスキャンします。この際、スタックフレームのどこにポインタがあるかを正確に知る必要があります。
  • プロファイリング: 実行中の関数の呼び出しパスを追跡し、パフォーマンスのボトルネックを特定するために使用されます。

スタックアンワインドを正確に行うためには、各スタックフレームのサイズ、特に引数領域のサイズを正確に知る必要があります。

2. reflectパッケージと動的関数呼び出し

Goのreflectパッケージは、実行時に型情報やオブジェクトの構造を検査・操作する機能を提供します。これには、実行時に動的に関数を呼び出す機能も含まれます。

  • reflect.Value.Call(): reflect.Value型の関数オブジェクトに対して、引数をreflect.Valueのスライスとして渡し、関数を呼び出すことができます。
  • reflect.MakeFunc(): 任意の関数シグネチャを持つ新しい関数を動的に作成し、その実装をGoの関数で提供することができます。

これらの動的な関数呼び出しは、Goのコンパイラが生成する通常の関数呼び出しとは異なり、引数の型や数がコンパイル時には確定していません。そのため、reflectパッケージは、これらの動的な呼び出しを処理するために、特殊なアセンブリコード(reflect.methodValueCallmakeFuncStubなど)を内部的に使用します。これらのアセンブリコードは、可変長の引数を効率的に処理するために、通常の関数呼び出しとは異なるスタックフレーム構造を持つことがあります。

3. ArgSizeメタデータ

Goのコンパイラは、各関数について、その引数領域のサイズに関するメタデータ(ArgSize)を生成します。このメタデータは、ランタイムがスタックアンワインドを行う際に、スタックポインタを適切に調整し、次のスタックフレームの開始位置を特定するために使用されます。

通常の関数ではArgSizeは固定ですが、可変長引数を持つ関数やreflectパッケージが生成する動的な関数では、呼び出しごとに引数サイズが異なる可能性があります。このような場合、コンパイラは呼び出しサイト(call site)でその呼び出し固有の引数サイズ情報を記録する必要があります。

4. INSTCALL

INSTCALLは、Goコンパイラ内部で「間接関数呼び出し(indirect function call)」を指す用語です。これは、関数ポインタやインターフェースメソッドの呼び出しのように、呼び出される関数がコンパイル時には確定せず、実行時に決定される呼び出しを指します。reflectパッケージによる動的呼び出しも、この間接呼び出しの一種と見なせます。

技術的詳細

このコミットの核心は、Goコンパイラのバックエンド(cmd/gc、具体的には5g, 6g, 8gといった各アーキテクチャ向けのジェネレータ)が、間接関数呼び出しの際に、その呼び出しの引数サイズ情報をより網羅的に記録するように変更された点です。

Goのランタイムは、スタックトレースやガベージコレクションのためにスタックをスキャンする際、各スタックフレームのサイズ、特に引数領域のサイズを知る必要があります。これは、スタックポインタを適切に調整し、呼び出し元のスタックフレームに移動するために不可欠です。

従来のコンパイラは、ほとんどの関数(固定引数を持つ関数)に対しては、その関数の型情報から引数サイズを決定し、メタデータとして記録していました。しかし、runtimeパッケージ内の一部の可変長引数関数や、reflectパッケージが動的に生成する関数(reflect.methodValueCallmakeFuncStub)は、呼び出しごとに引数サイズが異なる可能性があります。これらのケースでは、呼び出しサイト(call site)で具体的な引数サイズを記録する必要がありました。

このコミット以前は、runtimeパッケージ内の特定の可変長引数関数に対してのみ、呼び出しサイトでの引数サイズ記録が行われていました。しかし、reflect.methodValueCallmakeFuncStubのようなreflectが生成する関数は、本質的に可変長の引数を扱うため、これらも同様に呼び出しサイトで引数サイズを記録する必要がありました。Issue #5954は、このreflect関連の関数における引数サイズ情報の欠如が、スタックアンワインドの不正確さを引き起こしていることを指摘していました。

この変更により、cmd/gcは、runtimeパッケージの関数だけでなく、すべての間接関数呼び出し(f->sym == S、つまりシンボルが不明な場合、またはf->sym->pkg == runtimepkgの場合)に対して、arg(引数サイズ)のメタデータを記録するようになりました。これにより、reflect.methodValueCallmakeFuncStubのような、コンパイル時には具体的な引数シグネチャが不明な動的呼び出しであっても、ランタイムが正確な引数サイズ情報を取得できるようになります。

結果として、INSTCALL(間接呼び出し)のうち、ArgSizeメタデータを持つものの数が約2,600増加し、godocバイナリのサイズがわずかに増加しました。これは、より多くの呼び出しサイトで追加のメタデータが生成されるようになったためです。

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

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

  1. src/cmd/{5g,6g,8g}/ggen.c: Goコンパイラの各アーキテクチャ(386, amd64, arm)向けのコード生成部分です。ginscall関数内で、間接関数呼び出しに対する引数サイズ情報の記録ロジックが変更されています。

    • 変更前: if(f->type != T && ((f->sym != S && f->sym->pkg == runtimepkg) || proc == 1 || proc == 2))
    • 変更後: if(f->type != T && (f->sym == S || (f->sym != S && f->sym->pkg == runtimepkg) || proc == 1 || proc == 2)) この変更により、f->sym == S(シンボルが不明な場合、つまり間接呼び出し全般)も引数サイズ記録の対象に含まれるようになりました。
  2. src/pkg/reflect/asm_*.s: reflectパッケージのアセンブリコードファイルです。makeFuncStubmethodValueCallの定義に、// No argsize here, gc generates argsize info at call site.というコメントが追加されました。これは、これらの関数自体が引数サイズ情報を持たず、コンパイラが呼び出しサイトでその情報を生成することを明示しています。

  3. src/pkg/runtime/traceback_*.c: Goランタイムのスタックトレース関連のCコードファイルです。スタックアンワインドのロジックに関するコメントが更新され、可変長引数を持つ関数がruntimeパッケージだけでなくreflectパッケージにも存在することが明記されました。

    • 変更前: // in package runtime, and for those we use call-specific
    • 変更後: // in package runtime and reflect, and for those we use call-specific

コアとなるコードの解説

src/cmd/{5g,6g,8g}/ggen.c の変更

// 変更前
if(f->type != T && ((f->sym != S && f->sym->pkg == runtimepkg) || proc == 1 || proc == 2)) {
    // ...
}

// 変更後
// Most functions have a fixed-size argument block, so traceback uses that during unwind.
// Not all, though: there are some variadic functions in package runtime,
// and for those we emit call-specific metadata recorded by caller.
// Reflect generates functions with variable argsize (see reflect.methodValueCall/makeFuncStub),
// so we do this for all indirect calls as well.
if(f->type != T && (f->sym == S || (f->sym != S && f->sym->pkg == runtimepkg) || proc == 1 || proc == 2)) {
    // ...
}

このコードスニペットは、Goコンパイラのginscall関数内にあります。ginscallは、関数呼び出しのコードを生成する際に、その呼び出しに関するメタデータ(特に引数サイズ)を決定する役割を担っています。

変更のポイントはif文の条件式です。

  • f->type != T: 関数fの型が不明ではないことを確認します。
  • f->sym == S: これは、関数fのシンボルが不明であることを意味します。Goコンパイラにおいて、シンボルが不明な関数呼び出しは、通常、実行時に解決される間接関数呼び出し(例: 関数ポインタを介した呼び出し、インターフェースメソッド呼び出し、reflectによる動的呼び出し)を指します。
  • f->sym != S && f->sym->pkg == runtimepkg: これは、関数fruntimeパッケージに属しており、かつシンボルが既知である場合を指します。runtimeパッケージには、可変長引数を持つ関数がいくつか存在するため、これらも特別扱いが必要です。
  • proc == 1 || proc == 2: これらは、特定の内部的なプロシージャ呼び出しタイプを指し、これらも引数サイズ情報の記録が必要なケースです。

変更前は、f->sym == S(シンボル不明の間接呼び出し全般)が条件に含まれていませんでした。そのため、reflect.methodValueCallmakeFuncStubのような、シンボルがコンパイル時に直接解決できない間接呼び出しに対して、呼び出しサイトでの引数サイズ情報が適切に記録されていませんでした。

変更後は、f->sym == Sが追加されたことで、すべての間接関数呼び出しに対して、arg(引数サイズ)のメタデータが生成されるようになりました。これにより、reflectパッケージが生成する動的な呼び出しであっても、ランタイムが正確な引数サイズ情報を取得できるようになり、スタックアンワインドの問題が解決されます。

src/pkg/reflect/asm_*.s の変更

// No argsize here, gc generates argsize info at call site.
TEXT ·makeFuncStub(SB),7,$8
// ...
// No argsize here, gc generates argsize info at call site.
TEXT ·methodValueCall(SB),7,$8

これらの変更は、アセンブリコード自体ではなく、その上のコメントの追加です。makeFuncStubmethodValueCallは、Goのreflectパッケージが動的に関数を呼び出す際に使用する特殊なアセンブリ関数です。

追加されたコメント「No argsize here, gc generates argsize info at call site.」は、これらのアセンブリ関数自体が固定の引数サイズ情報を持たないことを明示しています。代わりに、Goコンパイラ(gc)が、これらの関数を呼び出す各サイトで、その呼び出し固有の引数サイズ情報を生成し、メタデータとして記録することを意味しています。これは、前述のggen.cの変更と密接に関連しており、コンパイラが間接呼び出し全般に対して引数サイズ情報を記録するようになった結果、これらのreflect関連のアセンブリ関数がその恩恵を受けることを示しています。

src/pkg/runtime/traceback_*.c の変更

// 変更前
// Not all, though: there are some variadic functions
// in package runtime, and for those we use call-specific
// metadata recorded by f's caller.

// 変更後
// Not all, though: there are some variadic functions
// in package runtime and reflect, and for those we use call-specific
// metadata recorded by f's caller.

これらの変更もコメントの更新です。traceback_*.cファイルは、Goランタイムがスタックトレースを生成する際のロジックを含んでいます。

変更前のコメントは、可変長引数を持つ関数がruntimeパッケージに存在し、それらの引数サイズ情報は呼び出し元によって記録されることを述べていました。変更後は、この説明に「and reflect」が追加されました。これは、reflectパッケージが生成する関数(methodValueCall, makeFuncStubなど)も同様に可変長引数を持ち、その引数サイズ情報が呼び出しサイトで記録されるようになったことを、ランタイムのスタックトレースロジックの文脈で明確にしています。これにより、ランタイム開発者がスタックアンワインドの挙動を理解する上で、より正確な情報が提供されます。

関連リンク

  • Go Issue #5954: reflect.methodValueCall and makeFuncStub unwinding
  • Go CL 12163043: cmd/gc: record argument size for all indirect function calls
    • https://golang.org/cl/12163043 (これはコミットメッセージに記載されているリンクですが、現在のGoのコードレビューシステムでは古いCL番号は直接アクセスできない場合があります。しかし、コミットハッシュからGitHubで確認できます。)

参考にした情報源リンク

  • Goのソースコード(上記コミットの変更点)
  • GoのIssueトラッカー(Issue #5954)
  • Goのコードレビューシステム(CL 12163043)
  • Goのreflectパッケージに関する公式ドキュメントや解説記事(一般的なreflectの動作理解のため)
  • Goのランタイムとスタックアンワインドに関する技術記事(一般的なスタックアンワインドの概念理解のため)