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

[インデックス 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.gosrc/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=0sys.caller 自体を、n=1sys.caller を呼び出した関数を指します。

実装のステップは以下の通りです。

  1. 現在のPCとSPの取得: sys.caller が呼び出された時点のプログラムカウンタ (pc) とスタックポインタ (sp) を取得します。これは、スタックフレームの構造を理解している必要があります。
  2. findfunc による関数情報の取得: 取得した pc を使って findfunc(pc) を呼び出し、現在の関数 (f) の情報を取得します。
  3. スタックの巻き戻し: n の値に基づいて、ループを使ってスタックを巻き戻します。
    • retfromnewstack の処理: Goのランタイムは、スタックの拡張やゴルーチンの切り替えのために、スタックを動的に管理します。retfromnewstack は、新しいスタックへの切り替えが行われたことを示す特別なPC値です。この場合、Stktop 構造体を使って古いスタックの情報 (stk->oldsp, stk->oldbase) を取得し、スタックポインタとPCを更新します。
    • フレームサイズの考慮: アセンブリ関数など、一部の関数は通常のGo関数とは異なるスタックフレーム構造を持つ場合があります。f->frame は関数のスタックフレームサイズを示し、これに基づいて sp を適切に調整します。
    • 次の呼び出し元のPCの取得: 各スタックフレームから、呼び出し元のPCを取得します。これは通常、スタックポインタから一定のオフセットに格納されています。
    • findfunc による次の関数情報の取得: 取得したPCを使って再度 findfunc を呼び出し、次の呼び出し元の関数情報を取得します。PCが不正な値(0x1000 以下など)であったり、関数情報が見つからなかったりした場合はエラーとして処理されます。
  4. 結果の返却: n レベルの巻き戻しが完了したら、最終的に取得されたPC、f->src (ソースファイル名)、funcline(f, pc-1) (行番号) を返します。ok は処理が成功したかどうかを示します。

src/cmd/gc/sys.gosrc/cmd/gc/sysimport.c の変更は、主に sys.caller の宣言を追加し、既存のシステム関数の宣言を整理するものです。特に、breakpointreflect などの関数が「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.callerint 型の引数 n を受け取り、uint64 型のプログラムカウンタ pcstring 型のファイル名 fileint 型の行番号 line、そして bool 型の成功フラグ ok を返すことを示しています。

また、既存の export func の宣言が「コンパイラによって出力され、Goプログラムからは参照されない」ものと「Goプログラムによって使用される」ものに分類され、整理されています。これは、Go言語のシステム関数がより明確に役割分担されるようになったことを示唆しています。

src/runtime/rt2_amd64.c

このファイルには、sys.caller の実際のランタイム実装が含まれています。これはC言語で書かれており、AMD64アーキテクチャに特化しています。

sys·caller 関数は、Goの関数呼び出し規約に従って引数と戻り値を受け取ります。

  1. 初期化: pc, sp, stk, f といった変数が宣言されます。これらはそれぞれプログラムカウンタ、スタックポインタ、スタックトップ構造体、関数情報を格納するために使用されます。
  2. 呼び出し元のPCとSPの取得:
    sp = (byte*)&n;
    pc = *(uint64*)(sp-8);
    
    n のアドレスを基準に、スタック上の戻りアドレス(呼び出し元のPC)とスタックポインタを計算して取得します。これは、Goの関数呼び出し規約とスタックフレームのレイアウトに依存します。
  3. 現在の関数情報の取得:
    if((f = findfunc(pc)) == nil) {
        // error handling
    }
    
    取得したPCを使って findfunc を呼び出し、現在の関数 f のメタデータを取得します。findfuncnil を返す場合(関数が見つからない場合)はエラーとして処理され、戻り値はゼロ値に設定されます。
  4. スタックの巻き戻しループ:
    while(n-- > 0) {
        // ...
    }
    
    n の値が0になるまでループを回し、指定されたレベル数だけスタックを巻き戻します。
    • retfromnewstack の処理:
      while(pc == (uint64)retfromnewstack) {
          sp = stk->oldsp;
          stk = (Stktop*)stk->oldbase;
          pc = *(uint64*)(sp+8);
          sp += 16;
      }
      
      Goのランタイムは、スタックの動的な拡張やゴルーチンの切り替えのために、スタックを移動させることがあります。retfromnewstack は、スタックが新しい場所に移動したことを示す特別なPC値です。この場合、Stktop 構造体(g->stackbase から取得)を使って古いスタックポインタ (oldsp) と古いスタックベース (oldbase) を取得し、sppc を更新して、正しい呼び出し元にジャンプします。
    • スタックポインタの調整:
      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を取得します。
    • 次の関数情報の取得とエラーチェック:
      if(pc <= 0x1000 || (f = findfunc(pc)) == nil)
          goto error;
      
      取得したPCが不正な値(非常に小さいアドレスなど)であったり、findfunc が関数情報を見つけられなかったりした場合は、goto error でエラー処理にジャンプします。
  5. 結果の格納とFLUSH:
    retpc = pc;
    retfile = f->src;
    retline = funcline(f, pc-1);
    retbool = true;
    FLUSH(&retpc);
    FLUSH(&retfile);
    FLUSH(&retline);
    FLUSH(&retbool);
    
    ループが完了したら、最終的に取得されたPC、ソースファイル名 (f->src)、および funcline 関数を使って計算された行番号を戻り値に格納します。retbooltrue に設定され、処理が成功したことを示します。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アーキテクチャの呼び出し規約とスタックフレームに関する一般的な情報。