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

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

このコミットは、Go言語のランタイムおよびリンカ(6l)におけるスタックマーク文字列の管理方法を変更するものです。具体的には、実行可能ファイル内に埋め込まれていたスタックマーク用の特殊な文字列(SOFmark)を削除し、代わりにメモリ上のシンボルテーブルのコピーを利用して関数やスタックフレームの情報を取得するように変更しています。これにより、実行可能ファイルのサイズ削減と、より柔軟なデバッグ情報の管理を目指しています。

コミット

commit 3aa063d79c5ae4057e312d534abf65ac37801258
Author: Russ Cox <rsc@golang.org>
Date:   Sun Nov 23 17:08:55 2008 -0800

    delete stack mark strings
    in favor of using in-memory copy of symbol table.
    
    $ ls -l pretty pretty.big
    -rwxr-xr-x  1 rsc  eng  439516 Nov 21 16:43 pretty
    -rwxr-xr-x  1 rsc  eng  580984 Nov 21 16:20 pretty.big
    $
    
    R=r
    DELTA=446  (238 added, 178 deleted, 30 changed)
    OCL=19851
    CL=19884
---
 src/cmd/6l/6.out.h      |   1 -
 src/cmd/6l/obj.c        |   1 -
 src/cmd/6l/pass.c       |  94 ------------------------
 src/runtime/Makefile    |   2 +
 src/runtime/iface.c     |  11 +--
 src/runtime/print.c     |   2 +-\n src/runtime/rt2_amd64.c |  31 +++-----\n src/runtime/runtime.c   |  75 ++++---------------\n src/runtime/runtime.h   |  50 +++++++++----\n src/runtime/string.c    |  15 +++-\n src/runtime/symtab.c    | 190 ++++++++++++++++++++++++++++++++++++++++++++++++\n 11 files changed, 268 insertions(+), 204 deletions(-)

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

https://github.com/golang/go/commit/3aa063d79c5ae4057e312d534abf65ac37801258

元コミット内容

delete stack mark strings
in favor of using in-memory copy of symbol table.

$ ls -l pretty pretty.big
-rwxr-xr-x  1 rsc  eng  439516 Nov 21 16:43 pretty
-rwxr-xr-x  1 rsc  eng  580984 Nov 21 16:20 pretty.big
$

変更の背景

このコミットの主な背景は、Goプログラムの実行可能ファイルサイズを削減し、デバッグ情報の管理を効率化することにあります。以前のGoランタイムでは、スタックトレースなどのデバッグ情報を取得するために、特定の関数エントリポイントに「スタックマーク文字列」(SOFmark)と呼ばれる特殊なバイトシーケンスを埋め込んでいました。これは、実行時にスタックフレームの境界や関数名を特定するための目印として機能していました。

しかし、この方法はいくつかの課題を抱えていました。

  1. ファイルサイズの増大: 各関数にスタックマーク文字列を埋め込むことは、特に多数の関数を持つ大規模なプログラムにおいて、実行可能ファイルのサイズを不必要に増大させる原因となっていました。コミットメッセージにあるls -lの出力は、この変更によってファイルサイズが削減されることを示唆しています(prettyが変更後のファイル、pretty.bigが変更前のファイルと推測されます)。
  2. 柔軟性の欠如: スタックマーク文字列は固定された形式であり、よりリッチなデバッグ情報や動的なランタイム解析には不向きでした。
  3. シンボルテーブルの活用: Goの実行可能ファイルには、コンパイル時に生成されるシンボルテーブルが既に存在します。このシンボルテーブルは、関数名、アドレス、型情報など、デバッグに必要な多くの情報を含んでいます。スタックマーク文字列に依存するのではなく、この既存のシンボルテーブルをメモリ上で活用することで、冗長性を排除し、より堅牢で柔軟なデバッグメカニズムを構築できると考えられました。

この変更は、Goランタイムの初期段階における最適化と設計改善の一環として行われました。

前提知識の解説

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

1. Go言語のランタイム (Runtime)

