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

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

このコミットは、Go言語のリンカ(cmd/ld)におけるDWARFデバッグ情報の生成に関するバグ修正です。具体的には、.debug_lineセクションのデータ生成において発生していたオフバイワンエラーを修正しています。

コミット

commit 15ec569ba952d47e6e3c96705c9ec7888fe90877
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date:   Thu Feb 20 09:06:32 2014 -0800

    cmd/ld: fix off-by-one error in DWARF .debug_line transcription
    
    The liblink refactor changed the DWARF .debug_line flow control. The mapping was off by one pcline entry. The fix here preserves pc until it can be compared to pcline.pc.
    
    Sample dwarfdump .debug_line output for main.main from the program in issue 7351, before liblink (correct):
    
    0x0000003c: 00 Extended: <9> 02 DW_LNE_set_address( 0x0000000000002000 )
    0x00000047: 03 DW_LNS_advance_line( 6 )
    0x00000049: 01 DW_LNS_copy
                0x0000000000002000      1      7      0 is_stmt
    
    0x0000004a: 8b address += 21,  line += 1
                0x0000000000002021      1      8      0 is_stmt
    
    0x0000004b: 02 DW_LNS_advance_pc( 153 )
    0x0000004e: 03 DW_LNS_advance_line( 1 )
    0x00000050: 01 DW_LNS_copy
                0x00000000000020ba      1      9      0 is_stmt
    
    After liblink (off by one entry):
    
    0x00001bbf: 00 Extended: <9> 02 DW_LNE_set_address( 0x0000000000002000 )
    0x00001bca: 02 DW_LNS_advance_pc( 33 )
    0x00001bcc: 03 DW_LNS_advance_line( 6 )
    0x00001bce: 01 DW_LNS_copy
                0x0000000000002021      1      7      0 is_stmt
    
    0x00001bcf: 02 DW_LNS_advance_pc( 153 )
    0x00001bd2: 03 DW_LNS_advance_line( 1 )
    0x00001bd4: 01 DW_LNS_copy
                0x00000000000020ba      1      8      0 is_stmt
    
    0x00001bd5: 02 DW_LNS_advance_pc( 153 )
    0x00001bd8: 03 DW_LNS_advance_line( 1 )\n    0x00001bda: 01 DW_LNS_copy
                0x0000000000002153      1      9      0 is_stmt
    
    After this CL (the line 9 pc offset changed due to intervening compiler changes):
    
    0x00001d07: 00 Extended: <9> 02 DW_LNE_set_address( 0x0000000000002000 )
    0x00001d12: 03 DW_LNS_advance_line( 6 )
    0x00001d14: 01 DW_LNS_copy
                0x0000000000002000      1      7      0 is_stmt
    
    0x00001d15: 8b address += 21,  line += 1
                0x0000000000002021      1      8      0 is_stmt
    
    0x00001d16: 02 DW_LNS_advance_pc( 189 )
    0x00001d19: 03 DW_LNS_advance_line( 1 )
    0x00001d1b: 01 DW_LNS_copy
                0x00000000000020de      1      9      0 is_stmt
    
    Fixes #7351.
    
    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/66290043

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

https://github.com/golang/go/commit/15ec569ba952d47e6e3c96705c9ec7888fe90877

元コミット内容

このコミットは、Goリンカ(cmd/ld)におけるDWARFデバッグ情報の.debug_lineセクションの転記(transcription)におけるオフバイワンエラーを修正するものです。以前のliblinkのリファクタリングによって、DWARFの.debug_lineのフロー制御が変更され、その結果、pclineエントリのマッピングが1つずれていました。この修正は、pc(プログラムカウンタ)の値をpcline.pcと比較できるまで保持することで、この問題を解決します。

コミットメッセージには、dwarfdumpツールによる.debug_lineの出力例が示されており、liblinkリファクタリング前(正しい出力)、liblinkリファクタリング後(オフバイワンエラーのある出力)、そしてこのコミット適用後(修正された出力)の比較がなされています。これにより、問題がどのように現れ、どのように修正されたかが視覚的に示されています。

変更の背景

この変更の背景には、Goリンカの内部構造に対する大規模なリファクタリングであるliblinkの導入があります。liblinkは、Goのツールチェインにおけるリンカの共通部分を抽象化し、コードの再利用性と保守性を向上させることを目的としていました。しかし、このリファクタリングの過程で、DWARFデバッグ情報の生成ロジック、特にプログラムカウンタ(PC)とソースコードの行番号のマッピングを扱う.debug_lineセクションの処理に意図しない変更が加えられました。

