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

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

このコミットは、Goコンパイラ(cmd/5g, cmd/6g, cmd/8g)がruntimeパッケージ内の関数呼び出し、特に可変引数を持つC関数への呼び出しを処理する方法に関する変更を導入しています。具体的には、これらの呼び出しの前後で引数のサイズ情報を付加するメカニズムを追加し、setmaxargによる従来の引数サイズ設定を置き換えています。

コミット

commit 7b3c8b7ac8d16239ca7768b2b846ce4492232b4f
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jul 16 16:25:10 2013 -0400

    cmd/5g, cmd/6g, cmd/8g: insert arg size annotations on runtime calls
    
    If calling a function in package runtime, emit argument size
    information around the call in case the call is to a variadic C function.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/11371043

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

https://github.com/golang/go/commit/7b3c8b7ac8d16239ca7768b2b846ce4492232b4f

元コミット内容

cmd/5g, cmd/6g, cmd/8g: insert arg size annotations on runtime calls

このコミットは、runtimeパッケージ内の関数を呼び出す際に、特にその呼び出しが可変引数を持つC関数である場合に備えて、呼び出しの周囲に引数サイズ情報を出力するようにします。

変更の背景

Go言語のランタイムは、ガベージコレクションやスタック管理など、低レベルの操作を効率的に行う必要があります。これらの操作では、スタック上のデータのレイアウト、特に各関数の引数が占めるメモリサイズを正確に把握することが不可欠です。

従来のGoコンパイラ(cmd/5g, cmd/6g, cmd/8g)は、関数の引数サイズを追跡するためにsetmaxargのようなメカニズムを使用していました。しかし、C言語で実装された可変引数関数(printfのような関数)をGoのruntimeパッケージから呼び出す場合、その引数の数はコンパイル時には確定しません。このような動的な引数を持つ関数呼び出しでは、従来の静的な引数サイズ追跡方法では不十分であり、ガベージコレクタがスタックを正確にスキャンしたり、スタックの巻き戻し(unwinding)を正しく行ったりする上で問題が生じる可能性がありました。

このコミットの目的は、runtimeパッケージ内の関数呼び出し、特に可変引数を持つC関数への呼び出しに対して、より堅牢な引数サイズ追跡メカニズムを導入することです。これにより、ランタイムがスタックの状態をより正確に把握し、ガベージコレクションの正確性と効率性を向上させることが可能になります。

前提知識の解説

Goコンパイラ (cmd/5g, cmd/6g, cmd/8g)

このコミットが対象としているcmd/5gcmd/6gcmd/8gは、Go言語の初期のコンパイラ群です。

  • cmd/5g: ARMアーキテクチャ(GOARCH=arm)向けのコンパイラ。
  • cmd/6g: x86-64アーキテクチャ(GOARCH=amd64)向けのコンパイラ。
  • cmd/8g: x86アーキテクチャ(GOARCH=386)向けのコンパイラ。

これらのコンパイラは、Goのソースコードを各アーキテクチャの機械語に変換する役割を担っていました。現在では、これらのコンパイラは単一のcmd/compileツールに統合されていますが、このコミットが作成された時点では個別のコンパイラとして存在していました。コンパイラは、コード生成の過程で、関数呼び出しのスタックフレームのレイアウトや、引数の渡し方などを決定します。

runtimeパッケージ

runtimeパッケージは、Go言語の実行環境(ランタイム)のコア部分を実装しています。これには、ガベージコレクタ、スケジューラ、メモリ管理、ゴルーチン管理、低レベルのシステムコールインターフェースなどが含まれます。runtimeパッケージのコードは、Go言語とアセンブリ言語、そして一部C言語で書かれています。Goプログラムが実行される際には、このruntimeパッケージが常に動作し、プログラムのライフサイクルを管理します。

可変引数C関数 (Variadic C function)

C言語における可変引数関数は、引数の数が固定されていない関数です。例えば、printf関数は、フォーマット文字列に加えて任意の数の引数を受け取ることができます。

int printf(const char *format, ...);

このような関数を呼び出す際、呼び出し側はスタックに引数を積みますが、その引数の総サイズはコンパイル時には確定せず、実行時に渡される引数の数と型によって変化します。GoのランタイムがC言語で書かれた可変引数関数を呼び出す場合、Goのガベージコレクタやスタックウォーカーがスタックを正確に解析するためには、これらの動的な引数サイズを正確に知る必要があります。

