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

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

このコミットは、Goコンパイラ(特に6g)におけるPlan 9スタイルの行番号とラインテーブルの取り扱いに関する重要な変更を導入しています。これは、Goの初期開発段階において、デバッグ情報の精度とコンパイラの内部処理の改善を目指したものです。

コミット

commit efec14bc5af7f1f43b6e736a4fa2138ea5a328a2
Author: Ken Thompson <ken@golang.org>
Date:   Fri Jun 13 18:16:23 2008 -0700

    plan9 line numbers and line table

    SVN=122793

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

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

元コミット内容

plan9 line numbers and line table

SVN=122793

変更の背景

Go言語の初期のコンパイラツールチェインは、ベル研究所で開発されたオペレーティングシステムであるPlan 9のツールチェインから大きな影響を受けています。Goの主要な設計者であるRob PikeとKen Thompsonは、Plan 9の開発に深く関わっていました。このコミットは、Goコンパイラが生成するバイナリにおけるデバッグ情報の質、特にソースコードの行番号とファイルパスの管理を改善することを目的としています。

当時のGoコンパイラ(6gなど)は、コンパイルされたコードと元のソースコードの対応付けを行うためのラインテーブルの管理がまだ発展途上でした。正確な行番号情報は、デバッガがプログラムの実行をソースコードレベルで追跡したり、エラーメッセージが正確なファイルと行を指し示したりするために不可欠です。この変更は、コンパイラがより堅牢で詳細な行番号履歴を保持し、出力に反映できるようにするための基盤を築いています。

具体的には、curio.linenoのようなグローバル変数で管理されていた行番号を、より柔軟なlineno変数に移行し、さらにHist構造体を用いた履歴管理メカニズムを導入することで、インクルードファイルやマクロ展開など、複雑なソースコードの変換過程における行番号の追跡を改善しようとしています。

前提知識の解説

  • Plan 9ツールチェイン: Go言語の初期のコンパイラ(6g, 8gなど)は、Plan 9オペレーティングシステムのコンパイラ設計思想を強く継承していました。これは、シンプルさ、モジュール性、そしてクロスコンパイルの容易さを特徴としています。
  • コンパイラのラインテーブル: コンパイラは、ソースコードを機械語に変換する際に、元のソースファイルのどの行が生成された機械語のどの部分に対応するかを記録する「ラインテーブル」と呼ばれるデータ構造を生成します。この情報は、デバッガがソースコードレベルでのステップ実行、ブレークポイントの設定、スタックトレースの表示などを行うために不可欠です。
  • 6gコンパイラ: Go言語の初期のコンパイラの一つで、AMD64アーキテクチャ(64-bit x86)をターゲットとしていました。Go 1.5以降は、go tool compileに統合され、6gというコマンド名は使われなくなりましたが、Goコンパイラの歴史において重要な役割を果たしました。
  • Biobuf: Plan 9のlibbioライブラリに由来するバッファリングI/Oの構造体です。Goコンパイラの初期の実装では、ファイルの読み書きにこのBiobufが使われていました。
  • Fmt: Plan 9のlibfmtライブラリに由来するフォーマット出力のための構造体です。C言語のprintfに似た機能を提供しますが、より拡張性があります。このコミットでは、新しい行番号のフォーマット指定子%Lが追加されています。
  • Hist構造体: このコミットで導入された新しいデータ構造で、ファイル名と行番号の履歴を記録するために使用されます。これにより、コンパイラは複数のファイルにまたがる処理(例: インクルード)や、#lineディレクティブによる行番号の変更を正確に追跡できるようになります。

技術的詳細

