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

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

このコミットは、Go言語のツールチェインにおけるシンボルテーブルの管理と利用方法に重要な変更を加えています。具体的には、シンボルテーブルにGo言語の型情報(gotype)を追加し、リンカ(6l)がこの型情報をシンボルに紐付けるように修正しています。さらに、シンボルテーブルとPC/行番号テーブルを特定のメモリ領域にロードし、Goランタイムからこれらの生データにアクセスするための新しいシステムコールsys.symdat()を導入しています。これにより、デバッグやプロファイリング、リフレクションといった高度なランタイム操作のための基盤が強化されています。

コミット

commit 67addd4e11f147125952b0d4b50c1ed2563129e9
Author: Russ Cox <rsc@golang.org>
Date:   Thu Nov 20 17:32:18 2008 -0800

    symbol table changes
    
            * add gotype string to symbol table
            * fill in gotype in 6l for known funcs/vars
            * print gotype with nm -t
    
            * load symbol and pc/ln tables into memory at magic address 0x99<<32.
            * add sys.symdat() to retrieve raw bytes of symbol table
              and pc/ln table.
    
    most of this should be considered experimental
    and subject to change.
    
    R=r
    DELTA=157  (128 added, 0 deleted, 29 changed)
    OCL=19746
    CL=19750

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

https://github.com/golang/go/commit/67addd4e11f147125952b0d4b50c1ed2563129e9

元コミット内容

symbol table changes
        * add gotype string to symbol table
        * fill in gotype in 6l for known funcs/vars
        * print gotype with nm -t
        * load symbol and pc/ln tables into memory at magic address 0x99<<32.
        * add sys.symdat() to retrieve raw bytes of symbol table
          and pc/ln table.
most of this should be considered experimental
and subject to change.
R=r
DELTA=157  (128 added, 0 deleted, 29 changed)
OCL=19746
CL=19750

変更の背景

このコミットが行われた2008年当時、Go言語はまだ初期開発段階にありました。Goの設計目標の一つに、効率的なコンパイルと実行、そして強力なデバッグ・プロファイリング機能の提供がありました。シンボルテーブルは、コンパイルされたバイナリ内の関数、変数、その他のエンティティに関する情報(名前、アドレス、型など)を格納する重要なデータ構造です。

この変更の主な背景は以下の点にあると考えられます。

  1. 型情報の強化: 従来のシンボルテーブルには、Go言語特有の豊富な型情報が十分に格納されていませんでした。gotypeの導入により、リンカがGoの関数や変数の正確な型情報をシンボルテーブルに埋め込むことが可能になり、デバッガやプロファイラ、あるいは将来的なリフレクション機能がより詳細な型情報にアクセスできるようになります。
  2. ランタイムからのシンボル情報アクセス: デバッグやプロファイリング、あるいは高度なランタイム機能(例: スタックトレースの生成、ガベージコレクションの最適化)を実現するためには、実行中のプログラムが自身のシンボル情報やPC(プログラムカウンタ)とソースコードの行番号のマッピング情報(PC/行番号テーブル)にアクセスできる必要があります。このコミットは、これらのテーブルをメモリにロードし、sys.symdat()というシステムコールを通じてランタイムに公開することで、その基盤を構築しています。
  3. 実験的な機能の導入: コミットメッセージに「most of this should be considered experimental and subject to change.」とあるように、これはGo言語の初期段階における実験的な試みであり、将来の設計変更を視野に入れたものでした。Goのランタイムとツールチェインの連携を深めるための重要な一歩でした。

前提知識の解説

