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

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

このコミットは、Go言語のランタイムにおいて、パニック発生時のトレースバック出力にソースファイル名と行番号を含めるように改善するものです。これにより、デバッグ時の情報が格段に豊富になり、問題の特定が容易になります。

コミット

commit a5433369aa6c1b0ca2380d34fd99b41529a613fe
Author: Russ Cox <rsc@golang.org>
Date:   Tue Nov 25 09:23:36 2008 -0800

    use pc/ln table to print source lines in traceback
    
    r45=; 6.out
    oops
    panic PC=0x400316
    0x400316?zi /home/rsc/go/src/runtime/rt0_amd64_linux.s:83
            main·g(4195177, 0, 4205661, ...)
            main·g(0x400369, 0x402c5d, 0x403e49, ...)
    0x40034c?zi /home/rsc/go/src/runtime/x.go:24
            main·f(4205661, 0, 4210249, ...)
            main·f(0x402c5d, 0x403e49, 0x1, ...)
    0x400368?zi /home/rsc/go/src/runtime/x.go:37
            main·main(4210249, 0, 1, ...)
            main·main(0x403e49, 0x1, 0x7fff9d894bd8, ...)
    0x402c5c?zi /home/rsc/go/src/runtime/rt0_amd64.s:70
            mainstart(1, 0, 2643020760, ...)
            mainstart(0x1, 0x7fff9d894bd8, 0x0, ...)
    r45=;
    
    R=r
    DELTA=251  (198 added, 25 deleted, 28 changed)
    OCL=19965
    CL=19979

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

https://github.com/golang/go/commit/a5433369aa6c1b0ca2380d34fd99b41529a613fe

元コミット内容

このコミットの目的は、トレースバック(スタックトレース)の出力に、プログラムカウンタ(PC)のアドレスだけでなく、対応するソースファイル名と行番号を表示することです。コミットメッセージには、変更前と変更後のトレースバックの例が示されており、変更後には /home/rsc/go/src/runtime/rt0_amd64_linux.s:83 のようにファイル名と行番号が追加されていることがわかります。

変更の背景

Go言語の初期段階において、パニック発生時のトレースバックは、関数名とプログラムカウンタ(PC)のアドレスのみを提供していました。これはデバッグを行う上で非常に不便であり、どのソースコードのどの行で問題が発生したのかを特定するためには、別途デバッガを使用するか、PCアドレスを手動で逆アセンブルして対応するソースコードを探す必要がありました。

このコミットは、このデバッグ体験を大幅に改善するために導入されました。コンパイラとリンカが生成するPC/行番号(PC/ln)テーブルを利用することで、ランタイムが実行時にPCアドレスからソースファイルと行番号を動的に解決し、トレースバックに表示できるようにすることが目的です。これにより、開発者はより迅速かつ効率的にバグの原因を特定できるようになります。

特に、コミットメッセージには「Plan 9 symbol table is not in a particularly convenient form.」や「eventually we'll change 6l to do this for us」といった記述があり、当時のGoのビルドツールチェイン(特にリンカである6l)が生成するシンボルテーブルの形式が、この目的には最適ではなかったこと、そして将来的にはリンカ側でより使いやすい形式に改善する意図があったことが伺えます。このコミットは、その過渡期におけるランタイム側での対応として位置づけられます。

前提知識の解説

プログラムカウンタ (PC)

プログラムカウンタ(Program Counter, PC)は、CPUが次に実行する命令のアドレスを保持するレジスタです。トレースバックでは、各スタックフレームにおける関数の呼び出し元アドレス(リターンアドレス)としてPCが表示されます。

スタックトレース(トレースバック)

スタックトレース(またはトレースバック)は、プログラムがエラーや例外(Goではパニック)で停止した際に、その時点での関数呼び出しの履歴(コールスタック)を表示するものです。これにより、どの関数がどの関数を呼び出し、最終的にどこで問題が発生したのかを追跡できます。

