[インデックス 16785] ファイルの概要
このコミットは、Goランタイムのアセンブリ関数において、引数のサイズを明示的に記録するメカニズムを導入するものです。これにより、ガベージコレクション(GC)やスタックトレースの生成時に、アセンブリで書かれた関数のスタックフレームをより正確に解析できるようになります。特に、asm_386.s, asm_amd64.s, asm_arm.sといった各アーキテクチャ固有のアセンブリファイルと、新たに導入されたfuncdata.hが変更されています。
コミット
commit 9ddfb64365230db8f3d3f3d3e7ab7384cade2cb1
Author: Russ Cox <rsc@golang.org>
Date: Tue Jul 16 16:24:09 2013 -0400
runtime: record argument size in assembly functions
I have not done the system call stubs in sys_*.s.
I hope to avoid that, because those do not block, so those
frames will not appear in stack traces during garbage
collection.
R=golang-dev, dvyukov, khr
CC=golang-dev
https://golang.org/cl/11360043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9ddfb64365230db8f3d3f3d3e7ab7384cade2cb1
元コミット内容
Goランタイムのアセンブリ関数において、引数のサイズを記録するように変更します。
sys_*.sにあるシステムコールスタブについては、ブロックしないためガベージコレクション中のスタックトレースには現れないことから、今回の変更の対象外としています。
変更の背景
Goのガベージコレクタは、実行中のプログラムのスタックをスキャンし、どの変数がまだ使用されているか(ライブであるか)を判断する必要があります。このプロセスを正確に行うためには、各関数のスタックフレームのレイアウト、特に引数とローカル変数がスタック上で占める領域のサイズを正確に知る必要があります。
Goのコンパイラが生成するGo言語の関数については、この情報はコンパイラによって生成される関数メタデータ(FUNCDATA)に含まれています。しかし、Goランタイムの低レベルな部分や、特定のパフォーマンス要件を持つ関数は、Goアセンブリ(Plan 9アセンブラの構文に基づく)で直接記述されています。これらのアセンブリ関数は、コンパイラが自動的に生成するメタデータを持たないため、ガベージコレクタがスタックを正確にスキャンする上で課題がありました。
このコミットの背景には、アセンブリ関数が呼び出された際に、その関数の引数がスタック上でどれだけの領域を占めているかをガベージコレクタに伝える必要性がありました。これにより、ガベージコレクタはスタック上のポインタを正確に識別し、誤って解放したり、誤ったポインタを追跡したりするのを防ぐことができます。
前提知識の解説
- Goアセンブリ (Plan 9アセンブラ): Go言語のランタイムや標準ライブラリの一部は、パフォーマンスが重要な部分や、OSとの低レベルなインタラクションが必要な部分で、Goアセンブリ(Plan 9アセンブラの構文をベースにしたもの)で記述されています。これは一般的なx86/x64アセンブリとは異なる独自の構文を持ちます。
- スタックフレーム: 関数が呼び出されると、その関数に必要な情報(引数、ローカル変数、戻りアドレスなど)を格納するために、スタック上に領域が確保されます。この領域をスタックフレームと呼びます。
- ガベージコレクション (GC): Goのランタイムは、不要になったメモリを自動的に解放するガベージコレクタを備えています。GCは、プログラムが現在使用しているメモリ(ライブオブジェクト)を識別し、それ以外のメモリを解放します。スタック上のポインタを正確に追跡することは、GCがライブオブジェクトを正しく識別するために不可欠です。
- スタックトレース: プログラムの実行中にエラーが発生したり、デバッグのために現在の実行パスを調べたりする際に、関数呼び出しの履歴(スタックトレース)が生成されます。スタックトレースを正確に生成するためには、各スタックフレームの境界と、そのフレームがどの関数に属しているかを正確に知る必要があります。
TEXTディレクティブ: Goアセンブリにおいて、関数の定義を開始するために使用されます。通常、TEXT symbol(SB), flags, $framesizeの形式で記述されます。symbol(SB): 関数のシンボル名。SBはStatic Baseで、グローバルシンボルであることを示します。flags: 関数の特性を示すフラグ。$framesize: 関数のスタックフレームサイズ(ローカル変数とレジスタ保存領域の合計)。このコミットでは、この部分に加えて引数サイズに関する情報が追加されます。
PCDATA(PC Data): Goバイナリ内のプログラムカウンタ(PC)に関連付けられたデータです。ガベージコレクタやデバッガがスタックをスキャンする際に使用するメタデータの一種です。特定のPC値(命令アドレス)において、スタックの状態やレジスタの内容に関する情報を提供します。FUNCDATA(Function Data): 関数全体に関連付けられたデータです。関数の引数や戻り値の型情報、ローカル変数のポインタ情報など、GCがスタックをスキャンするために必要な情報が含まれます。ARGSIZEマクロ: このコミットで導入される新しいアセンブリマクロです。PCDATAディレクティブを使用して、現在のPCにおける関数の引数サイズを記録するために使用されます。
技術的詳細
このコミットの主要な目的は、Goアセンブリで書かれた関数が、その引数サイズに関するメタデータをGoランタイムに提供できるようにすることです。これは、ガベージコレクタがスタックを正確にスキャンし、ライブポインタを識別するために不可欠です。
変更の核心は、GoアセンブリのTEXTディレクティブの拡張と、新しいARGSIZEマクロの導入にあります。
-
TEXTディレクティブの変更: 以前のTEXTディレクティブは、TEXT symbol(SB), flags, $framesizeの形式でした。ここで$framesizeはローカル変数とレジスタ保存領域の合計サイズを示していました。 このコミットでは、TEXTディレクティブのフレームサイズ指定が$framesize-argsizeの形式に変更されます。$framesize: 関数が使用するスタックフレームの合計サイズ(ローカル変数とレジスタ保存領域)。argsize: 関数が呼び出し元から受け取る引数の合計サイズ。 この変更により、TEXTディレクティブ自体が、その関数のスタックフレームサイズと引数サイズの両方を明示的に示すようになります。例えば、TEXT runtime·gosave(SB), 7, $0がTEXT runtime·gosave(SB), 7, $0-4に変更されています。これは、gosave関数が4バイトの引数を受け取ることを示しています。
-
funcdata.hの導入とARGSIZEマクロ: 新しくsrc/pkg/runtime/funcdata.hが追加されました。このヘッダファイルは、PCDATAおよびFUNCDATA命令のIDを定義します。 特に重要なのは、#define ARGSIZE(n) PCDATA $PCDATA_ArgSize, $nというマクロの定義です。PCDATA_ArgSizeは、引数サイズに関するPCDATAであることを示すIDです。ARGSIZE(n)マクロは、PCDATA命令を生成し、現在のプログラムカウンタ(PC)において引数サイズがnであることを記録します。 このマクロは、アセンブリ関数内で、引数サイズが動的に変化する可能性がある場所(例:runtime·newprocの呼び出し前後)で使用されます。
なぜこれが重要なのか?
- 正確なスタックスキャン: Goのガベージコレクタは、スタック上のポインタを正確に識別するために、各スタックフレームの境界と、そのフレーム内のどの領域が引数やローカル変数として使用されているかを知る必要があります。アセンブリ関数が引数サイズを明示的に記録することで、GCはこれらの関数がスタック上に残した引数を正確にスキップし、その下の呼び出し元のスタックフレームを正しく解析できます。
- スタックトレースの改善: デバッグ時やパニック発生時に生成されるスタックトレースは、各関数の呼び出し元と呼び出し先、そしてその間のスタックの状態を正確に反映している必要があります。引数サイズの情報が正確に記録されることで、スタックトレースの信頼性が向上します。
- ポータビリティと保守性: アセンブリコードは通常、特定のアーキテクチャに依存しますが、このようなメタデータ記録の仕組みを導入することで、ランタイムのコアロジック(GCなど)がアセンブリコードの詳細な実装に過度に依存することなく、より抽象的なレベルでスタックを扱えるようになります。
具体的な変更点:
src/pkg/runtime/asm_386.s,src/pkg/runtime/asm_amd64.s,src/pkg/runtime/asm_arm.sの各ファイルで、多くのTEXTディレクティブが$framesize-argsizeの形式に変更されています。- 特に、
runtime·newprocのような関数呼び出しの前後で、ARGSIZE(n)とARGSIZE(-1)が挿入されています。ARGSIZE(n):newprocを呼び出す直前に、その呼び出しの引数サイズをnとして記録します。ARGSIZE(-1):newprocの呼び出しから戻った後、引数サイズが不明であることを示します(または、呼び出し元の引数サイズに戻ることを示唆)。これは、newprocが新しいゴルーチンを作成し、スタックの状態が変化するため、その後のスタックフレームの解釈に影響を与えないようにするためと考えられます。
この変更は、Goランタイムの堅牢性とデバッグ能力を向上させるための重要なステップです。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
src/pkg/runtime/asm_386.s: x86アーキテクチャ向けのアセンブリ関数。src/pkg/runtime/asm_amd64.s: AMD64アーキテクチャ向けのアセンブリ関数。src/pkg/runtime/asm_arm.s: ARMアーキテクチャ向けのアセンブリ関数。src/pkg/runtime/funcdata.h: 新規追加されたファイルで、PCDATAおよびFUNCDATAのIDとARGSIZEマクロを定義。src/pkg/runtime/memclr_arm.s,src/pkg/runtime/memmove_386.s,src/pkg/runtime/memmove_amd64.s,src/pkg/runtime/memmove_arm.s,src/pkg/runtime/race_amd64.s: これらのファイルでも、TEXTディレクティブのフレームサイズ指定が更新されています。
具体的なコード変更の例 (src/pkg/runtime/asm_386.s から抜粋):
--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -95,7 +96,9 @@ ok:
// create a new goroutine to start program
PUSHL $runtime·main·f(SB) // entry
PUSHL $0 // arg size
++ ARGSIZE(8)
CALL runtime·newproc(SB)
++ ARGSIZE(-1)
POPL AX
POPL AX
@@ -108,11 +111,11 @@ ok:
DATA runtime·main·f+0(SB)/4,$runtime·main(SB)
GLOBL runtime·main·f(SB),8,$4
--TEXT runtime·breakpoint(SB),7,$0
+-TEXT runtime·breakpoint(SB),7,$0-0
INT $3
RET
--TEXT runtime·asminit(SB),7,$0
+-TEXT runtime·asminit(SB),7,$0-0
// Linux and MinGW start the FPU in extended double precision.
// Other operating systems use double precision.
// Change to double precision to match them,
@@ -128,7 +131,7 @@ TEXT runtime·asminit(SB),7,$0
// void gosave(Gobuf*)
// save state in Gobuf; setjmp
--TEXT runtime·gosave(SB), 7, $0
+-TEXT runtime·gosave(SB), 7, $0-4
MOVL 4(SP), AX // gobuf
LEAL 4(SP), BX // caller's SP
MOVL BX, gobuf_sp(AX)
新規ファイル src/pkg/runtime/funcdata.h の内容:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file defines the IDs for PCDATA and FUNCDATA instructions
// in Go binaries. It is included by both C and assembly, so it must
// be written using #defines. It is included by the runtime package
// as well as the compilers.
#define PCDATA_ArgSize 0
// To be used in assembly.
#define ARGSIZE(n) PCDATA $PCDATA_ArgSize, $n
コアとなるコードの解説
-
funcdata.h: このファイルは、Goバイナリ内で使用されるPCDATAおよびFUNCDATA命令の識別子を定義します。PCDATA_ArgSizeは、PCDATA命令が引数サイズに関する情報であることを示すための定数(ID)です。ARGSIZE(n)マクロは、アセンブリコード内でPCDATA $PCDATA_ArgSize, $nという命令を生成するためのショートカットです。これは、現在のプログラムカウンタ(PC)の時点で、スタック上の引数サイズがnバイトであることをランタイムに伝えます。 -
アセンブリファイルの変更:
TEXTディレクティブの変更: 多くのTEXTディレクティブのフレームサイズ指定が$framesize-argsizeの形式に変更されています。 例:TEXT runtime·gosave(SB), 7, $0がTEXT runtime·gosave(SB), 7, $0-4に変更。 これは、gosave関数が4バイトの引数を受け取ることを示しています。GoアセンブリのTEXTディレクティブの第3引数は、関数のスタックフレームサイズ(ローカル変数とレジスタ保存領域)を示しますが、このコミットでは、その値から引数サイズを引いた値が指定されることで、間接的に引数サイズをランタイムに伝えています。ランタイムは、この情報と、関数呼び出し規約に基づいて、実際の引数サイズを計算できます。ARGSIZEマクロの挿入:runtime·newprocのような関数呼び出しの前後で、ARGSIZE(n)とARGSIZE(-1)が挿入されています。 例:PUSHL $runtime·main·f(SB) // entry PUSHL $0 // arg size ARGSIZE(8) // ここで引数サイズが8バイトであることを記録 CALL runtime·newproc(SB) ARGSIZE(-1) // 呼び出しから戻った後、引数サイズが不明であることを示すruntime·newprocは新しいゴルーチンを作成する関数であり、その呼び出し前後でスタックの状態が変化します。ARGSIZE(8)は、runtime·newprocが呼び出される際に、スタック上に8バイトの引数(runtime·main·f(SB)と0)が積まれることを明示的に示しています。ARGSIZE(-1)は、newprocからの戻り後、スタックの状態が変化したため、以前の引数サイズ情報が無効になることを示唆しています。これは、ガベージコレクタがスタックをスキャンする際に、これらの特定のポイントでのスタックフレームの解釈を調整するために使用されます。
これらの変更により、Goランタイムはアセンブリ関数がスタック上でどのように引数を扱っているかを正確に把握できるようになり、ガベージコレクションの精度とスタックトレースの信頼性が向上します。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/
- Goアセンブリに関するドキュメント: https://go.dev/doc/asm
- Goのガベージコレクションに関する情報: https://go.dev/doc/gc-guide
参考にした情報源リンク
- Goのソースコード (GitHub): https://github.com/golang/go
- Go Code Review (Gerrit): https://go-review.googlesource.com/
- このコミットのChange-ID:
https://golang.org/cl/11360043(コミットメッセージに記載) - GoのPCDATAとFUNCDATAに関する議論やドキュメント (GoのIssueトラッカーやメーリングリストで検索すると見つかる可能性があります)
- Plan 9アセンブラの構文に関する情報 (Goアセンブリの理解に役立ちます)
- Goのガベージコレクタの内部動作に関する技術記事や論文