具体的には、liblinkの変更により、リンカが.debug_lineテーブルを構築する際の内部的なPCの進め方と、pcline(PC-lineテーブル)のエントリの処理順序にずれが生じました。これにより、デバッガがソースコードの行と実行中のマシンコードのアドレスを正確にマッピングできなくなり、デバッグ体験に悪影響を及ぼしていました。この問題は、Go issue #7351として報告され、このコミットはその問題を解決するために作成されました。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. DWARF (Debugging With Attributed Record Formats): DWARFは、ソースレベルデバッガがプログラムの実行状態を理解するために必要な情報を提供する標準的なデバッグファイル形式です。コンパイルされたバイナリに埋め込まれるか、別のファイルとして提供されます。DWARFは、変数名、型情報、関数定義、ソースコードの行番号と実行可能コードのアドレスのマッピングなど、多岐にわたるデバッグ情報を格納します。

  2. .debug_line セクション: DWARF情報の中で、.debug_lineセクションは特に重要です。これは、コンパイルされたバイナリの特定のマシンコードアドレスが、元のソースファイルのどの行番号に対応するかをマッピングする情報を含んでいます。デバッガがブレークポイントを設定したり、ステップ実行したりする際に、この情報を用いてユーザーにソースコードの正確な位置を表示します。 .debug_lineテーブルは、一連のステートメント(命令)で構成されるステートマシンとして機能します。これらのステートメントは、プログラムカウンタ(PC)の進捗とソースコードの行番号の変化を記述します。主なステートメントには以下のようなものがあります。

    • DW_LNE_set_address: 現在のPCアドレスを設定します。
    • DW_LNS_advance_line: 現在の行番号を進めます。
    • DW_LNS_advance_pc: 現在のPCアドレスを進めます。
    • DW_LNS_copy: 現在のPCと行番号のペアをテーブルに記録します。
    • 特殊なオペコード: address += delta, line += delta のように、PCと行番号を同時に進める効率的なオペコード。
  3. Goリンカ (cmd/ld): Go言語のビルドプロセスにおいて、リンカ(cmd/ld)は、コンパイルされたオブジェクトファイル(.oファイル)を結合し、実行可能なバイナリを生成する役割を担います。この過程で、デバッグ情報もオブジェクトファイルから収集され、最終的なバイナリのDWARFセクションにまとめられます。

  4. liblink: Go 1.2のリリースサイクルで導入されたliblinkは、Goのリンカの内部構造を再設計したものです。それ以前は、各アーキテクチャ(amd64, armなど)ごとにリンカのコードが重複していましたが、liblinkは共通のリンキングロジックを共有ライブラリとして提供することで、コードベースの簡素化と保守性の向上を図りました。このリファクタリングは、リンカの内部動作、特にPCと行番号のマッピング処理に影響を与えました。

  5. pciterinit, pciternext: これらはGoリンカ内部でPC-lineテーブル(pcln)をイテレートするためのユーティリティ関数です。pclnは、プログラムカウンタ(PC)とソースファイル、行番号のマッピングを効率的に格納するためのデータ構造です。pciterinitはイテレータを初期化し、pciternextは次のPC-lineエントリに進みます。

技術的詳細

このバグは、liblinkリファクタリング後のcmd/ld/dwarf.c内のwritelines関数で発生していました。この関数は、DWARFの.debug_lineセクションを生成する主要なロジックを含んでいます。

問題の核心は、pc(現在のプログラムカウンタ)とepc(次のエントリのプログラムカウンタ)の更新ロジックにありました。writelines関数は、pcfile(PC-ファイルマッピング)とpcline(PC-行マッピング)という2つのイテレータを使用して、PCの範囲とそれに対応するファイルおよび行番号の情報を処理します。

修正前のコードでは、ループの冒頭でpcpcfile.nextpcまたはpcline.nextpcと比較され、適切なイテレータが進められていました。しかし、putpclcdelta関数が呼び出される前にpcepcに更新されてしまうため、putpclcdeltaに渡されるpcの値が、本来比較されるべきpcline.pcとの差を計算する際に、すでに次のPCに更新されてしまっていました。これにより、pclineエントリのマッピングが1つずれる「オフバイワン」エラーが発生していました。

具体的には、putpclcdelta(epc - pc, pcline.value - line)という行で、epc - pcがPCの差分を計算していましたが、このpcが既に更新された値であるため、正しい差分が計算されませんでした。

修正は、このpcの更新タイミングを変更することにあります。

  1. ループの開始時に、epc = pc;として、現在のpcの値をepcに退避させます。これにより、pcfile.nextpcpcline.nextpcとの比較には、まだ更新されていないpcの値が使われるようになります。
  2. putpclcdeltaの呼び出しをputpclcdelta(s->value + pcline.pc - pc, pcline.value - line);に変更します。ここで、s->value + pcline.pcは、現在のpclineエントリが指す絶対PCアドレスを表します。この値から、まだ更新されていないpcを引くことで、正確なPCの差分を計算できるようになります。
  3. pc = epc;という行をputpclcdeltaの呼び出しの後に移動させます。これにより、pcの更新が、必要な計算がすべて行われた後に行われるようになります。