シンボルテーブル

シンボルテーブルは、コンパイルされたプログラム内に含まれる、関数名、変数名、ファイル名などのシンボルと、それらがメモリ上のどこに配置されているか(アドレス)をマッピングしたデータ構造です。デバッガやプロファイラ、そしてこのコミットのようにランタイムがデバッグ情報を解決するために利用します。

PC/行番号 (PC/ln) テーブル

PC/行番号テーブルは、プログラムカウンタ(PC)のアドレスと、対応するソースコードのファイル名および行番号をマッピングしたデータ構造です。コンパイラやリンカによって生成され、デバッグ情報の一部として実行ファイルに埋め込まれます。このテーブルは、デバッガが実行中のプログラムのどの部分がソースコードのどの行に対応するかを特定するために不可欠です。Go言語のランタイムも、このテーブルを利用してトレースバックにソースコードの情報を表示します。

Go言語のランタイム

Go言語のランタイムは、Goプログラムの実行を管理する低レベルのコード群です。ガベージコレクション、スケジューラ、メモリ管理、そしてパニック処理やトレースバックの生成など、多岐にわたる機能を提供します。このコミットは、そのランタイムの一部であるトレースバック生成ロジックに手を入れるものです。

Plan 9 と 6l

Go言語は、ベル研究所のPlan 9オペレーティングシステムの設計思想やツールチェインから大きな影響を受けています。6lは、Go言語の初期のリンカ(go tool linkの内部で使われるツール)の一つで、Plan 9のツールチェインに由来します。このコミットの時点では、6lが生成するシンボルテーブルの形式が、PC/ln情報の効率的な利用には最適ではなかったことが示唆されています。

技術的詳細

