[インデックス 18696] ファイルの概要
このコミットは、Go言語のリンカ (cmd/ld
) における pcln
(Program Counter Line Number) テーブルのファイル番号の生成に関するバグ修正です。具体的には、デバッグ情報の一部として生成されるファイル番号のエンコーディングが誤っていた問題を解決します。
コミット
commit 91e3681105912bf39404be544c6d5a41ce2b789a
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date: Fri Feb 28 11:08:32 2014 -0800
cmd/ld: fix misgenerated pcln file numbers
The pcln file number was being encoded incorrectly. The recorded delta was always against -1, not against the previous value.
Update #7369
This CL fixes the bad DWARF file numbers. It does not, however, fix the gdb continue-to-end bug.
LGTM=iant
R=rsc, minux.ma, iant
CC=golang-codereviews, graham
https://golang.org/cl/68960046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/91e3681105912bf39404be544c6d5a41ce2b789a
元コミット内容
cmd/ld: fix misgenerated pcln file numbers
pcln
ファイル番号が誤ってエンコードされていました。記録されたデルタは常に -1 に対するものであり、前の値に対するものではありませんでした。
これは不正な DWARF ファイル番号を修正します。しかし、gdb の continue-to-end バグは修正しません。
変更の背景
Go言語の実行可能ファイルには、デバッグやプロファイリングのために重要なメタデータが含まれています。その一つが pcln
(Program Counter Line Number) テーブルです。このテーブルは、プログラムカウンタ (PC) の値と、それに対応するソースファイルの行番号情報をマッピングするために使用されます。デバッガ(例: GDB)が実行中のプログラムの現在の位置をソースコードのどの行に対応するかを特定するために不可欠な情報です。
このコミット以前は、pcln
テーブル内でファイル番号をエンコードする際にバグがありました。具体的には、ファイル番号の「デルタエンコーディング」が正しく機能していませんでした。デルタエンコーディングとは、連続する値の差分(デルタ)を記録することで、元の値を直接記録するよりも効率的にデータを保存する手法です。この場合、各ファイル番号は前のファイル番号からの差分として記録されるべきでしたが、実際には常に固定値(-1)からの差分として計算されていました。
この誤ったエンコーディングは、生成される DWARF (Debugging With Attributed Record Formats) デバッグ情報に不正なファイル番号が含まれる原因となっていました。結果として、デバッガがソースコードのファイルパスを正しく解決できず、デバッグ体験に悪影響を及ぼしていました。特に、ソースコードのステップ実行やブレークポイントの設定において、デバッガが混乱する可能性がありました。
コミットメッセージには「gdb continue-to-end bug」には影響しないと明記されており、この修正が特定のデバッグシナリオ(例えば、GDBでプログラムの最後まで実行を継続する際の挙動)とは直接関係ないことを示唆しています。このコミットの目的は、あくまで pcln
テーブル内のファイル番号の正確性を確保することにありました。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
Go リンカ (
cmd/ld
): Go言語のコンパイルプロセスにおいて、cmd/ld
は最終的な実行可能ファイルを生成する役割を担うリンカです。コンパイラによって生成されたオブジェクトファイル(.o
ファイル)を結合し、必要なライブラリをリンクし、実行可能形式に変換します。この過程で、デバッグ情報やランタイムに必要なメタデータ(pcln
テーブルなど)も生成・埋め込みます。 -
pcln
テーブル (Program Counter Line Number Table): Goの実行可能ファイルに埋め込まれる内部データ構造の一つで、プログラムカウンタ (PC) の値と、対応するソースコードのファイル名および行番号をマッピングします。これにより、デバッガは実行中の命令がソースコードのどの部分に由来するかを特定できます。pcln
テーブルは、関数情報、PC-SP (Stack Pointer) マッピング、PC-File マッピング、PC-Line マッピングなど、様々なPC関連のデータを含んでいます。 -
DWARF (Debugging With Attributed Record Formats): Unix系システムで広く使われている標準的なデバッグ情報フォーマットです。コンパイラやリンカによって生成され、実行可能ファイルに埋め込まれます。デバッガは DWARF 情報を使用して、変数名、型情報、ソースコードの行番号、スタックフレームの構造などを解釈し、デバッグセッションを可能にします。
pcln
テーブルの情報は、最終的に DWARF 形式のデバッグ情報の一部として利用されることがあります。 -
デルタエンコーディング (Delta Encoding): データの圧縮手法の一つで、連続する値の差分(デルタ)を記録することで、元の値を直接記録するよりも少ないビット数で表現しようとします。例えば、
[10, 12, 15, 16]
というシーケンスがある場合、デルタエンコーディングでは[10, +2, +3, +1]
のように記録されます。これにより、特に値が連続して変化するが、その変化量が小さい場合に高い圧縮率が得られます。pcln
テーブルのファイル番号や行番号のようなデータは、しばしばデルタエンコーディングを用いて効率的に格納されます。 -
可変長整数 (Varint): Google Protocol Buffersなどで使用されるエンコーディング方式で、小さな数値を少ないバイト数で、大きな数値をより多くのバイト数で表現します。これにより、平均的にデータサイズを削減できます。
pcln
テーブル内のデルタ値は、この可変長整数形式でエンコードされることが一般的です。
技術的詳細
このコミットの核心は、src/cmd/ld/pcln.c
ファイル内の renumberfiles
関数における newval
変数の更新ロジックの修正です。
renumberfiles
関数は、pcln
テーブル内のファイル番号を再割り当てし、それらをデルタエンコーディングされた形式で出力するための関数です。この関数は、Pcdata
構造体(PC-データテーブルを表現)と、ファイル名に対応するシンボル (LSym
) の配列を受け取ります。
元のコードでは、renumberfiles
関数内でファイル番号のデルタを計算する際に、newval
という変数が使用されていました。この newval
は、前のファイル番号を保持し、現在のファイル番号 val
との差分 dv = val - newval
を計算するために使われます。しかし、バグのあるコードでは、この newval
が val
で適切に更新されていませんでした。コミットメッセージにあるように、「The recorded delta was always against -1, not against the previous value.」という状況が発生していました。これは、newval
が初期値(おそらく0または-1に相当する値)から更新されず、常に同じ基準値に対してデルタが計算されていたことを意味します。
修正は非常にシンプルで、dv = val - newval;
の直後に newval = val;
という行を追加することです。この一行の追加により、newval
は現在のファイル番号 val
で適切に更新され、次のデルタ計算時には正しい「前の値」として機能するようになります。これにより、ファイル番号のデルタエンコーディングが正しく行われ、pcln
テーブルに格納されるファイル番号が正確になります。
この修正は、addvarint(&out, v);
を介して出力される可変長整数エンコードされたデルタ値の正確性に直接影響します。正しいデルタ値がエンコードされることで、デバッガが pcln
テーブルをパースする際に、正確なファイル番号を再構築できるようになります。
また、修正されたコードには、デバッグ用のコメントアウトされたサンティチェックブロックが含まれています。これは、修正後のファイル番号が期待される範囲内にあることを検証するためのもので、pciterinit
と pciternext
を使用して pcfile
データをイテレートし、ファイル番号が [1, ctxt->nhistfile]
の範囲内にあるかを確認しています。このチェックは、開発中に問題が再発しないことを保証するための防御的なプログラミングの一例です。
コアとなるコードの変更箇所
変更は src/cmd/ld/pcln.c
ファイルの以下の部分です。
--- a/src/cmd/ld/pcln.c
+++ b/src/cmd/ld/pcln.c
@@ -89,6 +89,7 @@ renumberfiles(LSym **files, int nfiles, Pcdata *d)
val = files[oldval]->value;
}
dv = val - newval;
+ newval = val;
v = (uint32)(dv<<1) ^ (uint32)(int32)(dv>>31);
addvarint(&out, v);
@@ -114,6 +115,7 @@ pclntab(void)
int32 off, end;
int64 funcdata_bytes;
Pcln *pcln;
+ Pciter it;
static Pcln zpcln;
funcdata_bytes = 0;
@@ -173,8 +175,18 @@ pclntab(void)
// and then remove this.
off = setuint32(ctxt, ftab, off, ctxt->cursym->locals + PtrSize);
- if(pcln != &zpcln)
+ if(pcln != &zpcln) {
renumberfiles(pcln->file, pcln->nfile, &pcln->pcfile);
+ if(0) {
+ // Sanity check the new numbering
+ for(pciterinit(&it, &pcln->pcfile); !it.done; pciternext(&it)) {
+ if(it.value < 1 || it.value > ctxt->nhistfile) {
+ diag("bad file number in pcfile: %d not in range [1, %d]\n", it.value, 1, ctxt->nhistfile);
+ errorexit();
+ }
+ }
+ }
+ }
// pcdata
off = addpctab(ftab, off, &pcln->pcsp);
コアとなるコードの解説
renumberfiles
関数内の変更
dv = val - newval;
+ newval = val;
v = (uint32)(dv<<1) ^ (uint32)(int32)(dv>>31);
addvarint(&out, v);
dv = val - newval;
: ここで現在のファイル番号val
と、前のファイル番号newval
の差分dv
(デルタ値) を計算しています。newval = val;
: この行が追加された修正箇所です。 これにより、newval
が現在のval
の値で更新されます。次のイテレーションでは、この更新されたnewval
が「前の値」として使用され、正しいデルタが計算されるようになります。v = (uint32)(dv<<1) ^ (uint32)(int32)(dv>>31);
: 計算されたデルタdv
を可変長整数エンコーディングに適した形式に変換しています。これは、符号付き整数を符号なし整数にマッピングする一般的な手法です。dv<<1
は値を2倍し、dv>>31
は符号ビットを抽出してXORすることで、負の数も効率的にエンコードできるようにします。addvarint(&out, v);
: 変換されたv
を可変長整数として出力ストリームout
に追加します。
pclntab
関数内の変更
Pcln *pcln;
+ Pciter it;
static Pcln zpcln;
Pciter it;
:Pciter
型のイテレータ変数it
が追加されています。これは、pcln
テーブル内のPC-データ(特にPC-ファイルデータ)を走査するために使用されます。
if(pcln != &zpcln) {
renumberfiles(pcln->file, pcln->nfile, &pcln->pcfile);
+ if(0) {
+ // Sanity check the new numbering
+ for(pciterinit(&it, &pcln->pcfile); !it.done; pciternext(&it)) {
+ if(it.value < 1 || it.value > ctxt->nhistfile) {
+ diag("bad file number in pcfile: %d not in range [1, %d]\n", it.value, 1, ctxt->nhistfile);
+ errorexit();
+ }
+ }
+ }
}
if(0) { ... }
: このブロックは、コンパイル時に常に偽となる条件if(0)
で囲まれているため、実際には実行されません。これは、デバッグ目的で一時的に追加されたサンティチェックコードであり、問題が修正された後にコメントアウト(または削除)されるべきものです。pciterinit(&it, &pcln->pcfile);
:pcln->pcfile
(PC-ファイルデータ) をイテレートするためのPciter
を初期化します。for(...; !it.done; pciternext(&it))
:it.done
が真になるまでループを回し、pciternext(&it)
で次のPC-ファイルエントリに進みます。if(it.value < 1 || it.value > ctxt->nhistfile)
: 各エントリのファイル番号it.value
が有効な範囲(1からctxt->nhistfile
まで)にあるかをチェックします。ctxt->nhistfile
は、リンカが認識している履歴ファイルの総数を表します。diag(...)
とerrorexit()
: もしファイル番号が範囲外であれば、エラーメッセージを出力してプログラムを終了します。これは、デバッグ情報が正しく生成されていないことを示す重大なエラーです。
このサンティチェックは、renumberfiles
関数が正しく機能し、有効なファイル番号を生成していることを検証するために一時的に導入されたものと考えられます。
関連リンク
- Go Issue 7369: https://github.com/golang/go/issues/7369
- Go CL 68960046: https://golang.org/cl/68960046
参考にした情報源リンク
- Goのソースコード (
src/cmd/ld/pcln.c
) - GoのIssueトラッカー
- DWARF Debugging Standard: https://dwarfstd.org/
- Protocol Buffers - Varints: https://protobuf.dev/programming-guides/encoding/#varints
- Goのリンカに関する一般的な情報 (例: Goのドキュメント、ブログ記事など)
- Goのデバッグ情報に関するより深い理解のためには、Goのランタイムとリンカの内部構造に関するドキュメントや記事が役立ちます。