PCDATA (Program Counter Data)

PCDATAは、Goのバイナリに埋め込まれるメタデータの一種で、プログラムカウンタ(PC)の値に基づいて変化する情報を提供します。これは主にガベージコレクションやスタックトレースのために使用されます。PCDATAは、特定のPC値(命令アドレス)に対応するスタック上のポインタの位置や、スタックフレームのサイズなどの情報を提供することで、ランタイムが実行中のゴルーチンのスタックを正確に解析できるようにします。

PCDATAにはいくつかの種類があり、このコミットで言及されているPCDATA_ArgSizeはその一つです。

  • PCDATA_ArgSize: このPCDATAは、関数呼び出しの引数領域のサイズを示します。特に可変引数関数や、スタック上の引数レイアウトが複雑な場合に、ガベージコレクタがスタックをスキャンする際に、どの範囲が引数領域であるかを正確に判断するために利用されます。

APCDATA命令

APCDATAは、Goコンパイラが生成するアセンブリ命令の一種で、PCDATAを埋め込むために使用されます。これは、特定のプログラムカウンタのポイントで、指定されたPCDATAの値を設定する役割を果たします。

技術的詳細

このコミットの核心は、Goコンパイラがruntimeパッケージ内の関数を呼び出す際に、その呼び出しの前後で引数サイズに関するPCDATA情報を挿入する点にあります。

  1. 引数サイズの計算: ggen.c内のginscall関数が変更され、関数fruntimepkgruntimeパッケージ)に属するか、または特定のプロシージャタイプ(proc == 1またはproc == 2)である場合に、引数サイズargを計算します。

    • f->type->argwid: これは関数の引数全体の幅(バイト単位)を示します。
    • proc == 1またはproc == 2の場合、追加で3*widthptr(5g)または2*widthptr(6g, 8g)が加算されます。widthptrはポインタのサイズ(通常4バイトまたは8バイト)であり、これは特定の呼び出し規約や、レジスタ渡しされる引数など、スタック上に直接配置されない追加の引数領域を考慮している可能性があります。
  2. gargsize関数の導入: gsubr.cgargsize関数が新しく追加されました。この関数は、引数として渡されたサイズsizeを受け取り、APCDATA命令を生成します。

    void
    gargsize(int32 size)
    {
        Node n1, n2;
        nodconst(&n1, types[TINT32], PCDATA_ArgSize);
        nodconst(&n2, types[TINT32], size);
        gins(APCDATA, &n1, &n2);
    }
    

    このコードは、PCDATA_ArgSizeという定数と計算された引数サイズsizeを引数としてAPCDATA命令を生成します。これにより、コンパイラは生成されるバイナリに「現在のPC位置での引数サイズはsizeである」というメタデータを埋め込みます。

  3. ginscallでのgargsizeの利用: ggen.cginscall関数内で、runtimeパッケージの関数呼び出しの直前にgargsize(arg)が呼び出され、呼び出しの直後にgargsize(-1)が呼び出されます。

    • gargsize(arg): 関数呼び出しの直前に、その呼び出しの引数サイズをランタイムに通知します。これにより、ランタイムは関数が実行される間のスタック上の引数領域の正確なサイズを把握できます。
    • gargsize(-1): 関数呼び出しの直後に-1を渡してgargsizeを呼び出します。これは、引数サイズ情報の「リセット」または「無効化」を意味すると考えられます。つまり、この関数呼び出しの引数サイズ情報は、このポイントで適用が終了することを示唆しています。これにより、次の命令からは通常のスタックフレームの解釈に戻ることができます。
  4. setmaxargの削除: cgen_callinterおよびcgen_call関数からsetmaxargの呼び出しが削除されました。これは、新しいgargsizeメカニズムが、従来のsetmaxargが担っていた引数サイズ情報の提供をより正確かつ動的に行うため、setmaxargが不要になったことを示しています。setmaxargは、おそらく関数の最大引数サイズを静的に設定するものでしたが、可変引数関数には対応できませんでした。

  5. peep.cの変更: peep.c(peephole optimizer)のcopyu関数にAPCDATAが追加されています。これは、peephole最適化の過程でAPCDATA命令が正しく扱われるようにするためです。APCDATA命令は、コードの実行には直接影響しませんが、ランタイムのメタデータとして重要であるため、最適化によって誤って削除されたり変更されたりしないように、特別に処理される必要があります。