このコミットの核心は、Goランタイムが実行ファイルに埋め込まれたPC/lnテーブルを解析し、PCアドレスからソースファイル名と行番号を動的に取得するメカニズムを導入した点にあります。

  1. Func 構造体の拡張: src/runtime/runtime.h に定義されている Func 構造体(Goの関数に関するメタデータを保持)に、以下のフィールドが追加されました。

    • string src;: ソースファイル名。
    • Array pcln;: この関数に対応するPC/lnテーブルのデータ部分。Array はGoランタイム内部で使われる動的配列のような型です。
    • int64 pc0;: この関数のPC/lnテーブルが始まるPCアドレス。
    • int32 ln0;: この関数のPC/lnテーブルが始まる行番号。
  2. シンボルテーブル解析の強化 (src/runtime/symtab.c): symtab.c は、実行ファイル内のシンボルテーブルを解析し、ランタイムが利用できる形式に変換する役割を担っています。このコミットで、以下の重要な関数が追加・修正されました。

    • dofunc(Sym *sym): シンボルテーブルを走査し、関数('t'または'T'シンボル)やフレームサイズ('m'シンボル)、ファイル名('f'シンボル)に関する情報を収集します。特に、'f'シンボルからファイル名のリスト (fname) を構築するようになりました。

    • makepath(byte *buf, int32 nbuf, byte *path): 'z'シンボル(パス参照文字列)と、dofuncで収集したファイル名のリスト (fname) を組み合わせて、完全なソースファイルパスを再構築するユーティリティ関数です。'z'シンボルは、インクルードパスの履歴をエンコードしており、この関数がそれをデコードします。

    • dosrcline(Sym *sym): シンボルテーブルを再度走査し、各関数 ('t'または'T'シンボル)にソースファイル名 (src) とベースとなる行番号 (ln0) を関連付けます。'z'シンボルを処理することで、ソースファイルの切り替わり(GoにはCのような#includeはありませんが、コンパイラが内部的にソースファイルを処理する際のパス情報)を追跡し、正確な行番号のオフセットを計算します。

    • splitpcln(void): これがPC/lnテーブル処理の核心です。実行ファイル全体に存在する単一のPC/lnテーブルを、各関数に対応する小さな部分テーブルに分割し、それぞれの Func 構造体の pcln フィールドに格納します。この関数は、PC/lnテーブルのバイト列をデコードし、PCアドレスと行番号の増減を追跡しながら、各関数の開始PC (pc0) と開始行番号 (ln0) を特定します。PC/lnテーブルは非常にコンパクトな形式でエンコードされており、PCの増分と行番号の増減をバイト単位で表現しています。

    • funcline(Func *f, uint64 targetpc): 指定された関数 f とターゲットPCアドレス targetpc を受け取り、そのPCアドレスに対応するソースコードの行番号を返します。この関数は、f->pcln に格納されたPC/lnテーブルのバイト列をデコードし、targetpc に到達するまでのPCと行番号の変化をシミュレートすることで、最終的な行番号を計算します。

  3. トレースバック出力の変更 (src/runtime/rt2_amd64.c): traceback 関数は、スタックフレームを走査し、各フレームのPCアドレスから関数情報を取得します。このコミットでは、traceback 関数内で findfunc を呼び出して Func 構造体を取得した後、新しく追加された funcline 関数を呼び出すことで、そのPCアドレスに対応する正確なソース行番号を取得し、出力に含めるようになりました。

    変更前:

    prints("?zi\\n");
    

    変更後:

    prints("?zi ");
    sys·printstring(f->src);
    prints(":");
    sys·printint(funcline(f, (uint64)callpc-1)); // -1 to get to CALL instr.
    prints("\\n");
    

    これにより、?zi の後にソースファイル名と行番号が追加されるようになりました。-1 しているのは、callpc が呼び出し命令の次の命令を指すため、呼び出し命令自体のアドレスに戻すためです。

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

src/runtime/runtime.h

Func 構造体に src, pcln, pc0, ln0 フィールドが追加され、funcline 関数のプロトタイプが宣言されました。

--- a/src/runtime/runtime.h
+++ b/src/runtime/runtime.h
@@ -189,9 +189,13 @@ struct	SigTab
 struct	Func
 {
 	string	name;
-	string	type;
-	uint64	entry;
-	int64	frame;
+	string	type;	// go type string
+	string	src;	// src file name
+	uint64	entry;	// entry pc
+	int64	frame;	// stack frame size
+	Array	pcln;	// pc/ln tab for this func
+	int64	pc0;	// starting pc, ln for table
+	int32	ln0;
 };
 
 /*
@@ -261,6 +265,7 @@ void	signalstack(byte*, int32);
 G*	malg(int32);
 void	minit(void);
 Func*	findfunc(uint64);
+int32	funcline(Func*, uint64);
 
 /*
  * mutual exclusion locks.  in the uncontended case,

src/runtime/rt2_amd64.c

traceback 関数内で、ソースファイル名と行番号を出力するロジックが追加されました。

--- a/src/runtime/rt2_amd64.c
+++ b/src/runtime/rt2_amd64.c
@@ -11,8 +11,6 @@ extern uint8 end;
 void
 traceback(uint8 *pc, uint8 *sp, void* r15)
 {
-	int32 spoff;
-	int8* spp;
 	uint8* callpc;
 	int32 counter;
 	int32 i;
@@ -60,7 +58,11 @@ traceback(uint8 *pc, uint8 *sp, void* r15)
 		/* print this frame */
 		prints("0x");
 		sys·printpointer(callpc  - 1);	// -1 to get to CALL instr.
-		prints("?zi\\n");
+		prints("?zi ");
+		sys·printstring(f->src);
+		prints(":");
+		sys·printint(funcline(f, (uint64)callpc-1));	// -1 to get to CALL instr.
+		prints("\\n");
 		prints("\t");
 		sys·printstring(name);
 		prints("(");

src/runtime/symtab.c

シンボルテーブルの解析とPC/lnテーブルの処理に関する大幅な変更が行われました。特に、dofunc, makepath, dosrcline, splitpcln, funcline といった新しい静的関数が追加され、buildfuncs 関数がこれらの新しいロジックを呼び出すように修正されました。

--- a/src/runtime/symtab.c
+++ b/src/runtime/symtab.c
@@ -2,21 +2,22 @@
 // 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.  Work in progress.
+// The Plan 9 symbol table is not in a particularly convenient form.
+// The routines here massage it into a more usable form; eventually
+// we'll change 6l to do this for us, but it is easier to experiment
+// here than to change 6l and all the other tools.
+//
+// The symbol table also needs to be better integrated with the type
+// strings table in the future.  This is just a quick way to get started
+// and figure out exactly what we want.
 
-// Runtime symbol table access.
-// Very much a work in progress.
+#include "runtime.h"
 
 #define SYMCOUNTS ((int32*)(0x99LL<<32))\t// 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)
 {
@@ -50,7 +51,7 @@ struct Sym
 };
 
 // Walk over symtab, calling fn(&s) for each symbol.
-void
+static void
 walksymtab(void (*fn)(Sym*))\n {
 	int32 *v;
 	byte *p, *ep, *q;
@@ -68,10 +69,10 @@ walksymtab(void (*fn)(Sym*))
 			break;
 		s.symtype = p[4] & ~0x80;
 		p += 5;
+		s.name = p;
 		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)
@@ -81,12 +82,10 @@ walksymtab(void (*fn)(Sym*))
 				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);
