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

[インデックス 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 テーブル内のファイル番号の正確性を確保することにありました。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. Go リンカ (cmd/ld): Go言語のコンパイルプロセスにおいて、cmd/ld は最終的な実行可能ファイルを生成する役割を担うリンカです。コンパイラによって生成されたオブジェクトファイル(.o ファイル)を結合し、必要なライブラリをリンクし、実行可能形式に変換します。この過程で、デバッグ情報やランタイムに必要なメタデータ(pcln テーブルなど)も生成・埋め込みます。

  2. pcln テーブル (Program Counter Line Number Table): Goの実行可能ファイルに埋め込まれる内部データ構造の一つで、プログラムカウンタ (PC) の値と、対応するソースコードのファイル名および行番号をマッピングします。これにより、デバッガは実行中の命令がソースコードのどの部分に由来するかを特定できます。pcln テーブルは、関数情報、PC-SP (Stack Pointer) マッピング、PC-File マッピング、PC-Line マッピングなど、様々なPC関連のデータを含んでいます。

  3. DWARF (Debugging With Attributed Record Formats): Unix系システムで広く使われている標準的なデバッグ情報フォーマットです。コンパイラやリンカによって生成され、実行可能ファイルに埋め込まれます。デバッガは DWARF 情報を使用して、変数名、型情報、ソースコードの行番号、スタックフレームの構造などを解釈し、デバッグセッションを可能にします。pcln テーブルの情報は、最終的に DWARF 形式のデバッグ情報の一部として利用されることがあります。

  4. デルタエンコーディング (Delta Encoding): データの圧縮手法の一つで、連続する値の差分(デルタ)を記録することで、元の値を直接記録するよりも少ないビット数で表現しようとします。例えば、[10, 12, 15, 16] というシーケンスがある場合、デルタエンコーディングでは [10, +2, +3, +1] のように記録されます。これにより、特に値が連続して変化するが、その変化量が小さい場合に高い圧縮率が得られます。pcln テーブルのファイル番号や行番号のようなデータは、しばしばデルタエンコーディングを用いて効率的に格納されます。

  5. 可変長整数 (Varint): Google Protocol Buffersなどで使用されるエンコーディング方式で、小さな数値を少ないバイト数で、大きな数値をより多くのバイト数で表現します。これにより、平均的にデータサイズを削減できます。pcln テーブル内のデルタ値は、この可変長整数形式でエンコードされることが一般的です。

技術的詳細

このコミットの核心は、src/cmd/ld/pcln.c ファイル内の renumberfiles 関数における newval 変数の更新ロジックの修正です。

renumberfiles 関数は、pcln テーブル内のファイル番号を再割り当てし、それらをデルタエンコーディングされた形式で出力するための関数です。この関数は、Pcdata 構造体(PC-データテーブルを表現)と、ファイル名に対応するシンボル (LSym) の配列を受け取ります。

元のコードでは、renumberfiles 関数内でファイル番号のデルタを計算する際に、newval という変数が使用されていました。この newval は、前のファイル番号を保持し、現在のファイル番号 val との差分 dv = val - newval を計算するために使われます。しかし、バグのあるコードでは、この newvalval で適切に更新されていませんでした。コミットメッセージにあるように、「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 テーブルをパースする際に、正確なファイル番号を再構築できるようになります。

また、修正されたコードには、デバッグ用のコメントアウトされたサンティチェックブロックが含まれています。これは、修正後のファイル番号が期待される範囲内にあることを検証するためのもので、pciterinitpciternext を使用して 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のソースコード (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のランタイムとリンカの内部構造に関するドキュメントや記事が役立ちます。