この変更により、Goのランタイム、特にガベージコレクタは、runtimeパッケージ内の関数呼び出し(特に可変引数C関数への呼び出し)のスタックフレームをより正確に解析できるようになります。これにより、ガベージコレクションの正確性が向上し、メモリリークやクラッシュのリスクが低減されます。

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

このコミットの主要な変更は、以下のファイルに集中しています。

  • src/cmd/{5g,6g,8g}/gg.h: gargsize関数のプロトタイプ宣言が追加されました。
  • src/cmd/{5g,6g,8g}/ggen.c:
    • ginscall関数が変更され、runtimeパッケージの関数呼び出しの前後でgargsizeを呼び出すロジックが追加されました。
    • cgen_callinterおよびcgen_call関数からsetmaxargの呼び出しが削除されました。
  • src/cmd/{5g,6g,8g}/gsubr.c:
    • ../../pkg/runtime/funcdata.hがインクルードされました。
    • gargsize関数の実装が追加されました。この関数はAPCDATA命令を生成し、引数サイズ情報を埋め込みます。
  • src/cmd/5g/peep.c: copyu関数内のswitch文にAPCDATAケースが追加されました。

src/cmd/5g/ggen.c (抜粋)

--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -73,9 +73,23 @@ fixautoused(Prog* p)
 void
 ginscall(Node *f, int proc)
 {
+	int32 arg;
 	Prog *p;
 	Node n1, r, r1, con;
 
+	if(f->type != T)
+		setmaxarg(f->type); // この行は変更前のもので、変更後は削除されるか、新しいロジックに置き換えられる
+
+	arg = -1;
+	if(f->type != T && ((f->sym != S && f->sym->pkg == runtimepkg) || proc == 1 || proc == 2)) {
+		arg = f->type->argwid;
+		if(proc == 1 || proc == 2)
+			arg += 3*widthptr;
+	}
+
+	if(arg != -1)
+		gargsize(arg);
+
 	switch(proc) {
 	default:
 	fatal("ginscall: bad proc %d", proc);
@@ -170,6 +184,9 @@ ginscall(Node *f, int proc)
 		}
 		break;
 	}