このコミットを理解するためには、以下の概念について知っておく必要があります。

  • シンボルテーブル (Symbol Table): コンパイラやリンカが生成するデータ構造で、プログラム内の識別子(変数名、関数名など)とそれに対応する情報(メモリ上のアドレス、型、スコープなど)をマッピングします。デバッガはシンボルテーブルを利用して、実行中のプログラムの変数値を表示したり、関数呼び出しのスタックトレースを解決したりします。

  • PC/行番号テーブル (PC/Line Table): プログラムカウンタ(PC、現在実行中の命令のアドレス)とソースコードのファイル名および行番号をマッピングするテーブルです。これにより、実行中のプログラムのどの部分がソースコードのどの行に対応するのかを特定できます。デバッグ時のスタックトレース表示や、プロファイリング時のホットスポット特定に不可欠です。

  • Goリンカ (6l): Go言語のコンパイルプロセスにおいて、コンパイラが生成したオブジェクトファイル(.oファイル)を結合し、実行可能なバイナリを生成するツールです。このコミットでは、6lがシンボルテーブルにgotype情報を追加し、シンボルテーブルとPC/行番号テーブルをバイナリに埋め込む処理が変更されています。

  • nm コマンド: Unix系のシステムで利用されるツールで、オブジェクトファイルや実行可能ファイル内のシンボル(関数名、変数名など)をリスト表示します。このコミットでは、nmコマンドに-tオプションが追加され、シンボルに関連付けられたgotypeを表示できるようになっています。

  • Goランタイム (Go Runtime): Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、システムコールインターフェースなどを担当します。このコミットでは、ランタイムがシンボルテーブルとPC/行番号テーブルにアクセスするためのsys.symdat()関数が追加されています。

  • ELF (Executable and Linkable Format): Unix系システムで広く使われている実行可能ファイル、オブジェクトファイル、共有ライブラリの標準フォーマットです。GoのバイナリもELF形式で生成されます。このコミットでは、ELFヘッダの構造やセグメントの配置に関する変更が含まれています。

技術的詳細

このコミットは、Go言語のツールチェインとランタイムの複数のコンポーネントにわたる協調的な変更を含んでいます。

  1. Sym構造体へのgotypeの追加: include/mach_amd64.hにおいて、シンボルを表すSym構造体にchar *gotype;フィールドが追加されました。これにより、各シンボルがGo言語の型情報を文字列として保持できるようになります。

  2. リンカ(6l)によるgotypeの埋め込み:

    • src/cmd/6l/go.cgotypefor(char *name)関数が追加されました。この関数は、シンボル名からGoの型情報を抽出するロジックを含んでいます。具体的には、シンボル名に含まれる特定のパターン(例: .·)を解析し、それが関数や変数の型情報である場合にその型文字列を返します。
    • src/cmd/6l/span.cputsymb関数が変更され、新しい引数char *gogotype)を受け取るようになりました。この関数は、シンボルテーブルにシンボル情報を書き込む際に、gotypeも一緒に書き込みます。
    • src/cmd/6l/span.casmsym関数では、SCONST(定数)、SDATA(データ)、SBSS(BSSセクション)のシンボルに対してgotypefor(s->name)を呼び出し、その結果をputsymbに渡すように変更されました。これにより、既知の関数や変数に対してGoの型情報がシンボルテーブルに埋め込まれるようになります。
  3. シンボルテーブルとPC/行番号テーブルのメモリロード:

    • src/cmd/6l/asm.cにおいて、シンボルテーブルとPC/行番号テーブルを特定の「マジックアドレス」0x99LL<<320x9900000000)にロードするロジックが追加されました。これは、Goのバイナリが実行時にこれらのテーブルを特定の固定アドレスから参照できるようにするためのものです。
    • ELFヘッダの生成ロジックも変更され、__SYMDATという新しいセグメントが追加されました。このセグメントは、シンボルテーブルとPC/行番号テーブルの生データを格納し、実行時に0x99LL<<32にマッピングされます。
  4. sys.symdat()システムコールの導入:

    • src/cmd/gc/sys.gosrc/cmd/gc/sysimport.cにおいて、export func sys.symdat() (symtab *[]byte, pclntab *[][]byte)という新しいシステムコールが宣言・追加されました。
    • src/runtime/runtime.csys.symdat関数の実装が追加されました。この関数は、マジックアドレス0x99LL<<32からシンボルテーブルとPC/行番号テーブルの生データを読み取り、それぞれをバイトスライスとしてGoランタイムに返します。これにより、Goプログラムは実行時に自身のシンボル情報やデバッグ情報にアクセスできるようになります。
  5. nmツールでのgotype表示:

    • src/cmd/nm/nm.cにおいて、nmコマンドに新しいオプション-tが追加されました。
    • printsyms関数が変更され、-tオプションが指定された場合、シンボルのgotypeフィールドが存在すればそれを表示するように修正されました。