Go言語は、ガベージコレクション、スケジューラ、スタック管理など、プログラムの実行をサポートする独自のランタイムを持っています。C言語で書かれた部分が多く、Goプログラムの実行時に重要な役割を果たします。スタックトレースの生成やデバッグ情報の提供もランタイムの機能の一部です。

2. リンカ (Linker) 6l

6lは、Go言語の初期のツールチェインにおけるAMD64アーキテクチャ向けのリンカです。コンパイラ(例: 6g)によって生成されたオブジェクトファイル(.6ファイル)を結合し、実行可能なバイナリを生成する役割を担います。この過程で、シンボルテーブルの埋め込みや、必要に応じてスタックマークのような特殊な情報の挿入が行われます。

3. スタックマーク文字列 (SOFmark)

Goの初期のランタイムでは、関数呼び出しのスタックフレームの開始位置を識別するために、特定のバイトシーケンス(例: \xa7\xf1\xd9\x2a\x82\xc8\xd8\xfe)を関数エントリに埋め込んでいました。これを「スタックマーク文字列」と呼びます。ランタイムは、スタックトレースを生成する際にこのマークを検索し、スタックフレームの境界を特定していました。

4. シンボルテーブル (Symbol Table)

シンボルテーブルは、プログラム内のシンボル(関数名、変数名など)とそのアドレスや型情報などの関連付けを記録したデータ構造です。コンパイルおよびリンク時に生成され、デバッグやプロファイリング、動的なコード解析などに利用されます。Goの実行可能ファイルには、このシンボルテーブルが埋め込まれており、ランタイムが実行時にアクセスできます。

5. findnull 関数

findnull関数は、C言語スタイルのヌル終端文字列の長さを計算するユーティリティ関数です。GoランタイムのCコード部分で頻繁に使用されます。

6. gostring 関数

gostring関数は、C言語スタイルのヌル終端バイト配列をGoのstring型(内部的にはstruct String)に変換するための関数です。Goの文字列は長さ情報を持つため、単なるバイト配列とは異なります。

7. Func 構造体

Func構造体は、Goランタイムが関数に関する情報を保持するために使用する内部データ構造です。関数名、エントリポイントのアドレス、スタックフレームサイズなどの情報が含まれます。このコミットでは、このFunc構造体とシンボルテーブルを連携させることで、スタックマーク文字列の代替として利用しています。

8. symtab.c

このコミットで新しく追加されたsrc/runtime/symtab.cファイルは、Goランタイムが実行時にシンボルテーブルにアクセスし、解析するためのロジックを実装しています。このファイルが、メモリ上のシンボルテーブルを活用する新しいアプローチの核心となります。

技術的詳細

このコミットの技術的な核心は、スタックトレースやデバッグ情報の取得方法を、実行可能ファイルに埋め込まれた固定のスタックマーク文字列から、メモリ上にロードされたシンボルテーブルの動的な解析へと移行した点にあります。

変更前のアプローチ:

  • リンカ(6l)が、各関数のエントリポイントにSOFmarkという特殊なバイトシーケンスと、スタックオフセット、関数名を埋め込んでいました。
  • ランタイムのtraceback関数(src/runtime/rt2_amd64.cなど)は、スタックを遡りながらこのSOFmarkを検索し、見つかったマークからスタックフレームのサイズや関数名を抽出していました。これは、バイナリコードを直接スキャンするようなアプローチでした。

