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

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

このコミットは、Goコンパイラ(cmd/gc)において、ローカル変数およびグローバル変数に対する明示的な型情報をアセンブリコードに埋め込む変更を導入します。これにより、デバッガやガベージコレクタがより正確な型情報を利用できるようになり、特にガベージコレクタの正確なヒープ回収におけるバグを修正することを目的としています。

コミット

commit 1d5dc4fd481ae5ccebe1be0091a88b9343fe0904
Author: Russ Cox <rsc@golang.org>
Date:   Mon Feb 25 12:13:47 2013 -0500

    cmd/gc: emit explicit type information for local variables

    The type information is (and for years has been) included
    as an extra field in the address chunk of an instruction.
    Unfortunately, suppose there is a string at a+24(FP) and
    we have an instruction reading its length. It will say:

            MOVQ x+32(FP), AX

    and the type of *that* argument is int (not slice), because
    it is the length being read. This confuses the picture seen
    by debuggers and now, worse, by the garbage collector.

    Instead of attaching the type information to all uses,
    emit an explicit list of TYPE instructions with the information.
    The TYPE instructions are no-ops whose only role is to
    provide an address to attach type information to.

    For example, this function:

            func f(x, y, z int) (a, b string) {
                    return
            }

    now compiles into:

            --- prog list "f" ---
            0000 (/Users/rsc/x.go:3) TEXT    f+0(SB),$0-56
            0001 (/Users/rsc/x.go:3) LOCALS  ,
            0002 (/Users/rsc/x.go:3) TYPE    x+0(FP){int},$8
            0003 (/Users/rsc/x.go:3) TYPE    y+8(FP){int},$8
            0004 (/Users/rsc/x.go:3) TYPE    z+16(FP){int},$8
            0005 (/Users/rsc/x.go:3) TYPE    a+24(FP){string},$16
            0006 (/Users/rsc/x.go:3) TYPE    b+40(FP){string},$16
            0007 (/Users/rsc/x.go:3) MOVQ    $0,b+40(FP)
            0008 (/Users/rsc/x.go:3) MOVQ    $0,b+48(FP)
            0009 (/Users/rsc/x.go:3) MOVQ    $0,a+24(FP)
            0010 (/Users/rsc/x.go:3) MOVQ    $0,a+32(FP)
            0011 (/Users/rsc/x.go:4) RET     ,

    The { } show the formerly hidden type information.
    The { } syntax is used when printing from within the gc compiler.
    It is not accepted by the assemblers.

    The same type information is now included on global variables:

    0055 (/Users/rsc/x.go:15) GLOBL   slice+0(SB){[]string},$24(AL*0)

    This more accurate type information fixes a bug in the
    garbage collector's precise heap collection.

    The linker only cares about globals right now, but having the
    local information should make things a little nicer for Carl
    in the future.

    Fixes #4907.

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

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

https://github.com/golang/go/commit/1d5dc4fd481ae5ccebe1be0091a88b9343fe0904

元コミット内容

Goコンパイラ(cmd/gc)は、これまで命令のアドレスチャンクの追加フィールドとして型情報を含んでいました。しかし、この方法では、例えば文字列の長さを読み取る命令(MOVQ x+32(FP), AX)の場合、その引数の型がint(文字列のスライスではない)と解釈されてしまい、デバッガやガベージコレクタが誤った型情報を認識する問題がありました。特に、ガベージコレクタの正確なヒープ回収においてバグを引き起こしていました。

変更の背景

この変更の主な背景は、Goのガベージコレクタがより正確なヒープ回収(Precise GC)を行うために、実行時の型情報を正確に把握する必要があったことです。従来の型情報の埋め込み方では、特定のケースで型が誤って解釈され、ガベージコレクタがヒープ上のオブジェクトを正しく識別できない可能性がありました。これにより、メモリリークや誤ったメモリ解放といった問題が発生するリスクがありました。

また、デバッガが変数の型を正確に表示するためにも、より明示的な型情報が必要とされていました。このコミットは、Go issue #4907("cmd/gc: precise GC needs type info for local variables")の修正として行われました。

前提知識の解説

Goコンパイラの構造

Goコンパイラは、複数のコンポーネントから構成されています。

  • cmd/gc: Go言語のフロントエンドコンパイラであり、Goのソースコードを中間表現に変換し、最終的にアセンブリコードを生成します。このコミットの主要な変更対象です。
  • cmd/5g, cmd/6g, cmd/8g: それぞれ異なるアーキテクチャ(5g: ARM, 6g: AMD64, 8g: x86)に対応するGoコンパイラのバックエンドです。gcが生成した中間表現を受け取り、各アーキテクチャ固有のアセンブリコードを生成します。
  • cmd/5l, cmd/6l, cmd/8l: それぞれ異なるアーキテクチャに対応するGoリンカです。コンパイラが生成したオブジェクトファイルをリンクし、実行可能ファイルを生成します。

Goのアセンブリ言語の基本