これらの変更により、Goのバイナリはよりリッチなデバッグ情報を持ち、ランタイムがその情報にアクセスできる基盤が構築されました。

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

include/mach_amd64.h (Sym構造体への gotype 追加)

--- a/include/mach_amd64.h
+++ b/include/mach_amd64.h
@@ -84,6 +84,7 @@ struct	Sym
 	uint	sig;
 	char	type;
 	char	*name;
+	char *gotype;
 };
 /*
  * End of Plan 9 a.out.h

src/cmd/6l/go.c ( gotypefor 関数の追加)

--- a/src/cmd/6l/go.c
+++ b/src/cmd/6l/go.c
@@ -70,6 +70,27 @@ ilookup(char *name)
 	return x;
 }
 
+char*
+gotypefor(char *name)
+{
+	Import *x;
+	char *s, *p;
+
+	s = strdup(name);
+	p = utfrune(s, 0xB7);	// center dot
+	if(p == nil)
+		return nil;
+	*p++ = '.';
+	memmove(p, p+1, strlen(p));
+	x = ilookup(s);
+	free(s);
+	if(x == nil || x->prefix == nil)
+		return nil;
+	if(strcmp(x->prefix, "var") != 0 && strcmp(x->prefix, "func") != 0)
+		return nil;
+	return x->def;
+}
+
 static void loadpkgdata(char*, char*, int);
 static int parsemethod(char**, char*, char**);
 static int parsepkgdata(char*, char**, char*, int*, char**, char**, char**);

src/cmd/6l/span.c ( putsymb 関数の変更と asmsym での利用)

--- a/src/cmd/6l/span.c
+++ b/src/cmd/6l/span.c
@@ -150,9 +150,9 @@ xdefine(char *p, int t, vlong v)
 }
 
 void
-putsymb(char *s, int t, vlong v, int ver)
+putsymb(char *s, int t, vlong v, int ver, char *go)
 {
-	int i, f, l;
+	int i, j, f, l;
 
 	if(t == 'f')
 		s++;
@@ -181,7 +181,13 @@ putsymb(char *s, int t, vlong v, int ver)
 		cput(s[i]);
 		cput(0);
 	}
-	symsize += l + 1 + i + 1;
+	j = 0;
+	if(go) {
+		for(j=0; go[j]; j++)
+			cput(go[j]);
+	}
+	cput(0);
+	symsize += l + 1 + i + 1 + j + 1;
 
 	if(debug['n']) {
 		if(t == 'z' || t == 'Z') {
@@ -194,9 +200,9 @@ putsymb(char *s, int t, vlong v, int ver)
 			return;
 		}
 		if(ver)
-			Bprint(&bso, "%c %.8llux %s<%d>\n", t, v, s, ver);
+			Bprint(&bso, "%c %.8llux %s<%d> %s\n", t, v, s, ver, go);
 		else
-			Bprint(&bso, "%c %.8llux %s\n", t, v, s);
+			Bprint(&bso, "%c %.8llux %s %s\n", t, v, s, go);
 	}
 }
 
@@ -210,25 +216,25 @@ asmsym(void)
 
 	s = lookup("etext", 0);
 	if(s->type == STEXT)
-		putsymb(s->name, 'T', s->value, s->version);
+		putsymb(s->name, 'T', s->value, s->version, nil);
 
 	for(h=0; h<NHASH; h++)
 		for(s=hash[h]; s!=S; s=s->link)
 			switch(s->type) {
 			case SCONST:
-				putsymb(s->name, 'D', s->value, s->version);
+				putsymb(s->name, 'D', s->value, s->version, gotypefor(s->name));
 				continue;
 
 			case SDATA:
-				putsymb(s->name, 'D', s->value+INITDAT, s->version);
+				putsymb(s->name, 'D', s->value+INITDAT, s->version, gotypefor(s->name));
 				continue;
 
 			case SBSS:
-				putsymb(s->name, 'B', s->value+INITDAT, s->version);
+				putsymb(s->name, 'B', s->value+INITDAT, s->version, gotypefor(s->name));
 				continue;
 
 			case SFILE:
-				putsymb(s->name, 'f', s->value, s->version);
+				putsymb(s->name, 'f', s->value, s->version, nil);
 				continue;
 			}
 
@@ -240,22 +246,23 @@ asmsym(void)
 		/* filenames first */
 		for(a=p->to.autom; a; a=a->link)
 			if(a->type == D_FILE)