変更後のアプローチ:

  1. SOFmarkの削除: src/cmd/6l/6.out.hからSOFmarkマクロが削除され、src/cmd/6l/pass.cからmarkstkおよびaddstackmark関数が完全に削除されました。これにより、リンカが実行可能ファイルにスタックマーク文字列を埋め込む処理がなくなりました。
  2. symtab.cの導入: src/runtime/symtab.cが新しく追加されました。このファイルは以下の主要な機能を提供します。
    • シンボルテーブルのロード: 実行可能ファイルに埋め込まれたシンボルテーブル(SYMDATA)と、そのサイズ情報(SYMCOUNTS)をメモリ上で扱えるようにします。
    • walksymtab関数: シンボルテーブルを走査し、各シンボルに対してコールバック関数を呼び出す汎用的なメカニズムを提供します。
    • buildfuncs関数: walksymtabを利用して、シンボルテーブルから関数に関する情報を抽出し、Func構造体の配列(func)を構築します。このFunc配列には、関数名、エントリポイントアドレス、フレームサイズなどが格納されます。
    • findfunc関数: 特定のアドレスがどの関数に属するかを、構築されたFunc配列をバイナリサーチで検索して特定します。
  3. traceback関数の変更: src/runtime/rt2_amd64.ctraceback関数が大幅に変更されました。
    • spmark(旧SOFmark)の利用が廃止されました。
    • 代わりに、findfunc関数を呼び出して現在のPC(プログラムカウンタ)に対応するFunc構造体を取得し、そこから関数名やフレームサイズなどの情報を取得するように変更されました。これにより、より正確で構造化されたデバッグ情報が得られるようになりました。
  4. 文字列処理の改善: src/runtime/iface.c, src/runtime/runtime.c, src/runtime/string.cにおいて、Cスタイルのヌル終端バイト配列からGoのstring型への変換にgostring関数が導入・活用されました。これにより、文字列の扱いが一貫し、安全性が向上しました。特に、sys·argvsys·envvといったシステムコール関連の関数で、引数や環境変数の文字列処理が簡素化されています。
  5. runtime.hの変更: Func構造体の定義が追加され、findnull関数の引数型がint8*からbyte*に変更されるなど、型定義が更新されました。

この変更により、Goランタイムは、実行可能ファイルに埋め込まれた冗長なデバッグ情報に依存することなく、より効率的かつ柔軟にシンボル情報を利用できるようになりました。これは、Goのデバッグ機能の基盤を強化し、将来的な拡張性を高める上で重要なステップでした。

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

1. src/cmd/6l/pass.c からのスタックマーク関連コードの削除

--- a/src/cmd/6l/pass.c
+++ b/src/cmd/6l/pass.c
@@ -293,100 +293,6 @@ byteq(int v)
 	return p;
 }
 