Goのアセンブリ言語は、Plan 9アセンブラの文法に基づいています。

  • TEXT: 関数の開始を宣言します。
  • LOCALS: 関数のローカル変数領域に関する情報を示します。
  • MOVQ: 64ビットの値を移動する命令です。
  • RET: 関数のリターン命令です。
  • GLOBL: グローバル変数を宣言します。
  • FP (Frame Pointer): 現在のスタックフレームの基準点を示すレジスタです。ローカル変数や引数はFPからのオフセットでアクセスされます(例: x+0(FP))。
  • SB (Static Base): 静的データセグメントの基準点を示すレジスタです。グローバル変数はSBからのオフセットでアクセスされます(例: f+0(SB))。
  • {} 構文: Goコンパイラの内部で型情報を表示するために使用される構文で、アセンブラでは直接解釈されません。

ガベージコレクション (GC) と Precise GC

ガベージコレクションは、プログラムが不要になったメモリを自動的に解放する仕組みです。Goのガベージコレクタは、ヒープ上のオブジェクトを追跡し、到達不能なオブジェクトを回収します。

  • Precise GC (正確なGC): ヒープ上のすべてのポインタを正確に識別し、それらが指すオブジェクトを正確に追跡できるGCのことです。これにより、誤ってポインタではない値をポインタとして解釈したり、その逆を行ったりする「保守的なGC」で発生しうる問題(メモリリークや誤った回収)を防ぐことができます。Precise GCを実現するためには、実行時にメモリ上のどの位置にポインタが存在し、そのポインタがどの型のオブジェクトを指しているかを正確に知る必要があります。

型情報の役割

  • デバッグ: デバッガがプログラムの実行中に変数の値だけでなく、その型も正確に表示するために型情報が必要です。
  • ガベージコレクション: Precise GCでは、ヒープ上のメモリをスキャンする際に、どの部分がポインタであり、どの部分が非ポインタデータであるかを識別するために型情報が不可欠です。これにより、GCはポインタをたどって到達可能なオブジェクトを正確にマークし、到達不能なオブジェクトを安全に回収できます。

技術的詳細

このコミットの核心は、従来の「命令のアドレスチャンクに型情報を埋め込む」方式から、「明示的なTYPE命令を導入する」方式への変更です。

従来の方式では、例えば文字列の長さを読み取る命令MOVQ x+32(FP), AXの場合、x+32(FP)というアドレスが指すデータは、文字列の長さ(int型)として解釈されていました。しかし、本来x+24(FP)から始まる領域は文字列全体(string型)であり、その一部を読み取っているに過ぎません。このため、デバッガやGCは、x+24(FP)から始まる領域がstring型であるという本来の情報を失い、int型として誤って認識してしまう可能性がありました。これは、特にGCがヒープ上のポインタを正確に識別する上で大きな問題となります。文字列型は内部にポインタ(文字列データへのポインタ)を持つため、そのポインタをGCが認識できないと、文字列データが誤って回収されたり、リークしたりする原因となります。

この問題を解決するため、コミットではATYPEという新しい命令を導入しました。ATYPE命令は、それ自体は実行時に何もしない「no-op」命令ですが、その唯一の役割は、特定のメモリアドレスに型情報を関連付けることです。

例として、以下のGo関数を考えます。

func f(x, y, z int) (a, b string) {
    return
}

この関数は、コミット後のコンパイラによって以下のようなアセンブリコードにコンパイルされます(抜粋)。

0002 (/Users/rsc/x.go:3) TYPE    x+0(FP){int},$8
0003 (/Users/rsc/x.go:3) TYPE    y+8(FP){int},$8
0004 (/Users/rsc/x.go:3) TYPE    z+16(FP){int},$8
0005 (/Users/rsc/x.go:3) TYPE    a+24(FP){string},$16
0006 (/Users/rsc/x.go:3) TYPE    b+40(FP){string},$16

ここで、TYPE命令の後に続く{}内の情報が、そのアドレス(例: x+0(FP))に紐付けられた型情報(例: {int})とサイズ(例: $8)を示しています。これにより、a+24(FP)string型であるという情報が明示的にアセンブリコードに埋め込まれ、GCやデバッガがこの情報を利用できるようになります。

同様に、グローバル変数についても型情報が明示的に埋め込まれるようになりました。

0055 (/Users/rsc/x.go:15) GLOBL   slice+0(SB){[]string},$24(AL*0)

この変更により、ガベージコレクタはヒープ上のポインタをより正確に識別できるようになり、Precise GCの精度が向上し、Go issue #4907で報告されたバグが修正されました。また、将来的にはリンカやデバッガにとっても、この明示的な型情報が役立つことが期待されています。

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