-				putsymb(a->asym->name, 'z', a->aoffset, 0);
+				putsymb(a->asym->name, 'z', a->aoffset, 0, nil);
 			else
 			if(a->type == D_FILE1)
-				putsymb(a->asym->name, 'Z', a->aoffset, 0);
+				putsymb(a->asym->name, 'Z', a->aoffset, 0, nil);
 
-		putsymb(s->name, 'T', s->value, s->version);
+		putsymb(s->name, 'T', s->value, s->version, gotypefor(s->name));
 
 		/* frame, auto and param after */
-		putsymb(".frame", 'm', p->to.offset+8, 0);
+		putsymb(".frame", 'm', p->to.offset+8, 0, nil);
 
+		/* TODO(rsc): Add types for D_AUTO and D_PARAM */
 		for(a=p->to.autom; a; a=a->link)
 			if(a->type == D_AUTO)
-				putsymb(a->asym->name, 'a', -a->aoffset, 0);
+				putsymb(a->asym->name, 'a', -a->aoffset, 0, nil);
 			else
 			if(a->type == D_PARAM)
-				putsymb(a->asym->name, 'p', a->aoffset, 0);
+				putsymb(a->asym->name, 'p', a->aoffset, 0, nil);
 	}
 	if(debug['v'] || debug['n'])
 		Bprint(&bso, "symsize = %lud\n", symsize);

src/runtime/runtime.c ( sys.symdat の実装)

--- a/src/runtime/runtime.c
+++ b/src/runtime/runtime.c
@@ -743,3 +743,34 @@ algarray[3] =
 //	{	pointerhash,	pointerequal,	pointerprint,	pointercopy	},  // 2
 	{	memhash,	memequal,	memprint,	memcopy	},  // 2 - treat pointers as ints
 };
+
+
+// Return a pointer to a byte array containing the symbol table segment.
+//
+// NOTE(rsc): I expect that we will clean up both the method of getting
+// at the symbol table and the exact format of the symbol table at some
+// point in the future.  It probably needs to be better integrated with
+// the type strings table too.  This is just a quick way to get started
+// and figure out what we want from/can do with it.
+void
+sys·symdat(Array *symtab, Array *pclntab)
+{
+	Array *a;
+	int32 *v;
+
+	v = (int32*)(0x99LL<<32);	/* known to 6l */
+
+	a = mal(sizeof *a);
+	a->nel = v[0];
+	a->cap = a->nel;
+	a->array = (byte*)&v[2];
+	symtab = a;
+	FLUSH(&symtab);
+
+	a = mal(sizeof *a);
+	a->nel = v[1];
+	a->cap = a->nel;
+	a->array = (byte*)&v[2] + v[0];
+	pclntab = a;
+	FLUSH(&pclntab);
+}

src/cmd/nm/nm.c ( nm -t オプションの追加)

--- a/src/cmd/nm/nm.c
+++ b/src/cmd/nm/nm.c
@@ -52,6 +52,7 @@ int	nflag;
 int	sflag;
 int	uflag;
 int	Tflag;
+int	tflag;
 
 Sym	**fnames;		/* file path translation table */
 Sym	**symptr;
@@ -90,6 +91,7 @@ main(int argc, char *argv[])
 	case 'n':	nflag = 1; break;
 	case 's':	sflag = 1; break;
 	case 'u':	uflag = 1; break;
+	case 't':	tflag = 1; break;
 	case 'T':	Tflag = 1; break;
 	} ARGEND
 	if (argc == 0)