この変更により、dwarfdumpの出力例で示されているように、DW_LNS_advance_pcのオペランドが正しくなり、PCと行番号の正確なマッピングが復元されました。

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

変更はsrc/cmd/ld/dwarf.cファイルに集中しています。

--- a/src/cmd/ld/dwarf.c
+++ b/src/cmd/ld/dwarf.c
@@ -1590,29 +1590,30 @@ writelines(void)
 
 	pciterinit(&pcfile, &s->pcln->pcfile);
 	pciterinit(&pcline, &s->pcln->pcline);
+	epc = pc; // 追加: 現在のpcをepcに退避
 	while(!pcfile.done && !pcline.done) {
-		if(pc - s->value >= pcfile.nextpc) {
+		if(epc - s->value >= pcfile.nextpc) { // 変更: pcの代わりにepcを使用
 			pciternext(&pcfile);
 			continue;
 		}
-		if(pc - s->value >= pcline.nextpc) {
+		if(epc - s->value >= pcline.nextpc) { // 変更: pcの代わりにepcを使用
 			pciternext(&pcline);
 			continue;
 		}
 
-		if(pcfile.nextpc < pcline.nextpc)
-			epc = pcfile.nextpc;
-		else
-			epc = pcline.nextpc;
-		epc += s->value;
-
 		if(file != pcfile.value) {
 			cput(DW_LNS_set_file);
 			uleb128put(pcfile.value);
 			file = pcfile.value;
 		}
-		putpclcdelta(epc - pc, pcline.value - line);
+		// 変更: putpclcdeltaの引数を修正し、pcの更新を後回しにする
+		putpclcdelta(s->value + pcline.pc - pc, pcline.value - line);
+
 		pc = epc;
+		// 移動: epcの計算をpc更新後に行う
+		if(pcfile.nextpc < pcline.nextpc)
+			epc = pcfile.nextpc;
+		else
+			epc = pcline.nextpc;
+		epc += s->value;
 		line = pcline.value;
 	}

コアとなるコードの解説

この修正の核心は、writelines関数内のpcepcという2つのプログラムカウンタ変数の役割と更新タイミングの調整にあります。

  • pc: これは、現在処理中のDWARF .debug_lineエントリが参照するプログラムカウンタの「開始」アドレスを表します。つまり、前の.debug_lineステートメントが適用された後のPCの値です。
  • epc: これは、次に処理すべきPC-lineエントリの「終了」アドレス、または次のファイル/行情報が適用されるべきプログラムカウンタの「次の」アドレスを表します。

修正前は、ループの冒頭でepcが計算され、そのepcpcの差分がputpclcdeltaに渡されていました。しかし、このpcは、putpclcdeltaが呼び出される前に、既に次のPC値に更新されてしまっている可能性がありました。

修正後のロジックは以下のようになります。

  1. epc = pc; の追加: ループの各イテレーションの開始時に、現在のpcの値をepcにコピーします。これにより、pcfile.nextpcpcline.nextpcとの比較に使用されるepcは、そのイテレーションの開始時点での正確なPC値(つまり、まだ進んでいないPC値)を反映するようになります。
  2. if(epc - s->value >= pcfile.nextpc)if(epc - s->value >= pcline.nextpc) への変更: ここで、pcの代わりにepcを使用することで、イテレータを進めるべきかどうかの判断が、現在のPCの正確な位置に基づいて行われるようになります。
  3. putpclcdelta(s->value + pcline.pc - pc, pcline.value - line); への変更:
    • s->value + pcline.pc: これは、現在のpclineエントリが指す絶対的なプログラムカウンタアドレスです。s->valueはセクションのベースアドレスのようなオフセットで、pcline.pcpclnテーブル内の相対PCです。
    • pc: これは、putpclcdeltaが呼び出される時点での、まだ更新されていない(つまり、前の.debug_lineステートメントが適用された後の)pcの値です。
    • この差分を計算することで、現在の.debug_lineステートメントがカバーすべきPCの正確な範囲が決定されます。
  4. pc = epc; の移動と epc の再計算: putpclcdeltaの呼び出し後、pcepcの値に更新します。そして、次のイテレーションのために、pcfile.nextpcpcline.nextpcのどちらが小さいかに基づいて、新しいepcの値を計算します。これにより、pcepcの間の関係が、DWARFの.debug_lineステートマシンの期待する動作と一致するようになります。

この修正により、リンカはPCと行番号の正確なマッピングを生成できるようになり、デバッガがGoプログラムの実行をソースコードレベルで正しく追跡できるようになりました。

関連リンク

参考にした情報源リンク

  • DWARF Debugging Information Format Standard: https://dwarfstd.org/ (特にSection 6: Line Number Information)
  • Go言語のリンカのソースコード (src/cmd/ld/)
  • Go言語のツールチェインに関するドキュメントやブログ記事 (特にliblinkに関するもの)
  • dwarfdump ツールに関するドキュメント