このコミットは、Goコンパイラの複数のコンポーネントにわたる広範な変更を含んでいます。主要な変更は以下のファイル群に見られます。

  • src/cmd/{5,6,8}g/ggen.c: 各アーキテクチャのコード生成器において、ATYPE命令の処理が追加されました。特に、markautousedfixautoused関数でATYPE命令をスキップまたは適切に処理するロジックが追加されています。
  • src/cmd/{5,6,8}g/gsubr.c: グローバル変数の型情報をggloblnod関数で設定するようになりました。また、naddr関数からアドレスチャンクへの型情報の直接的な埋め込みが削除されました。
  • src/cmd/{5,6,8}g/list.c: アセンブリコードのリスト表示において、ATYPE命令によって付加された型情報({}内の情報)を表示するようになりました。
  • src/cmd/{5,6,8}g/peep.c, src/cmd/{5,6,8}g/reg.c: 命令の最適化やレジスタ割り当てを行う際に、ATYPE命令を適切に無視または処理するように変更されました。
  • src/cmd/{5,6,8}l/{5,6,8}.out.h: 新しい命令タイプATYPEが定義されました。
  • src/cmd/{5,6,8}l/obj.c, src/cmd/{5,6,8}l/optab.c: リンカがATYPE命令を認識し、適切に処理するように変更されました。ATYPE命令はリンカにとっては基本的に無視されるべき命令です。
  • src/cmd/gc/closure.c, src/cmd/gc/dcl.c, src/cmd/gc/fmt.c: 型情報の扱い、特に匿名変数や関数の引数・戻り値の命名に関するロジックが調整されました。Node構造体からrealtypeフィールドが削除され、型情報の取得方法がn->typeに統一されました。
  • src/cmd/gc/go.h: Node構造体からrealtypeフィールドが削除され、ggloblnod関数のシグネチャが変更されました。
  • src/cmd/gc/obj.c: グローバル変数の型情報を設定するためにggloblnod関数が呼び出されるようになりました。
  • src/cmd/gc/pgen.c: コード生成の段階で、ローカル変数や引数に対して明示的にATYPE命令を挿入するロジックが追加されました。これがこのコミットの最も重要な変更点の一つです。
  • src/cmd/gc/subr.c: ngotype関数が、n->realtypeではなくn->typeから型情報を取得するように変更されました。
  • src/cmd/gc/typecheck.c: typecheck1関数からn->realtype = n->type;の行が削除されました。
  • src/cmd/gc/walk.c: ヒープへのパラメータの移動に関するロジックで、匿名変数(~で始まる名前)の扱いが調整されました。

コアとなるコードの解説

このコミットの最も重要な変更は、src/cmd/gc/pgen.cにおけるATYPE命令の生成です。

// src/cmd/gc/pgen.c (抜粋)
// ...
	for(l=fn->dcl; l; l=l->next) {
		n = l->n;
		if(n->op != ONAME) // might be OTYPE or OLITERAL
			continue;
		switch(n->class) {
		case PAUTO:
		case PPARAM:
		case PPARAMOUT:
			nodconst(&nod1, types[TUINTPTR], l->n->type->width);
			p = gins(ATYPE, l->n, &nod1);
			p->from.gotype = ngotype(l->n);
			break;
		}
	}
// ...

このコードブロックは、関数の宣言(fn->dcl)をイテレートし、ローカル変数、関数パラメータ、戻り値パラメータ(PAUTO, PPARAM, PPARAMOUT)であるONAMEノードを見つけます。これらの変数に対して、gins(ATYPE, l->n, &nod1)を呼び出すことで、新しいATYPE命令を生成しています。

  • gins(ATYPE, l->n, &nod1): ATYPE命令を生成します。
    • 第一引数 ATYPE: 命令の種類を指定します。
    • 第二引数 l->n: 型情報を付加する対象のノード(変数)です。このノードには、変数の名前やオフセットなどの情報が含まれています。
    • 第三引数 &nod1: 変数のサイズ情報を含むノードです。
  • p->from.gotype = ngotype(l->n): 生成されたATYPE命令のfromフィールドに、ngotype関数を通じて取得した変数の正確な型情報(gotype)を設定します。このgotypeが、アセンブリコードのリスト表示で{}内に表示される情報であり、GCやデバッガが利用する重要なメタデータとなります。

この変更により、コンパイラは各ローカル変数やパラメータの定義時に、その型情報を明示的にアセンブリコードに埋め込むようになりました。これにより、実行時のアドレスと型情報の関連付けがより正確になり、従来の命令のアドレスチャンクに依存する方式での問題が解決されました。

また、src/cmd/gc/subr.cngotype関数の変更も重要です。

// src/cmd/gc/subr.c (抜粋)
// ...
Sym*
ngotype(Node *n)
{
	if(n->type != T)
		return typenamesym(n->type);
	return S;
}
// ...

この変更により、ngotype関数はノードのrealtypeフィールドではなく、typeフィールドから型情報を取得するようになりました。これは、Node構造体からrealtypeフィールドが削除されたことと整合性を保つための変更であり、型情報の取得を一元化する役割を果たします。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Goコンパイラ、アセンブリ言語、ガベージコレクションに関する情報)
  • Goのソースコード (特にsrc/cmd/gc以下のファイル)
  • Go issue #4907の議論 (もしアクセス可能であれば、問題の詳細な背景を理解するのに役立ちます)
  • Plan 9アセンブラのドキュメント (Goのアセンブリ文法の理解に役立ちます)