このコミットの主要な技術的変更点は、コンパイラがソースコードの行番号情報をどのように管理し、出力するかという点にあります。

  1. 行番号管理の改善:

    • 以前はcurio.linenoというIo構造体内のフィールドで現在の行番号を管理していましたが、このコミットではグローバルなlineno変数を導入し、より直接的に行番号を更新するように変更されています。
    • curio.infileも同様に、infileというグローバル変数や、linehist関数に渡される引数として管理されるようになっています。
    • getcungetcgetnscといった文字読み込み関数内でcurio.linenoの代わりにlinenoがインクリメント/デクリメントされるよう修正されています。
  2. ライン履歴(Line History)の導入:

    • Histという新しい構造体が定義されました。これには、ファイル名 (name)、行番号 (line)、オフセット (offset)、そして次の履歴エントリへのリンク (link) が含まれます。
    • linehist関数が追加され、ソースファイルの切り替え時(例: importfileunimportfilecannedimports)や、コンパイルの開始・終了時に呼び出され、Hist構造体のリンクリストを構築します。このリンクリストは、コンパイラが処理したソースファイルの履歴と、その時点での行番号を記録します。
    • histehistというグローバル変数が導入され、Histリンクリストの先頭と末尾を指します。
  3. 新しいフォーマット指定子 %L の追加:

    • src/cmd/gc/go.hsrc/cmd/gc/subr.cLconv関数が追加され、Fmtライブラリに新しいフォーマット指定子%Lが登録されました。
    • %Lは、エラーメッセージや警告メッセージの出力時に、現在のファイルと行番号をより詳細な形式で表示するために使用されます。特に、インクルードされたファイルや#lineディレクティブによって変更された行番号の履歴を辿って表示する機能を持っています。
    • Lconv関数は、Histリンクリストを逆順に辿り、現在の行番号がどのファイルのどの行に由来するかを計算し、filename:linenoの形式で出力します。これにより、デバッグ時の情報が格段に向上します。
  4. コンパイラ出力へのラインテーブル情報の追加:

    • src/cmd/6g/obj.cdumpobj関数にouthist(bout)の呼び出しが追加されました。これは、コンパイルされたオブジェクトファイルにライン履歴情報を埋め込むためのものです。
    • outhist関数は、Histリンクリストを走査し、各履歴エントリをAHISTORYアセンブリ命令としてオブジェクトファイルに書き込みます。これにより、デバッガや他のツールがバイナリからソースコードの行番号情報を抽出できるようになります。
  5. クリーンアップスクリプトの更新:

    • src/clean.bashsrc/cmd/clean.bashが更新され、新しく追加されたライブラリやツール(libmach_amd64db)のクリーンアップ処理が追加されました。

これらの変更により、Goコンパイラはより正確で詳細なデバッグ情報を生成できるようになり、Goプログラムのデバッグ体験が向上しました。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  • src/cmd/6g/gg.h: Hist構造体の前方宣言と、histzprogなどのグローバル変数の宣言が追加されています。また、outhist関数のプロトタイプ宣言も追加されています。
  • src/cmd/6g/list.c: Pconv関数内のsnprint呼び出しが変更され、出力されるアセンブリコードの行にp->lineno(ソースコードの行番号)が含まれるようになりました。これにより、アセンブリリストとソースコードの対応がより明確になります。
  • src/cmd/6g/obj.c:
    • dumpobj関数内でouthist(bout)が呼び出されるようになり、オブジェクトファイルにライン履歴情報が書き込まれるようになりました。
    • outhist関数が新規追加され、Histリンクリストを走査し、AHISTORY命令として履歴情報をオブジェクトファイルに書き込むロジックが実装されています。
  • src/cmd/gc/go.h:
    • Hist構造体の定義が追加されました。
    • linenopathnamehistehistといったグローバル変数が宣言されました。
    • linehist関数とLconv関数のプロトタイプ宣言が追加されました。
  • src/cmd/gc/lex.c:
    • mainlex関数内でpathnameの初期化とlinehistの呼び出しが追加され、コンパイル開始時のファイル履歴が記録されるようになりました。
    • fmtinstall('L', Lconv)により、新しいフォーマット指定子%Lが登録されました。
    • getcungetcgetnsc関数内で、行番号の更新がcurio.linenoからグローバルなlineno変数に変更されました。
    • importfileunimportfilecannedimports関数内でlinehistが適切に呼び出され、インクルードファイルの切り替え時にも行番号履歴が更新されるようになりました。
  • src/cmd/gc/subr.c:
    • yyerrorwarnfatal関数内で、エラーメッセージの出力形式が%s:%ld:から%L:に変更され、新しい%Lフォーマット指定子を使用するようになりました。
    • linehist関数が新規追加され、行番号履歴を管理するロジックが実装されました。
    • Lconv関数が新規追加され、%Lフォーマット指定子の実際の処理(Histリンクリストを辿ってファイルと行番号の履歴を整形して出力)が実装されました。
  • src/clean.bash および src/cmd/clean.bash: クリーンアップ対象に新しいライブラリやツールが追加されました。

