[インデックス 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/5g
、cmd/6g
、cmd/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
情報を挿入する点にあります。
-
引数サイズの計算:
ggen.c
内のginscall
関数が変更され、関数f
がruntimepkg
(runtime
パッケージ)に属するか、または特定のプロシージャタイプ(proc == 1
またはproc == 2
)である場合に、引数サイズarg
を計算します。f->type->argwid
: これは関数の引数全体の幅(バイト単位)を示します。proc == 1
またはproc == 2
の場合、追加で3*widthptr
(5g)または2*widthptr
(6g, 8g)が加算されます。widthptr
はポインタのサイズ(通常4バイトまたは8バイト)であり、これは特定の呼び出し規約や、レジスタ渡しされる引数など、スタック上に直接配置されない追加の引数領域を考慮している可能性があります。
-
gargsize
関数の導入:gsubr.c
にgargsize
関数が新しく追加されました。この関数は、引数として渡されたサイズ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
である」というメタデータを埋め込みます。 -
ginscall
でのgargsize
の利用:ggen.c
のginscall
関数内で、runtime
パッケージの関数呼び出しの直前にgargsize(arg)
が呼び出され、呼び出しの直後にgargsize(-1)
が呼び出されます。gargsize(arg)
: 関数呼び出しの直前に、その呼び出しの引数サイズをランタイムに通知します。これにより、ランタイムは関数が実行される間のスタック上の引数領域の正確なサイズを把握できます。gargsize(-1)
: 関数呼び出しの直後に-1
を渡してgargsize
を呼び出します。これは、引数サイズ情報の「リセット」または「無効化」を意味すると考えられます。つまり、この関数呼び出しの引数サイズ情報は、このポイントで適用が終了することを示唆しています。これにより、次の命令からは通常のスタックフレームの解釈に戻ることができます。
-
setmaxarg
の削除:cgen_callinter
およびcgen_call
関数からsetmaxarg
の呼び出しが削除されました。これは、新しいgargsize
メカニズムが、従来のsetmaxarg
が担っていた引数サイズ情報の提供をより正確かつ動的に行うため、setmaxarg
が不要になったことを示しています。setmaxarg
は、おそらく関数の最大引数サイズを静的に設定するものでしたが、可変引数関数には対応できませんでした。 -
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
パッケージ内の関数呼び出しに対して、以下のロジックが追加されました。
-
引数サイズの決定:
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 }
ここで、呼び出される関数
f
がruntimepkg
(runtime
パッケージ)に属するか、または特定のプロシージャタイプ(proc == 1
またはproc == 2
)である場合に、その関数の引数サイズarg
が計算されます。f->type->argwid
は、その関数の型定義から得られる引数の合計サイズです。proc == 1
やproc == 2
は、特定のランタイム呼び出し(例えば、Goルーチンの開始やCgo呼び出しなど)を示す内部的なフラグであると考えられます。これらのケースでは、追加のポインタサイズが加算されることで、可変引数や特殊な呼び出し規約によるスタックレイアウトの差異が考慮されます。 -
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_callinter
とcgen_call
からsetmaxarg
の呼び出しが削除されたことは、この新しいPCDATA_ArgSize
ベースのメカニズムが、従来の引数サイズ追跡方法を完全に置き換えることを意味します。setmaxarg
は、おそらく関数の最大引数サイズをコンパイル時に設定するものでしたが、可変引数関数のような動的なシナリオには対応できませんでした。新しいアプローチは、より柔軟で正確な引数サイズ情報を提供します。
これらの変更により、Goのコンパイラは、runtime
パッケージ内の関数呼び出し、特に可変引数を持つC関数への呼び出しに対して、ガベージコレクタがスタックを正確に解析するために必要な引数サイズ情報を、実行時に動的に提供できるようになりました。これは、Goランタイムの堅牢性と正確性を向上させる上で重要な改善です。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goのソースコードリポジトリ: https://github.com/golang/go
- Goのコンパイラに関する情報 (古いコンパイラについても言及があるかもしれません): https://go.dev/doc/articles/go-compiler
参考にした情報源リンク
- 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
で定義されています。