-void
-markstk(Prog *l)
-{
-	Prog *p0, *p, *q, *r;
-	int32 i, n, line;
-	Sym *s;
-
-	version++;
-	s = lookup(l->from.sym->name, version);
-	s->type = STEXT;
-	line = l->line;
-
-	// start with fake copy of ATEXT
-	p0 = prg();
-	p = p0;
-	*p = *l;	// note this gets p->pcond and p->line
-
-	p->from.type = D_STATIC;
-	p->from.sym = s;
-	p->to.offset = 0;
-
-	// put out magic sequence
-	n = strlen(SOFmark);
-	for(i=0; i<n; i++) {
-		q = byteq(SOFmark[i]);
-		q->line = line;
-		p->link = q;
-		p = q;
-	}
-
-	// put out stack offset
-	n = l->to.offset;
-	if(n < 0)
-		n = 0;
-	for(i=0; i<3; i++) {
-		q = byteq(n);
-		q->line = line;
-		p->link = q;
-		p = q;
-		n = n>>8;
-	}
-
-	// put out null terminated name
-	for(i=0;; i++) {
-		n = s->name[i];
-		q = byteq(n);
-		q->line = line;
-		p->link = q;
-		p = q;
-		if(n == 0)
-			break;
-	}
-
-	// put out return instruction
-	q = prg();
-	q->as = ARET;
-	q->line = line;
-	p->link = q;
-	p = q;
-
-	r = l->pcond;
-	l->pcond = p0;
-	p->link = r;
-	p0->pcond = r;
-
-	// hard part is linking end of
-	// the text body to my fake ATEXT
-	for(p=l;; p=q) {
-		q = p->link;
-		if(q == r) {
-			p->link = p0;
-			return;
-		}
-	}
-}
-
-void
-addstackmark(void)
-{
-	Prog *p;
-
-	if(debug['v'])
-		Bprint(&bso, "%5.2f stkmark\n", cputime());
-	Bflush(&bso);
-
-	for(p=textp; p!=P; p=p->pcond) {
-		markstk(p);		// splice in new body
-		p = p->pcond;		// skip the one we just put in
-	}
-
-//	for(p=textp; p!=P; p=p->pcond)
-//		print("%P\n", p);
-}
-
 int
 relinv(int a)
 {

2. src/runtime/symtab.c の新規追加

--- /dev/null
+++ b/src/runtime/symtab.c
@@ -0,0 +1,190 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "runtime.h"
+
+// Runtime symbol table access.
+// Very much a work in progress.
+
+#define SYMCOUNTS ((int32*)(0x99LL<<32))	// known to 6l
+#define SYMDATA ((byte*)(0x99LL<<32) + 8)
+
+// 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 = SYMCOUNTS;
+
+	a = mal(sizeof *a);
+	a->nel = v[0];
+	a->cap = a->nel;
+	a->array = SYMDATA;
+	symtab = a;
+	FLUSH(&symtab);
+
+	a = mal(sizeof *a);
+	a->nel = v[1];
+	a->cap = a->nel;
+	a->array = SYMDATA + v[0];
+	pclntab = a;
+	FLUSH(&pclntab);
+}
+
+typedef struct Sym Sym;
+struct Sym
+{
+	uint64 value;
+	byte symtype;
+	byte *name;
+	byte *gotype;
+};
+
+// Walk over symtab, calling fn(&s) for each symbol.
+void
+walksymtab(void (*fn)(Sym*))
+{
+	int32 *v;
+	byte *p, *ep, *q;
+	Sym s;
+
+	v = SYMCOUNTS;
+	p = SYMDATA;
+	ep = p + v[0];
+	while(p < ep) {
+		if(p + 7 > ep)
+			break;
+		s.value = ((uint32)p[0]<<24) | ((uint32)p[1]<<16) | ((uint32)p[2]<<8) | ((uint32)p[3]);
+		if(!(p[4]&0x80))
+			break;
+		s.symtype = p[4] & ~0x80;
+		p += 5;
+		if(s.symtype == 'z' || s.symtype == 'Z') {
+			// path reference string - skip first byte,
+			// then 2-byte pairs ending at two zeros.
+			// for now, just skip over it and ignore it.
+			q = p+1;
+			for(;;) {
+				if(q+2 > ep)
+					return;
+				if(q[0] == '\0' && q[1] == '\0')
+					break;
+				q += 2;
+			}
+			p = q+2;
+			s.name = nil;
+		}else{
+			q = mchr(p, '\0', ep);
+			if(q == nil)
+				break;
+			s.name = p;
+			p = q+1;
+		}
+		q = mchr(p, '\0', ep);
+		if(q == nil)
+			break;
+		s.gotype = p;
+		p = q+1;
+		fn(&s);
+	}
+}
+
+// Symtab walker; accumulates info about functions.
+
+Func *func;
+int32 nfunc;
+
+static void
+dofunc(Sym *sym)
+{
+	static byte *lastfuncname;
+	static Func *lastfunc;
+	Func *f;
+
+	if(lastfunc && sym->symtype == 'm') {
+		lastfunc->frame = sym->value;
+		return;
+	}
+	if(sym->symtype != 'T' && sym->symtype != 't')
+		return;
+	if(strcmp(sym->name, (byte*)"etext") == 0)
+		return;
+	if(func == nil) {
+		nfunc++;
+		return;
+	}
+
+	f = &func[nfunc++];
+	f->name = gostring(sym->name);
+	f->entry = sym->value;
+	lastfunc = f;
+}
+
+static void
+buildfuncs(void)
+{
+	extern byte etext[];
+
+	if(func != nil)
+		return;
+	nfunc = 0;
+	walksymtab(dofunc);
+	func = mal((nfunc+1)*sizeof func[0]);
+	nfunc = 0;
+	walksymtab(dofunc);
+	func[nfunc].entry = (uint64)etext;
+}
+
+Func*
+findfunc(uint64 addr)
+{
+	Func *f;
+	int32 i, nf, n;
+
+	if(func == nil)
+		buildfuncs();
+	if(nfunc == 0)
+		return nil;
+	if(addr < func[0].entry || addr >= func[nfunc].entry)
+		return nil;
+
+	// linear search, for debugging
+	if(0) {
+		for(i=0; i<nfunc; i++) {
+			if(func[i].entry <= addr && addr < func[i+1].entry)
+				return &func[i];
+		}
+		return nil;
+	}
+
+	// binary search to find func with entry <= addr.
+	f = func;
+	nf = nfunc;
+	while(nf > 0) {
+		n = nf/2;
+		if(f[n].entry <= addr && addr < f[n+1].entry)
+			return &f[n];
+		else if(addr < f[n].entry)
+			nf = n;
+		else {
+			f += n+1;
+			nf -= n+1;
+		}
+	}
+
+	// can't get here -- we already checked above
+	// that the address was in the table bounds.
+	// this can only happen if the table isn't sorted
+	// by address or if the binary search above is buggy.
+	prints("findfunc unreachable\n");
+	return nil;
+}

