[インデックス 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
ディレクティブによる行番号の変更を正確に追跡できるようになります。
技術的詳細
このコミットの主要な技術的変更点は、コンパイラがソースコードの行番号情報をどのように管理し、出力するかという点にあります。
-
行番号管理の改善:
- 以前は
curio.lineno
というIo
構造体内のフィールドで現在の行番号を管理していましたが、このコミットではグローバルなlineno
変数を導入し、より直接的に行番号を更新するように変更されています。 curio.infile
も同様に、infile
というグローバル変数や、linehist
関数に渡される引数として管理されるようになっています。getc
、ungetc
、getnsc
といった文字読み込み関数内でcurio.lineno
の代わりにlineno
がインクリメント/デクリメントされるよう修正されています。
- 以前は
-
ライン履歴(Line History)の導入:
Hist
という新しい構造体が定義されました。これには、ファイル名 (name
)、行番号 (line
)、オフセット (offset
)、そして次の履歴エントリへのリンク (link
) が含まれます。linehist
関数が追加され、ソースファイルの切り替え時(例:importfile
、unimportfile
、cannedimports
)や、コンパイルの開始・終了時に呼び出され、Hist
構造体のリンクリストを構築します。このリンクリストは、コンパイラが処理したソースファイルの履歴と、その時点での行番号を記録します。hist
とehist
というグローバル変数が導入され、Hist
リンクリストの先頭と末尾を指します。
-
新しいフォーマット指定子
%L
の追加:src/cmd/gc/go.h
とsrc/cmd/gc/subr.c
にLconv
関数が追加され、Fmt
ライブラリに新しいフォーマット指定子%L
が登録されました。%L
は、エラーメッセージや警告メッセージの出力時に、現在のファイルと行番号をより詳細な形式で表示するために使用されます。特に、インクルードされたファイルや#line
ディレクティブによって変更された行番号の履歴を辿って表示する機能を持っています。Lconv
関数は、Hist
リンクリストを逆順に辿り、現在の行番号がどのファイルのどの行に由来するかを計算し、filename:lineno
の形式で出力します。これにより、デバッグ時の情報が格段に向上します。
-
コンパイラ出力へのラインテーブル情報の追加:
src/cmd/6g/obj.c
のdumpobj
関数にouthist(bout)
の呼び出しが追加されました。これは、コンパイルされたオブジェクトファイルにライン履歴情報を埋め込むためのものです。outhist
関数は、Hist
リンクリストを走査し、各履歴エントリをAHISTORY
アセンブリ命令としてオブジェクトファイルに書き込みます。これにより、デバッガや他のツールがバイナリからソースコードの行番号情報を抽出できるようになります。
-
クリーンアップスクリプトの更新:
src/clean.bash
とsrc/cmd/clean.bash
が更新され、新しく追加されたライブラリやツール(libmach_amd64
、db
)のクリーンアップ処理が追加されました。
これらの変更により、Goコンパイラはより正確で詳細なデバッグ情報を生成できるようになり、Goプログラムのデバッグ体験が向上しました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
src/cmd/6g/gg.h
:Hist
構造体の前方宣言と、hist
、zprog
などのグローバル変数の宣言が追加されています。また、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
構造体の定義が追加されました。lineno
、pathname
、hist
、ehist
といったグローバル変数が宣言されました。linehist
関数とLconv
関数のプロトタイプ宣言が追加されました。
src/cmd/gc/lex.c
:mainlex
関数内でpathname
の初期化とlinehist
の呼び出しが追加され、コンパイル開始時のファイル履歴が記録されるようになりました。fmtinstall('L', Lconv)
により、新しいフォーマット指定子%L
が登録されました。getc
、ungetc
、getnsc
関数内で、行番号の更新がcurio.lineno
からグローバルなlineno
変数に変更されました。importfile
、unimportfile
、cannedimports
関数内でlinehist
が適切に呼び出され、インクルードファイルの切り替え時にも行番号履歴が更新されるようになりました。
src/cmd/gc/subr.c
:yyerror
、warn
、fatal
関数内で、エラーメッセージの出力形式が%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
とファイル名file
をHist
構造体に保存し、hist
とehist
(履歴リストの先頭と末尾)を使ってリンクリストを構築します。これにより、コンパイラはどのファイルがいつ、どの行から処理されたかの履歴を保持できます。
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
命令として書き込まれます。これにより、生成されたバイナリには、元のソースコードのファイルと行番号の対応関係が埋め込まれ、デバッガがこの情報を使ってソースレベルデバッグを行うことが可能になります。
関連リンク
- Go言語の歴史とPlan 9の影響: https://go.dev/doc/faq#What_is_the_relationship_between_Go_and_Plan_9
- Goコンパイラの内部構造 (Go 1.5以降の
go tool compile
について): https://go.dev/blog/go1.5-compiler - コンパイラのデバッグ情報とラインテーブル: https://sourceware.org/gdb/onlinedocs/gdb/Line-Tables.html
参考にした情報源リンク
- Go compiler 6g gc history: https://go.dev/blog/go1.5-compiler
- Plan 9 line numbers compiler go: https://go.dev/doc/faq#What_is_the_relationship_between_Go_and_Plan_9
- Compiler line table debugging: https://sourceware.org/gdb/onlinedocs/gdb/Line-Tables.html
- Go Biobuf Fmt Hist data structure: (直接的な情報は見つからなかったが、
bufio
とfmt
パッケージの一般的な説明を参考に、このコミットにおけるBiobuf
とFmt
の役割を推測した) - コミットハッシュ: efec14bc5af7f1f43b6e736a4fa2138ea5a328a2 のGitHubページ: https://github.com/golang/go/commit/efec14bc5af7f1f43b6e736a4fa2138ea5a328a2