コアとなるコードの解説

src/cmd/gc/go.h における Hist 構造体

typedef	struct	Hist	Hist;
struct	Hist
{
	Hist*	link;
	char*	name;
	long	line;
	long	offset;
};
#define	H	((Hist*)0)

このHist構造体は、ソースファイルの履歴を記録するためのものです。

  • link: 次のHistエントリへのポインタ。これによりリンクリストが形成されます。
  • name: ソースファイルの名前(パス)。
  • line: そのファイルが開始された時点での論理的な行番号。
  • offset: ファイル内のバイトオフセット(このコミットでは主に0)。 Hはリンクリストの終端を示すNULLポインタのエイリアスです。

src/cmd/gc/subr.c における linehist 関数

void
linehist(char *file, long off)
{
	Hist *h;

	if(debug['i'])
	if(file != nil)
		print("%L: import %s\n", file);
	else
		print("%L: <eof>\n");

	h = alloc(sizeof(Hist));
	h->name = file;
	h->line = lineno; // 現在のグローバルな行番号を記録
	h->offset = off;
	h->link = H;
	if(ehist == H) { // 最初の履歴エントリの場合
		hist = h;
		ehist = h;
		return;
	}
	ehist->link = h; // 既存のリンクリストの末尾に追加
	ehist = h;
}

linehist関数は、新しいソースファイルが読み込まれる際(importなど)や、ファイルの読み込みが終了する際に呼び出されます。現在のグローバルな行番号linenoとファイル名fileHist構造体に保存し、histehist(履歴リストの先頭と末尾)を使ってリンクリストを構築します。これにより、コンパイラはどのファイルがいつ、どの行から処理されたかの履歴を保持できます。

src/cmd/gc/subr.c における Lconv 関数

int
Lconv(Fmt *fp)
{
	char str[STRINGSZ], s[STRINGSZ];
	struct
	{
		Hist*	incl;	/* start of this include file */
		long	idel;	/* delta line number to apply to include */
		Hist*	line;	/* start of this #line directive */
		long	ldel;	/* delta line number to apply to #line */
	} a[HISTSZ]; // 履歴スタック
	long lno, d;
	int i, n;
	Hist *h;

	lno = dynlineno; // 動的な行番号(もしあれば)
	if(lno == 0)
		lno = lineno; // なければグローバルな行番号

	n = 0;
	for(h=hist; h!=H; h=h->link) { // 履歴リストを走査
		if(lno < h->line) // 現在の行番号が履歴エントリの開始行より小さい場合、その履歴は関係ない
			break;
		if(h->name) { // ファイルの開始エントリの場合
			if(n < HISTSZ) {
				a[n].incl = h;
				a[n].idel = h->line;
				a[n].line = 0;
			}
			n++;
			continue;
		}
		n--; // ファイルの終了エントリの場合
		if(n > 0 && n < HISTSZ) {
			d = h->line - a[n].incl->line;
			a[n-1].ldel += d;
			a[n-1].idel += d;
		}
	}

	if(n > HISTSZ)
		n = HISTSZ;

	str[0] = 0;
	for(i=n-1; i>=0; i--) { // 履歴スタックを逆順に辿って出力文字列を構築
		if(i != n-1) {
			if(fp->flags & ~(FmtWidth|FmtPrec))
				break;
			strcat(str, " ");
		}
		if(a[i].line) // #line ディレクティブによる行番号変更がある場合
			snprint(s, STRINGSZ, "%s:%ld[%s:%ld]",
				a[i].line->name, lno-a[i].ldel+1,
				a[i].incl->name, lno-a[i].idel+1);
		else // 通常のファイルインクルードの場合
			snprint(s, STRINGSZ, "%s:%ld",
				a[i].incl->name, lno-a[i].idel+1);
		if(strlen(s)+strlen(str) >= STRINGSZ-10)
			break;
		strcat(str, s);
		lno = a[i].incl->line - 1; // 次の履歴エントリの開始行を計算
	}
	if(n == 0)
		strcat(str, "<eof>"); // 履歴がない場合

	return fmtstrcpy(fp, str);
}