3. src/runtime/rt2_amd64.ctraceback 関数の変更

--- a/src/runtime/rt2_amd64.c
+++ b/src/runtime/rt2_amd64.c
@@ -6,8 +6,6 @@
 
 extern int32	debug;
 
-static int8 spmark[] = "\xa7\xf1\xd9\x2a\x82\xc8\xd8\xfe";
-
 extern uint8 end;
 
 void
@@ -18,7 +16,8 @@ traceback(uint8 *pc, uint8 *sp, void* r15)
 	uint8* callpc;
 	int32 counter;
 	int32 i;
-	int8* name;
+	string name;
+	Func *f;
 	G g;
 	Stktop *stktop;
 
@@ -33,7 +32,7 @@ traceback(uint8 *pc, uint8 *sp, void* r15)
 	}
 
 	counter = 0;
-	name = "panic";
+	name = gostring((byte*)"panic");
 	for(;;){
 		callpc = pc;
 		if((uint8*)retfromnewstack == pc) {
@@ -46,21 +45,11 @@ traceback(uint8 *pc, uint8 *sp, void* r15)
 			sp += 16;  // two irrelevant calls on stack - morestack, plus the call morestack made
 			continue;
 		}
-		/* find SP offset by stepping back through instructions to SP offset marker */
-		while(pc > (uint8*)0x1000+sizeof spmark-1) {
-			if(pc >= &end)
-				return;
-			for(spp = spmark; *spp != '\0' && *pc++ == (uint8)*spp++; )
-				;
-			if(*spp == '\0'){
-				spoff = *pc++;
-				spoff += *pc++ << 8;
-				spoff += *pc++ << 16;
-				name = (int8*)pc;
-				sp += spoff + 8;
-				break;
-			}
-		}
+		f = findfunc((uint64)callpc);
+		if(f == nil)
+			return;
+		name = f->name;
+		sp += f->frame;
 		if(counter++ > 100){
 			prints("stack trace terminated\n");
 			break;
@@ -73,7 +62,7 @@ traceback(uint8 *pc, uint8 *sp, void* r15)
 		sys·printpointer(callpc  - 1);	// -1 to get to CALL instr.
 		prints("?zi\n");
 		prints("\t");
-		prints(name);
+		sys·printstring(name);
 		prints("(");
 		for(i = 0; i < 3; i++){
 			if(i != 0)
@@ -82,7 +71,7 @@ traceback(uint8 *pc, uint8 *sp, void* r15)
 		}
 		prints(", ...)\n");
 		prints("\t");
-		prints(name);
+		sys·printstring(name);
 		prints("(");
 		for(i = 0; i < 3; i++){
 			if(i != 0)

コアとなるコードの解説

1. src/cmd/6l/pass.c からのスタックマーク関連コードの削除

この変更は、リンカが実行可能ファイルにスタックマーク文字列を埋め込む処理を完全に停止させるものです。

  • markstk関数は、個々の関数に対してスタックマーク文字列、スタックオフセット、関数名をバイナリコードとして挿入する役割を担っていました。
  • addstackmark関数は、すべての関数に対してmarkstkを呼び出し、スタックマークを適用していました。 これらの関数が削除されたことで、GoのバイナリからSOFmarkのような特殊なバイトシーケンスが消え、ファイルサイズ削減に貢献します。

2. src/runtime/symtab.c の新規追加

このファイルは、Goランタイムが実行時にシンボルテーブルを扱うための新しい基盤を提供します。

  • SYMCOUNTSSYMDATAマクロは、リンカによって特定のメモリ位置に配置されたシンボルテーブルの開始アドレスとサイズ情報へのポインタを定義しています。これは、Goの初期のリンカとランタイム間の暗黙的な取り決めを示しています。
  • sys·symdat関数は、シンボルテーブルとPC-Lineテーブル(pclntab)をArray構造体としてラップし、ランタイムがアクセスできるようにします。
  • Sym構造体は、シンボルテーブル内の個々のエントリ(値、型、名前、Go型情報)を表します。
  • walksymtab関数は、シンボルテーブルの生データを解析し、各シンボルに対してfnコールバック関数を呼び出します。これにより、シンボルテーブルの汎用的な走査が可能になります。
  • dofunc関数は、walksymtabのコールバックとして使用され、シンボルテーブルから関数シンボル(symtype == 'T'または't')を抽出し、Func構造体の配列funcに格納します。'm'タイプのシンボルは、関数のフレームサイズ情報を提供するために使用されます。
  • buildfuncs関数は、dofuncを2回呼び出すことで、func配列を構築します。1回目はnfunc(関数の数)をカウントするため、2回目は実際にFunc構造体を割り当ててデータを埋めるためです。
  • findfunc関数は、与えられたアドレスがどの関数に属するかを効率的に検索するために、func配列に対してバイナリサーチを実行します。これにより、PC(プログラムカウンタ)から関数情報を迅速に取得できるようになります。

このsymtab.cの導入により、ランタイムは実行時にシンボルテーブルを解析し、関数に関する詳細な情報を動的に取得できるようになり、スタックマーク文字列の必要性がなくなりました。

3. src/runtime/rt2_amd64.ctraceback 関数の変更

traceback関数は、パニック発生時などにスタックトレースを生成するGoランタイムの重要な部分です。

  • 変更前は、spmark(旧SOFmark)というバイト配列をスタック上で検索し、そのマークが見つかった位置からスタックオフセットや関数名を読み取っていました。これは、バイナリコードのパターンマッチングに似た原始的な方法でした。
  • 変更後は、static int8 spmark[]の定義が削除され、findfunc((uint64)callpc)が呼び出されるようになりました。これにより、現在のPC(callpc)に対応するFunc構造体(関数名、フレームサイズなどを含む)が取得されます。
  • 取得したFunc構造体からf->name(関数名)とf->frame(フレームサイズ)を直接利用することで、スタックポインタ(sp)を適切に進め、関数名を正確に表示できるようになりました。
  • prints(name)sys·printstring(name)に変更され、Goのstring型を安全に出力するようになりました。

この変更は、スタックトレースの生成が、バイナリ内の固定パターン検索から、構造化されたシンボルテーブルの動的なルックアップへと進化し、より堅牢で正確なデバッグ情報提供が可能になったことを示しています。

関連リンク

  • Go言語の初期の設計に関するドキュメントやメーリングリストのアーカイブは、Goプロジェクトの歴史的背景を理解する上で役立つ可能性があります。
  • Goのリンカ(cmd/link)やランタイム(runtime)の現在の実装に関する公式ドキュメントやソースコード。

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/6l, src/runtime ディレクトリ)
  • Go言語の初期のコミット履歴と関連する議論
  • Go言語のランタイムに関する一般的な情報源 (書籍、ブログ記事など)
  • シンボルテーブル、スタックトレース、リンカの動作に関するコンピュータサイエンスの一般的な知識