[インデックス 1459] ファイルの概要
コミット
commit a3ed4e716a61ffc8cbaba6094b82832a37d74222
Author: Russ Cox <rsc@golang.org>
Date: Fri Jan 9 15:52:43 2009 -0800
add sys.caller
R=r
DELTA=139 (101 added, 38 deleted, 0 changed)
OCL=22462
CL=22466
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a3ed4e716a61ffc8cbaba6094b82832a37d74222
元コミット内容
このコミットは、Go言語のシステムパッケージに sys.caller
関数を追加するものです。sys.caller
は、呼び出し元の関数に関する情報(プログラムカウンタ、ファイル名、行番号、および成功を示すブール値)を取得するために使用されます。
変更の概要は以下の通りです。
src/cmd/gc/sys.go
:sys.caller
関数のエクスポート宣言を追加。また、breakpoint
関数など、一部のexport func
の宣言を「コンパイラによって出力され、Goプログラムからは参照されない」ものと「Goプログラムによって使用される」ものに再編成。src/cmd/gc/sysimport.c
:sys.caller
関数のインポート宣言を追加。sys.go
と同様に、関数の宣言の順序を再編成。src/runtime/rt2_amd64.c
:sys.caller
関数のランタイム実装を追加。これは、スタックを巻き戻して呼び出し元の情報を取得するC言語のコードです。
変更の背景
このコミットは、Go言語の初期開発段階におけるもので、Goプログラムが自身の呼び出しスタックを検査する機能を提供することを目的としています。このような機能は、デバッグ、プロファイリング、エラー報告、あるいは特定のフレームワークやライブラリが呼び出し元のコンテキストに基づいて動作を変更する必要がある場合に不可欠です。
特に、sys.caller
は、Goの標準ライブラリにおける runtime.Caller
関数の基盤となる低レベルな実装を提供します。runtime.Caller
は、Goプログラムが現在のゴルーチンの呼び出しスタックに関する情報を取得するための標準的な方法です。このコミットは、そのためのプリミティブな機能を追加しています。
前提知識の解説
このコミットを理解するためには、以下の概念に関する知識が必要です。
- Go言語のコンパイラ (
gc
): Go言語の公式コンパイラはgc
と呼ばれます。これはGoソースコードを機械語に変換する役割を担います。src/cmd/gc/sys.go
やsrc/cmd/gc/sysimport.c
は、コンパイラの一部であり、Goプログラムが利用できる組み込み関数やシステムコールのようなものを定義しています。 - Goランタイム (
runtime
): Goランタイムは、Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、メモリ管理、スタック管理など、多くの低レベルな機能を提供します。src/runtime/rt2_amd64.c
は、AMD64アーキテクチャ向けのランタイムの一部であり、sys.caller
のような低レベルな関数がC言語で実装されています。 - スタックフレームとプログラムカウンタ (PC): プログラムが関数を呼び出すと、その関数のローカル変数、引数、戻りアドレスなどがスタックに積まれます。これをスタックフレームと呼びます。プログラムカウンタ (PC) は、現在実行中の命令のアドレスを示すレジスタです。呼び出し元の情報を取得するには、スタックを遡って、各スタックフレームの戻りアドレス(呼び出し元のPC)を特定する必要があります。
export func
宣言: Go言語の初期のコンパイラでは、export func
は、Goプログラムから直接呼び出される可能性のある、コンパイラやランタイムによって提供される特殊な関数を宣言するために使用されていました。これらは、通常のGo関数とは異なり、コンパイラが特別な処理を行う必要がありました。Stktop
構造体:src/runtime/rt2_amd64.c
に登場するStktop
は、Goのランタイムにおけるスタック管理に関連する内部構造体です。特に、ゴルーチンのスタックが拡張されたり、新しいスタックに切り替わったりする際に、古いスタックの情報(oldsp
,oldbase
)を保持するために使用されます。findfunc
関数: ランタイム内で定義されているfindfunc
は、与えられたプログラムカウンタ (PC) に対応する関数情報を検索する関数です。これにより、PCから関数名やファイル名、行番号などのデバッグ情報を取得できます。funcline
関数:funcline
は、関数情報とPCから、そのPCが属するソースコードの行番号を計算する関数です。
技術的詳細
このコミットの主要な技術的詳細は、sys.caller
のランタイム実装 (src/runtime/rt2_amd64.c
) にあります。
sys.caller
関数は、n
という整数引数を受け取ります。これは、現在の関数から何レベル遡った呼び出し元の情報を取得するかを指定します。n=0
は sys.caller
自体を、n=1
は sys.caller
を呼び出した関数を指します。
実装のステップは以下の通りです。
- 現在のPCとSPの取得:
sys.caller
が呼び出された時点のプログラムカウンタ (pc
) とスタックポインタ (sp
) を取得します。これは、スタックフレームの構造を理解している必要があります。 findfunc
による関数情報の取得: 取得したpc
を使ってfindfunc(pc)
を呼び出し、現在の関数 (f
) の情報を取得します。- スタックの巻き戻し:
n
の値に基づいて、ループを使ってスタックを巻き戻します。retfromnewstack
の処理: Goのランタイムは、スタックの拡張やゴルーチンの切り替えのために、スタックを動的に管理します。retfromnewstack
は、新しいスタックへの切り替えが行われたことを示す特別なPC値です。この場合、Stktop
構造体を使って古いスタックの情報 (stk->oldsp
,stk->oldbase
) を取得し、スタックポインタとPCを更新します。- フレームサイズの考慮: アセンブリ関数など、一部の関数は通常のGo関数とは異なるスタックフレーム構造を持つ場合があります。
f->frame
は関数のスタックフレームサイズを示し、これに基づいてsp
を適切に調整します。 - 次の呼び出し元のPCの取得: 各スタックフレームから、呼び出し元のPCを取得します。これは通常、スタックポインタから一定のオフセットに格納されています。
findfunc
による次の関数情報の取得: 取得したPCを使って再度findfunc
を呼び出し、次の呼び出し元の関数情報を取得します。PCが不正な値(0x1000
以下など)であったり、関数情報が見つからなかったりした場合はエラーとして処理されます。
- 結果の返却:
n
レベルの巻き戻しが完了したら、最終的に取得されたPC、f->src
(ソースファイル名)、funcline(f, pc-1)
(行番号) を返します。ok
は処理が成功したかどうかを示します。
src/cmd/gc/sys.go
と src/cmd/gc/sysimport.c
の変更は、主に sys.caller
の宣言を追加し、既存のシステム関数の宣言を整理するものです。特に、breakpoint
や reflect
などの関数が「Goプログラムによって使用される」セクションに移動されたことは、これらの関数がコンパイラの内部的な使用だけでなく、Goプログラムからも直接利用されるようになったことを示唆しています。
コアとなるコードの変更箇所
src/cmd/gc/sys.go
--- a/src/cmd/gc/sys.go
+++ b/src/cmd/gc/sys.go
@@ -5,8 +5,9 @@
package PACKAGE
+// emitted by compiler, not referred to by go programs
+//
export func mal(int32) *any;
-export func breakpoint();
export func throwindex();
export func throwreturn();
export func panicl(int32);
@@ -35,25 +36,6 @@ export func ifaceI2T2(sigt *byte, iface any) (ret any, ok bool);\
export func ifaceI2I(sigi *byte, iface any) (ret any);\
export func ifaceI2I2(sigi *byte, iface any) (ret any, ok bool);\
export func ifaceeq(i1 any, i2 any) (ret bool);\
-export func reflect(i interface { }) (uint64, string, bool);\
-export func unreflect(uint64, string, bool) (ret interface { });
-
-export func argc() int;
-export func envc() int;
-export func argv(int) string;
-export func envv(int) string;
-
-export func frexp(float64) (float64, int); // break fp into exp,fract
-export func ldexp(float64, int) float64; // make fp from exp,fract
-export func modf(float64) (float64, float64); // break fp into double.double
-export func isInf(float64, int) bool; // test for infinity
-export func isNaN(float64) bool; // test for not-a-number
-export func Inf(int) float64; // return signed Inf
-export func NaN() float64; // return a NaN
-export func float32bits(float32) uint32; // raw bits
-export func float64bits(float64) uint64; // raw bits
-export func float32frombits(uint32) float32; // raw bits
-export func float64frombits(uint64) float64; // raw bits
export func newmap(keysize int, valsize int,
keyalg int, valalg int,
@@ -85,6 +67,30 @@ export func arraysliced(old []any, lb int, hb int, width int) (ary []any);\
export func arrayslices(old *any, nel int, lb int, hb int, width int) (ary []any);\
export func arrays2d(old *any, nel int) (ary []any);\
+// used by go programs
+//
+export func breakpoint();
+
+export func reflect(i interface { }) (uint64, string, bool);
+export func unreflect(uint64, string, bool) (ret interface { });
+
+export func argc() int;
+export func envc() int;
+export func argv(int) string;
+export func envv(int) string;
+
+export func frexp(float64) (float64, int); // break fp into exp,fract
+export func ldexp(float64, int) float64; // make fp from exp,fract
+export func modf(float64) (float64, float64); // break fp into double.double
+export func isInf(float64, int) bool; // test for infinity
+export func isNaN(float64) bool; // test for not-a-number
+export func Inf(int) float64; // return signed Inf
+export func NaN() float64; // return a NaN
+export func float32bits(float32) uint32; // raw bits
+export func float64bits(float64) uint64; // raw bits
+export func float32frombits(uint32) float32; // raw bits
+export func float64frombits(uint64) float64; // raw bits
+
export func gosched();
export func goexit();
@@ -96,6 +102,7 @@ export func stringtorune(string, int) (int, int);\t// convert bytes to runes\
export func exit(int);\
export func symdat() (symtab []byte, pclntab []byte);\
+export func caller(n int) (pc uint64, file string, line int, ok bool);\
export func semacquire(sema *int32);\
export func semrelease(sema *int32);\
src/cmd/gc/sysimport.c
--- a/src/cmd/gc/sysimport.c
+++ b/src/cmd/gc/sysimport.c
@@ -1,7 +1,6 @@
char *sysimport =
"package sys\\n"\
"export func sys.mal (? int32) (? *any)\\n"\
-\t"export func sys.breakpoint ()\\n"\
"export func sys.throwindex ()\\n"\
"export func sys.throwreturn ()\\n"\
"export func sys.panicl (? int32)\\n"\
@@ -27,23 +26,6 @@ char *sysimport =\
"export func sys.ifaceI2I (sigi *uint8, iface any) (ret any)\\n"\
"export func sys.ifaceI2I2 (sigi *uint8, iface any) (ret any, ok bool)\\n"\
"export func sys.ifaceeq (i1 any, i2 any) (ret bool)\\n"\
-\t"export func sys.reflect (i interface { }) (? uint64, ? string, ? bool)\\n"\
-\t"export func sys.unreflect (? uint64, ? string, ? bool) (ret interface { })\\n"\
-\t"export func sys.argc () (? int)\\n"\
-\t"export func sys.envc () (? int)\\n"\
-\t"export func sys.argv (? int) (? string)\\n"\
-\t"export func sys.envv (? int) (? string)\\n"\
-\t"export func sys.frexp (? float64) (? float64, ? int)\\n"\
-\t"export func sys.ldexp (? float64, ? int) (? float64)\\n"\
-\t"export func sys.modf (? float64) (? float64, ? float64)\\n"\
-\t"export func sys.isInf (? float64, ? int) (? bool)\\n"\
-\t"export func sys.isNaN (? float64) (? bool)\\n"\
-\t"export func sys.Inf (? int) (? float64)\\n"\
-\t"export func sys.NaN () (? float64)\\n"\
-\t"export func sys.float32bits (? float32) (? uint32)\\n"\
-\t"export func sys.float64bits (? float64) (? uint64)\\n"\
-\t"export func sys.float32frombits (? uint32) (? float32)\\n"\
-\t"export func sys.float64frombits (? uint64) (? float64)\\n"\
"export func sys.newmap (keysize int, valsize int, keyalg int, valalg int, hint int) (hmap map[any] any)\\n"\
"export func sys.mapaccess1 (hmap map[any] any, key any) (val any)\\n"\
"export func sys.mapaccess2 (hmap map[any] any, key any) (val any, pres bool)\\n"\
@@ -68,6 +50,24 @@ char *sysimport =\
"export func sys.arraysliced (old []any, lb int, hb int, width int) (ary []any)\\n"\
"export func sys.arrayslices (old *any, nel int, lb int, hb int, width int) (ary []any)\\n"\
"export func sys.arrays2d (old *any, nel int) (ary []any)\\n"\
+\t"export func sys.breakpoint ()\\n"\
+\t"export func sys.reflect (i interface { }) (? uint64, ? string, ? bool)\\n"\
+\t"export func sys.unreflect (? uint64, ? string, ? bool) (ret interface { })\\n"\
+\t"export func sys.argc () (? int)\\n"\
+\t"export func sys.envc () (? int)\\n"\
+\t"export func sys.argv (? int) (? string)\\n"\
+\t"export func sys.envv (? int) (? string)\\n"\
+\t"export func sys.frexp (? float64) (? float64, ? int)\\n"\
+\t"export func sys.ldexp (? float64, ? int) (? float64)\\n"\
+\t"export func sys.modf (? float64) (? float64, ? float64)\\n"\
+\t"export func sys.isInf (? float64, ? int) (? bool)\\n"\
+\t"export func sys.isNaN (? float64) (? bool)\\n"\
+\t"export func sys.Inf (? int) (? float64)\\n"\
+\t"export func sys.NaN () (? float64)\\n"\
+\t"export func sys.float32bits (? float32) (? uint32)\\n"\
+\t"export func sys.float64bits (? float64) (? uint64)\\n"\
+\t"export func sys.float32frombits (? uint32) (? float32)\\n"\
+\t"export func sys.float64frombits (? uint64) (? float64)\\n"\
"export func sys.gosched ()\\n"\
"export func sys.goexit ()\\n"\
"export func sys.readfile (? string) (? string, ? bool)\\n"\
@@ -76,6 +76,7 @@ char *sysimport =\
"export func sys.stringtorune (? string, ? int) (? int, ? int)\\n"\
"export func sys.exit (? int)\\n"\
"export func sys.symdat () (symtab []uint8, pclntab []uint8)\\n"\
+\t"export func sys.caller (n int) (pc uint64, file string, line int, ok bool)\\n"\
"export func sys.semacquire (sema *int32)\\n"\
"export func sys.semrelease (sema *int32)\\n"\
"\\n"\
src/runtime/rt2_amd64.c
--- a/src/runtime/rt2_amd64.c
+++ b/src/runtime/rt2_amd64.c
@@ -69,3 +69,58 @@ traceback(byte *pc0, byte *sp, G *g)\
}\
prints(\"...\\n\");\
}\
+\
+// func caller(n int) (pc uint64, file string, line int, ok bool)\
+void\
+sys·caller(int32 n, uint64 retpc, string retfile, int32 retline, bool retbool)\
+{\
+\tuint64 pc;\
+\tbyte *sp;\
+\tStktop *stk;\
+\tFunc *f;\
+\
+\t// our caller\'s pc, sp.\
+\tsp = (byte*)&n;\
+\tpc = *(uint64*)(sp-8);\
+\tif((f = findfunc(pc)) == nil) {\
+\terror:\
+\t\tretpc = 0;\
+\t\tretline = 0;\
+\t\tretfile = nil;\
+\t\tretbool = false;\
+\t\tFLUSH(&retpc);\
+\t\tFLUSH(&retfile);\
+\t\tFLUSH(&retline);\
+\t\tFLUSH(&retbool);\
+\t\treturn;\
+\t}\
+\
+\t// now unwind n levels
+\tstk = (Stktop*)g->stackbase;\
+\twhile(n-- > 0) {\
+\t\twhile(pc == (uint64)retfromnewstack) {\
+\t\t\tsp = stk->oldsp;\
+\t\t\tstk = (Stktop*)stk->oldbase;\
+\t\t\tpc = *(uint64*)(sp+8);\
+\t\t\tsp += 16;\
+\t\t}\
+\
+\t\tif(f->frame < 8)\t// assembly functions lie\
+\t\t\tsp += 8;\
+\t\telse\
+\t\t\tsp += f->frame;\
+\
+\t\tpc = *(uint64*)(sp-8);\
+\t\tif(pc <= 0x1000 || (f = findfunc(pc)) == nil)\
+\t\t\tgoto error;\
+\t}\
+\
+\tretpc = pc;\
+\tretfile = f->src;\
+\tretline = funcline(f, pc-1);\
+\tretbool = true;\
+\tFLUSH(&retpc);\
+\tFLUSH(&retfile);\
+\tFLUSH(&retline);\
+\tFLUSH(&retbool);\
+}\
コアとなるコードの解説
src/cmd/gc/sys.go
および src/cmd/gc/sysimport.c
これらのファイルでは、sys.caller
関数のシグネチャが定義されています。
export func caller(n int) (pc uint64, file string, line int, ok bool);
これは、sys.caller
が int
型の引数 n
を受け取り、uint64
型のプログラムカウンタ pc
、string
型のファイル名 file
、int
型の行番号 line
、そして bool
型の成功フラグ ok
を返すことを示しています。
また、既存の export func
の宣言が「コンパイラによって出力され、Goプログラムからは参照されない」ものと「Goプログラムによって使用される」ものに分類され、整理されています。これは、Go言語のシステム関数がより明確に役割分担されるようになったことを示唆しています。
src/runtime/rt2_amd64.c
このファイルには、sys.caller
の実際のランタイム実装が含まれています。これはC言語で書かれており、AMD64アーキテクチャに特化しています。
sys·caller
関数は、Goの関数呼び出し規約に従って引数と戻り値を受け取ります。
- 初期化:
pc
,sp
,stk
,f
といった変数が宣言されます。これらはそれぞれプログラムカウンタ、スタックポインタ、スタックトップ構造体、関数情報を格納するために使用されます。 - 呼び出し元のPCとSPの取得:
sp = (byte*)&n; pc = *(uint64*)(sp-8);
n
のアドレスを基準に、スタック上の戻りアドレス(呼び出し元のPC)とスタックポインタを計算して取得します。これは、Goの関数呼び出し規約とスタックフレームのレイアウトに依存します。 - 現在の関数情報の取得:
取得したPCを使ってif((f = findfunc(pc)) == nil) { // error handling }
findfunc
を呼び出し、現在の関数f
のメタデータを取得します。findfunc
がnil
を返す場合(関数が見つからない場合)はエラーとして処理され、戻り値はゼロ値に設定されます。 - スタックの巻き戻しループ:
while(n-- > 0) { // ... }
n
の値が0になるまでループを回し、指定されたレベル数だけスタックを巻き戻します。retfromnewstack
の処理:
Goのランタイムは、スタックの動的な拡張やゴルーチンの切り替えのために、スタックを移動させることがあります。while(pc == (uint64)retfromnewstack) { sp = stk->oldsp; stk = (Stktop*)stk->oldbase; pc = *(uint64*)(sp+8); sp += 16; }
retfromnewstack
は、スタックが新しい場所に移動したことを示す特別なPC値です。この場合、Stktop
構造体(g->stackbase
から取得)を使って古いスタックポインタ (oldsp
) と古いスタックベース (oldbase
) を取得し、sp
とpc
を更新して、正しい呼び出し元にジャンプします。- スタックポインタの調整:
関数のスタックフレームサイズ (if(f->frame < 8) // assembly functions lie sp += 8; else sp += f->frame;
f->frame
) に基づいてsp
を調整します。アセンブリ関数など、一部の関数はフレームサイズが正確でない場合があるため、特別な処理 (f->frame < 8
) が行われます。 - 次の呼び出し元のPCの取得:
調整されたpc = *(uint64*)(sp-8);
sp
から、次の呼び出し元のPCを取得します。 - 次の関数情報の取得とエラーチェック:
取得したPCが不正な値(非常に小さいアドレスなど)であったり、if(pc <= 0x1000 || (f = findfunc(pc)) == nil) goto error;
findfunc
が関数情報を見つけられなかったりした場合は、goto error
でエラー処理にジャンプします。
- 結果の格納とFLUSH:
ループが完了したら、最終的に取得されたPC、ソースファイル名 (retpc = pc; retfile = f->src; retline = funcline(f, pc-1); retbool = true; FLUSH(&retpc); FLUSH(&retfile); FLUSH(&retline); FLUSH(&retbool);
f->src
)、およびfuncline
関数を使って計算された行番号を戻り値に格納します。retbool
はtrue
に設定され、処理が成功したことを示します。FLUSH
マクロは、コンパイラの最適化によって変数がレジスタに保持されるのを防ぎ、メモリに書き込まれることを保証するために使用されます。これは、GoのランタイムとCコンパイラの間のインターフェースで重要です。
関連リンク
- Go言語の
runtime.Caller
ドキュメント: このコミットで追加されたsys.caller
は、Goの標準ライブラリのruntime.Caller
の基盤となります。
参考にした情報源リンク
- Go言語のソースコード (特に
src/runtime
ディレクトリ): - Go言語のコンパイラとランタイムに関するドキュメントやブログ記事 (Goの内部構造に関する一般的な情報源):
- Goのスタック管理に関する記事 (例: "Go's work-stealing scheduler" や "Go's runtime: a deep dive")
- Goのコンパイラに関する記事 (例: "Go compiler internals")
- Go言語の初期のコミット履歴: このコミットはGo言語の非常に初期の段階のものであるため、当時の設計思想や実装の背景を理解するには、関連する他の初期コミットも参照すると良いでしょう。
- Goの
sys
パッケージに関する情報 (もし存在すれば):sys
パッケージはGoの内部的なものであり、直接ドキュメント化されていないことが多いですが、関連する議論や設計ドキュメントが存在する可能性があります。 - AMD64アーキテクチャの呼び出し規約とスタックフレームに関する一般的な情報。