Lconv関数は、Fmtライブラリのカスタムフォーマット指定子%Lのハンドラです。この関数は、現在の論理的な行番号(dynlinenoまたはlineno)に基づいて、Histリンクリストを走査し、その行がどのファイルのどの行に由来するのかを特定します。そして、その履歴を「ファイル名:行番号」の形式で整形して出力します。これにより、エラーメッセージや警告メッセージが、インクルードされたファイルや#lineディレクティブによる行番号の変更を考慮した、より詳細な情報を提供するようになります。

src/cmd/6g/obj.c における outhist 関数

void
outhist(Biobuf *b)
{
	Hist *h;
	char *p, *q, *op;
	Prog pg;
	int n;

	pg = zprog; // ゼロ初期化されたProg構造体
	pg.as = AHISTORY; // 履歴を示すアセンブリ命令タイプ
	for(h = hist; h != H; h = h->link) { // 履歴リストを走査
		p = h->name;
		op = 0;

		// パス名の処理(Plan 9のパス形式を考慮)
		if(p && p[0] != '/' && h->offset == 0 && pathname && pathname[0] == '/') {
			op = p;
			p = pathname;
		}

		while(p) {
			q = utfrune(p, '/'); // パスを '/' で分割
			if(q) {
				n = q-p;
				if(n == 0)
					n = 1;	// leading "/"
				q++;
			} else {
				n = strlen(p);
				q = 0;
			}
			if(n) {
				Bputc(b, ANAME); // 名前を示す命令
				Bputc(b, ANAME>>8);
				Bputc(b, D_FILE); // ファイル名であることを示す
				Bputc(b, 1);
				Bputc(b, '<');
				Bwrite(b, p, n); // ファイル名の部分を書き込み
				Bputc(b, 0);
			}
			p = q;
			if(p == 0 && op) {
				p = op;
				op = 0;
			}
		}

		pg.lineno = h->line; // 履歴エントリの行番号を設定
		pg.to.type = zprog.to.type;
		pg.to.offset = h->offset;
		if(h->offset)
			pg.to.type = D_CONST;

		// Prog構造体をオブジェクトファイルに書き込み
		Bputc(b, pg.as);
		Bputc(b, pg.as>>8);
		Bputc(b, pg.lineno);
		Bputc(b, pg.lineno>>8);
		Bputc(b, pg.lineno>>16);
		Bputc(b, pg.lineno>>24);
		zaddr(b, &pg.from, 0);
		zaddr(b, &pg.to, 0);
	}
}

outhist関数は、コンパイルの最終段階でオブジェクトファイルにライン履歴情報を書き込む役割を担います。Histリンクリストを走査し、各履歴エントリ(ファイル名と行番号)をAHISTORYという特殊なアセンブリ命令としてオブジェクトファイルにシリアライズします。ファイルパスは/で区切られたセグメントに分割され、それぞれANAME命令として書き込まれます。これにより、生成されたバイナリには、元のソースコードのファイルと行番号の対応関係が埋め込まれ、デバッガがこの情報を使ってソースレベルデバッグを行うことが可能になります。

関連リンク

参考にした情報源リンク