@@ -100,33 +99,198 @@ walksymtab(void (*fn)(Sym*))
 
 // Symtab walker; accumulates info about functions.
 
-Func *func;
-int32 nfunc;
+static Func *func;
+static int32 nfunc;
+
+static byte **fname;
+static int32 nfname;
 
 static void
 dofunc(Sym *sym)
 {
-	static byte *lastfuncname;
-	static Func *lastfunc;
 	Func *f;
 
-	if(lastfunc && sym->symtype == 'm') {
-		lastfunc->frame = sym->value;
-		return;
+	switch(sym->symtype) {
+	case 't':
+	case 'T':
+		if(strcmp(sym->name, (byte*)"etext") == 0)
+			break;
+		if(func == nil) {
+			nfunc++;
+			break;
+		}
+		f = &func[nfunc++];
+		f->name = gostring(sym->name);
+		f->entry = sym->value;
+		break;
+	case 'm':
+		if(nfunc > 0 && func != nil)
+			func[nfunc-1].frame = sym->value;
+		break;
+	case 'f':
+		if(fname == nil) {
+			if(sym->value >= nfname)
+				nfname = sym->value+1;
+			break;
+		}
+		fname[sym->value] = sym->name;
+		break;
 	}
-	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;
+}
+
+// put together the path name for a z entry.
+// the f entries have been accumulated into fname already.
+static void
+makepath(byte *buf, int32 nbuf, byte *path)
+{
+	int32 n, len;
+	byte *p, *ep, *q;
+
+	if(nbuf <= 0)
+		return;
+
+	p = buf;
+	ep = buf + nbuf;
+	*p = '\0';
+	for(;;) {
+		if(path[0] == 0 && path[1] == 0)
+			break;
+		n = (path[0]<<8) | path[1];
+		path += 2;
+		if(n >= nfname)
+			break;
+		q = fname[n];
+		len = findnull(q);
+		if(p+1+len >= ep)
+			break;
+		if(p > buf && p[-1] != '/')
+			*p++ = '/';
+		mcpy(p, q, len+1);
+		p += len;
+	}
+}
+
+// walk symtab accumulating path names for use by pc/ln table.
+// don't need the full generality of the z entry history stack because
+// there are no includes in go (and only sensible includes in our c).
+static void
+dosrcline(Sym *sym)
+{
+	static byte srcbuf[1000];
+	static string srcstring;
+	static int32 lno, incstart;
+	static int32 nf, nhist;
+	Func *f;
+
+	switch(sym->symtype) {
+	case 't':
+	case 'T':
+		f = &func[nf++];
+		f->src = srcstring;
+		f->ln0 += lno;
+		break;
+	case 'z':
+		if(sym->value == 1) {
+			// entry for main source file for a new object.
+			makepath(srcbuf, sizeof srcbuf, sym->name+1);
+			srcstring = gostring(srcbuf);
+			lno = 0;
+			nhist = 0;
+		} else {
+			// push or pop of included file.
+			makepath(srcbuf, sizeof srcbuf, sym->name+1);
+			if(srcbuf[0] != '\0') {
+				if(nhist++ == 0)
+					incstart = sym->value;
+			}else{
+				if(--nhist == 0)
+					lno -= sym->value - incstart;
+			}
+		}
+	}
+}
+
+enum { PcQuant = 1 };
+
+// Interpret pc/ln table, saving the subpiece for each func.
+static void
+splitpcln(void)
+{
+	int32 line;
+	uint64 pc;
+	byte *p, *ep;
+	Func *f, *ef;
+	int32 *v;
+
+	// pc/ln table bounds
+	v = SYMCOUNTS;
+	p = SYMDATA;
+	p += v[0];
+	ep = p+v[1];
+
+	f = func;
+	ef = func + nfunc;
+	f->pcln.array = p;
+	pc = func[0].entry;	// text base
+	line = 0;
+	for(; p < ep; p++) {
+		if(f < ef && pc >= (f+1)->entry) {
+			f->pcln.nel = p - f->pcln.array;
+			f->pcln.cap = f->pcln.nel;
+			f++;
+			f->pcln.array = p;
+			f->pc0 = pc;
+			f->ln0 = line;
+		}
+		if(*p == 0) {
+			// 4 byte add to line
+			line += (p[1]<<24) | (p[2]<<16) | (p[3]<<8) | p[4];
+			p += 4;
+		} else if(*p <= 64) {
+			line += *p;
+		} else if(*p <= 128) {
+			line -= *p - 64;
+		} else {
+			pc += PcQuant*(*p - 129);
+		}
+		pc += PcQuant;
+	}
+	if(f < ef) {
+		f->pcln.nel = p - f->pcln.array;
+		f->pcln.cap = f->pcln.nel;
+	}
+}
+
+
+// Return actual file line number for targetpc in func f.
+// (Source file is f->src.)
+int32
+funcline(Func *f, uint64 targetpc)
+{
+	byte *p, *ep;
+	uint64 pc;
+	int32 line;
+
+	p = f->pcln.array;
+	ep = p + f->pcln.nel;
+	pc = f->pc0;
+	line = f->ln0;
+	for(; p < ep; p++) {
+		if(pc >= targetpc)
+			return line;
+		if(*p == 0) {
+			line += (p[1]<<24) | (p[2]<<16) | (p[3]<<8) | p[4];
+			p += 4;
+		} else if(*p <= 64) {
+			line += *p;
+		} else if(*p <= 128) {
+			line -= *p - 64;
+		} else {
+			pc += PcQuant*(*p - 129);
+		}
+		pc += PcQuant;
+	}
+	return line;
 }
 
 static void
@@ -136,19 +300,30 @@ buildfuncs(void)
 
 	if(func != nil)
 		return;
+	// count funcs, fnames
 	nfunc = 0;
+	nfname = 0;
 	walksymtab(dofunc);
-
-	// initialize tables
+
+	// initialize tables
 	func = mal((nfunc+1)*sizeof func[0]);
+	func[nfunc].entry = (uint64)etext;
+	fname = mal(nfname*sizeof fname[0]);
 	nfunc = 0;
 	walksymtab(dofunc);
-	func[nfunc].entry = (uint64)etext;
+
+	// split pc/ln table by func
+	splitpcln();
+
+	// record src file and line info for each func
+	walksymtab(dosrcline);
 }
 
 Func*
 findfunc(uint64 addr)
 {
 	Func *f;
-	int32 i, nf, n;
+	int32 nf, n;
 
 	if(func == nil)
 		buildfuncs();
@@ -157,15 +342,6 @@ findfunc(uint64 addr)
 	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;

コアとなるコードの解説

このコミットの主要な変更は、src/runtime/symtab.c に集約されています。

  1. Func 構造体の拡張: Func 構造体は、Goの各関数に関するメタデータ(名前、エントリポイントのアドレスなど)を保持します。このコミットで、ソースファイル名 (src) と、その関数に特化したPC/lnテーブルのデータ (pcln, pc0, ln0) を保持するようになりました。これにより、各関数が自身のソースコード位置情報を直接参照できるようになります。

  2. dofuncfname: dofunc 関数は、シンボルテーブルを一度走査し、関数情報だけでなく、'f' シンボル(ファイル名)も収集して fname 配列に格納します。これは、後でパスを再構築する際に参照されます。

  3. makepath: makepath は、'z' シンボル(パス参照)が示すインデックスと fname 配列を使って、ソースファイルの完全なパスを生成します。'z' シンボルは、ファイルパスの各要素をインデックスとして参照する形式でエンコードされており、この関数がそれをデコードして人間が読めるパスに変換します。

  4. dosrcline: dosrcline は、シンボルテーブルを再度走査し、各関数に正しいソースファイル名と、その関数が始まる行番号のオフセットを関連付けます。'z' シンボルを処理することで、コンパイル時にソースファイルが切り替わった際の行番号の調整を行います。

  5. splitpcln: この関数は、実行ファイル全体に存在するPC/lnテーブルを、各関数に対応する部分テーブルに分割し、それぞれの Func 構造体の pcln フィールドに格納します。PC/lnテーブルは、PCアドレスと行番号の変化を非常にコンパクトな形式でエンコードしています。例えば、*p == 0 の場合は4バイトで大きな行番号の増分を、*p <= 64 の場合は1バイトで小さな行番号の増分を、*p > 128 の場合はPCの増分を表現しています。splitpcln はこのエンコーディングを解釈し、各関数のエントリポイントに基づいてテーブルを分割します。

  6. funcline: funcline は、特定の関数 f とその関数内のPCアドレス targetpc を受け取り、対応するソースコードの行番号を計算して返します。これは、splitpcln によって各関数に割り当てられた f->pcln テーブルを、targetpc に到達するまでデコードすることで実現されます。この関数が、トレースバックで表示される行番号の計算の要となります。

  7. traceback 関数の変更: 最終的に、src/runtime/rt2_amd64.ctraceback 関数が、findfunc で取得した Func 構造体と funcline 関数を利用して、トレースバック出力にソースファイル名と行番号を追加するように変更されました。これにより、デバッグ情報が大幅に強化されました。

これらの変更により、Goのランタイムは、コンパイル時に生成されたPC/lnテーブルを効率的に利用し、実行時のトレースバックに詳細なソースコード位置情報を提供できるようになりました。

関連リンク

  • Go言語の初期のランタイムに関する議論やドキュメントは、現在のGoのドキュメントサイトでは見つけにくい場合があります。当時のGoのメーリングリストや、Goのソースコードリポジトリの初期のコミットログが参考になることがあります。
  • Goのシンボルテーブルの進化については、Goの公式ブログや設計ドキュメントで言及されることがあります。

参考にした情報源リンク

  • Go言語のソースコード (特に src/runtime ディレクトリ)
  • Go言語の初期のコミット履歴 (GitHub: golang/go リポジトリ)
  • コンパイラとリンカの基本的な概念(シンボルテーブル、デバッグ情報、PC/lnテーブルなど)に関する一般的な情報源。
  • Plan 9 オペレーティングシステムとGo言語の関連性に関する情報。
  • Goのトレースバックに関する公式ドキュメントやブログ記事(現在のもの)。