+	
+	if(arg != -1)
+		gargsize(-1);
 }
 
 /*
@@ -239,14 +256,11 @@ cgen_callinter(Node *n, Node *res, int proc)
 		p->from.type = D_CONST;	// REG = &(20+offset(REG)) -- i.tab->fun[f]
 	}
 
-	// BOTCH nodr.type = fntype;
 	nodr.type = n->left->type;
 	ginscall(&nodr, proc);
 
 	regfree(&nodr);
 	regfree(&nodo);
-
-	setmaxarg(n->left->type); // この行が削除される
 }
 
 /*
@@ -274,8 +288,6 @@ cgen_call(Node *n, int proc)
 	genlist(n->list);		// assign the args
 	t = n->left->type;
 
-	setmaxarg(t); // この行が削除される
-
 	// call tempname pointer
 	if(n->left->ullman >= UINF) {
 		regalloc(&nod, types[tptr], N);

src/cmd/5g/gsubr.c (抜粋)

--- a/src/cmd/5g/gsubr.c
+++ b/src/cmd/5g/gsubr.c
@@ -31,6 +31,7 @@
 #include <u.h>
 #include <libc.h>
 #include "gg.h"
+#include "../../pkg/runtime/funcdata.h" // 新しく追加されたインクルード
 
 // TODO(rsc): Can make this bigger if we move
 // the text segment up higher in 5l for all GOOS.
@@ -209,6 +210,16 @@ ggloblnod(Node *nam)
 		p->reg |= NOPTR;
 }
 
+void
+gargsize(int32 size) // 新しく追加された関数
+{
+	Node n1, n2;
+	
+	nodconst(&n1, types[TINT32], PCDATA_ArgSize);
+	nodconst(&n2, types[TINT32], size);
+	gins(APCDATA, &n1, &n2);
+}
+
 void
 ggloblsym(Sym *s, int32 width, int dupok, int rodata)
 {

コアとなるコードの解説

ginscall関数の変更

ginscall関数は、Goコンパイラが関数呼び出しを生成する際の中心的な役割を担っています。このコミットでは、特にruntimeパッケージ内の関数呼び出しに対して、以下のロジックが追加されました。

  1. 引数サイズの決定:

    int32 arg;
    // ...
    arg = -1;
    if(f->type != T && ((f->sym != S && f->sym->pkg == runtimepkg) || proc == 1 || proc == 2)) {
        arg = f->type->argwid;
        if(proc == 1 || proc == 2)
            arg += 3*widthptr; // 6g, 8gでは 2*widthptr
    }
    

    ここで、呼び出される関数fruntimepkgruntimeパッケージ)に属するか、または特定のプロシージャタイプ(proc == 1またはproc == 2)である場合に、その関数の引数サイズargが計算されます。f->type->argwidは、その関数の型定義から得られる引数の合計サイズです。proc == 1proc == 2は、特定のランタイム呼び出し(例えば、Goルーチンの開始やCgo呼び出しなど)を示す内部的なフラグであると考えられます。これらのケースでは、追加のポインタサイズが加算されることで、可変引数や特殊な呼び出し規約によるスタックレイアウトの差異が考慮されます。

  2. gargsizeによるPCDATAの挿入:

    if(arg != -1)
        gargsize(arg);
    // ... (実際の関数呼び出しのコード生成) ...
    if(arg != -1)
        gargsize(-1);
    

    計算されたarg-1でない場合(つまり、runtimeパッケージの関数呼び出しであると判断された場合)、実際の関数呼び出しのコードが生成される直前にgargsize(arg)が呼び出されます。これにより、この関数呼び出しの引数サイズ情報がPCDATA_ArgSizeとしてバイナリに埋め込まれます。 そして、関数呼び出しのコード生成が完了した後、gargsize(-1)が呼び出されます。これは、この引数サイズ情報のスコープが終了したことをランタイムに通知する役割を果たします。これにより、ランタイムはスタックの解析を継続する際に、この特定の呼び出しの引数サイズ情報がもはや有効でないことを認識できます。

gargsize関数の実装

gargsize関数は、gsubr.cに新しく追加されたヘルパー関数です。

void
gargsize(int32 size)
{
    Node n1, n2;
    nodconst(&n1, types[TINT32], PCDATA_ArgSize);
    nodconst(&n2, types[TINT32], size);
    gins(APCDATA, &n1, &n2);
}

この関数は、PCDATA_ArgSizeという定数と、引数として渡されたsize(引数のバイトサイズ)をAPCDATA命令のオペランドとして使用します。gins関数は、指定されたオペコード(APCDATA)とオペランド(n1, n2)を持つアセンブリ命令を生成します。これにより、コンパイラは実行可能バイナリ内の特定のプログラムカウンタ位置に、引数サイズに関するメタデータを埋め込むことができます。このメタデータは、ガベージコレクタがスタックをスキャンする際に、どのメモリ領域が引数として使用されているかを正確に判断するために利用されます。

setmaxargの削除

cgen_callintercgen_callからsetmaxargの呼び出しが削除されたことは、この新しいPCDATA_ArgSizeベースのメカニズムが、従来の引数サイズ追跡方法を完全に置き換えることを意味します。setmaxargは、おそらく関数の最大引数サイズをコンパイル時に設定するものでしたが、可変引数関数のような動的なシナリオには対応できませんでした。新しいアプローチは、より柔軟で正確な引数サイズ情報を提供します。

これらの変更により、Goのコンパイラは、runtimeパッケージ内の関数呼び出し、特に可変引数を持つC関数への呼び出しに対して、ガベージコレクタがスタックを正確に解析するために必要な引数サイズ情報を、実行時に動的に提供できるようになりました。これは、Goランタイムの堅牢性と正確性を向上させる上で重要な改善です。

関連リンク

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されているhttps://golang.org/cl/11371043は、このGerritの変更リストへのリンクです。)
  • GoのPCDATAに関する議論やドキュメント (Goの内部実装に関する情報源):
    • "Go's runtime and the garbage collector" (Goのガベージコレクタに関する一般的な情報源)
    • "A Guide to the Go Compiler" (Goコンパイラの内部構造に関する情報源)
    • Goのソースコード内のsrc/runtime/funcdata.hや関連ファイル。
    • GoのIssue Tracker (Goのバグや機能リクエストに関する議論): https://github.com/golang/go/issues
    • Goのメーリングリスト (golang-devなど): https://groups.google.com/g/golang-dev
    • GoのPCDATAに関する具体的な情報を見つけるためには、Goのソースコードを直接参照するか、Goの内部実装に関するブログ記事やプレゼンテーションを探すのが最も効果的です。特にPCDATA_ArgSizeのような定数は、src/runtime/funcdata.hで定義されています。