@@ -298,7 +300,7 @@ printsyms(Sym **symptr, long nsym)
 
 	if(!sflag)
 		qsort(symptr, nsym, sizeof(*symptr), (void*)cmp);
-	
+
 	wid = 0;
 	for (i=0; i<nsym; i++) {
 		s = symptr[i];
@@ -306,7 +308,7 @@ printsyms(Sym **symptr, long nsym)
 		if (s->value >= 0x100000000LL && wid == 8)
 			wid = 16;
 	}	
+	}
 	for (i=0; i<nsym; i++) {
 		s = symptr[i];
 		if (multifile && !hflag)
@@ -322,7 +324,10 @@ printsyms(Sym **symptr, long nsym)
 			Bprint(&bout, "%*llux ", wid, s->value);
 		else
 			Bprint(&bout, "%*s ", wid, "");
-		Bprint(&bout, "%c %s\n", s->type, cp);
+		Bprint(&bout, "%c %s", s->type, cp);
+		if(tflag && s->gotype && s->gotype[0])
+			Bprint(&bout, " %s", s->gotype);
+		Bprint(&bout, "\n");
 	}
 }
 

コアとなるコードの解説

Sym構造体へのgotype追加

include/mach_amd64.hSym構造体にchar *gotype;が追加されたことで、各シンボルがGo言語の型情報を指すポインタを持つことができるようになりました。これにより、シンボルテーブルが単なる名前とアドレスのマッピングだけでなく、よりセマンティックな型情報も保持できるようになります。

gotypefor関数 (src/cmd/6l/go.c)

この関数は、Goのリンカがシンボル名からGoの型情報を推測するために使用されます。Goの内部的なシンボル名には、型情報を示す特定のパターンが含まれることがあります(例: main.init·fのような関数名)。gotypeforは、これらのパターンを解析し、対応する型文字列を返します。この関数は、リンカがシンボルテーブルに正確な型情報を埋め込むための重要な役割を担っています。

putsymb関数の変更とasmsymでの利用 (src/cmd/6l/span.c)

putsymb関数は、シンボル情報をバイナリのシンボルテーブルに書き込む役割を担っています。この変更により、gotype引数が追加され、シンボル名だけでなく型情報もシンボルテーブルに格納されるようになりました。

asmsym関数は、リンカがシンボルテーブルを構築する主要な場所です。この関数内で、SCONST, SDATA, SBSSといったデータシンボルや、STEXT(関数)シンボルに対してgotypeforを呼び出し、その結果をputsymbに渡しています。これにより、Goのコンパイル済みバイナリのシンボルテーブルには、関数や変数のGo型情報が埋め込まれるようになります。

sys.symdatの実装 (src/runtime/runtime.c)

sys.symdatは、Goランタイムが自身のシンボルテーブルとPC/行番号テーブルにアクセスするためのシステムコールです。この関数は、リンカによって特定の「マジックアドレス」0x99LL<<32に配置されたシンボルデータとPC/行番号データを指すポインタを取得します。そして、これらの生データをGoのバイトスライス(Array構造体)としてラップし、呼び出し元に返します。

この機能は、Goのデバッガ(gdbdlvなど)がGoプログラムの内部状態をより深く理解するために利用されたり、プロファイリングツールが実行中のコードのどの部分がボトルネックになっているかを特定するために利用されたりします。また、リフレクション機能の基盤としても機能し、実行時に型情報や関数情報を動的に取得することを可能にします。

nm -tオプションの追加 (src/cmd/nm/nm.c)

nmコマンドは、バイナリ内のシンボルを検査するための標準的なツールです。-tオプションの追加により、nmはシンボル名だけでなく、シンボルに関連付けられたGoの型情報も表示できるようになりました。これは、開発者がバイナリのデバッグ情報をより詳細に確認する際に役立ちます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (GitHub): https://github.com/golang/go
  • ELFファイルフォーマットに関する一般的な情報 (Wikipediaなど)
  • シンボルテーブルとデバッグ情報に関する一般的な情報 (コンパイラ理論やリンカの書籍など)
  • Go言語の初期のコミットログとデザインドキュメント (GoプロジェクトのメーリングリストやIssueトラッカー)