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

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

このコミットは、Go言語のリンカ (cmd/ld) におけるDWARFフレームテーブルの生成に関するオフバイワンエラーを修正するものです。具体的には、.debug_frameセクションで生成される「PCの進行 (advance PC)」と「SPオフセットの設定 (set SP offset)」の擬似命令の順序が誤っていたために発生していた、スタックフレーム情報の不正確さを解消します。

コミット

commit 7b0ee5342919908e1a6b91d7c92a530ef45f5824
Author: Rob Pike <r@golang.org>
Date:   Mon Jul 7 16:07:24 2014 -0700

    cmd/ld: fix off-by-one in DWARF frame tables
    The code generating the .debug_frame section emits pairs of "advance PC",
    "set SP offset" pseudo-instructions. Before the fix, the PC advance comes
    out before the SP setting, which means the emitted offset for a block is
    actually the value at the end of the block, which is incorrect for the
    block itself.
    
    The easiest way to fix this problem is to emit the SP offset before the
    PC advance.
    
    One delicate point: the last instruction to come out is now an
    "advance PC", which means that if there are padding intsructions after
    the final RET, they will appear to have a non-zero offset. This is odd
    but harmless because there is no legal way to have a PC in that range,
    or to put it another way, if you get here the SP is certainly screwed up
    so getting the wrong (virtual) frame pointer is the least of your worries.
    
    LGTM=iant
    R=rsc, iant, lvd
    CC=golang-codereviews
    https://golang.org/cl/112750043

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

https://github.com/golang/go/commit/7b0ee5342919908e1a6b91d7c92a530ef45f5824

元コミット内容

Go言語のリンカ (cmd/ld) におけるDWARFフレームテーブルのオフバイワンエラーを修正します。.debug_frameセクションを生成するコードが、「PCの進行」と「SPオフセットの設定」という擬似命令のペアを出力する際に、修正前はPCの進行がSPの設定よりも先に行われていました。これにより、あるコードブロックに対して出力されるSPオフセットが、実際にはそのブロックの終端での値となってしまい、ブロック自体にとっては不正確な値となっていました。

この問題を修正する最も簡単な方法は、SPオフセットの設定をPCの進行よりも先に出力することです。

ただし、この修正には一つ注意点があります。最終的に出力される命令が「PCの進行」となるため、最後のRET命令の後にパディング命令が存在する場合、それらが非ゼロのオフセットを持つように見えてしまいます。これは奇妙ではありますが、その範囲にPCが到達する正当な方法がないため、無害です。言い換えれば、もしPCがその範囲に到達した場合、SPは確実に壊れているため、誤った(仮想的な)フレームポインタを取得することは、最も小さな問題に過ぎません。

変更の背景

このコミットの背景には、デバッグ情報フォーマットであるDWARFのフレーム情報(Call Frame Information, CFI)の正確な生成が関係しています。デバッガがプログラムの実行中にスタックトレースを正確に表示したり、変数の値を検査したりするためには、各関数のスタックフレームの構造を正確に記述した情報が必要です。この情報は通常、実行ファイルの.debug_frameセクションに格納されます。

Go言語のリンカは、コンパイルされたオブジェクトファイルを結合し、実行ファイルを生成する際に、このDWARFデバッグ情報も生成します。問題は、リンカが.debug_frameセクション内で、プログラムカウンタ(PC)の変更とスタックポインタ(SP)のオフセット変更を記述するDWARF命令の順序を誤っていたことにありました。

具体的には、あるPC範囲(コードブロック)に対応するスタックポインタのオフセットを記述する際に、PCがその範囲の終わりに進んだ後にSPオフセットが設定されるような命令順になっていました。これにより、デバッガがそのPC範囲の途中でスタックフレーム情報を参照しようとすると、実際にはそのブロックの「次の」状態のSPオフセットが提供されてしまい、オフバイワンのエラーが発生し、デバッグ情報が不正確になるという問題がありました。この不正確さは、特にデバッガがスタックを巻き戻す(unwind)際に問題を引き起こす可能性がありました。

このコミットは、この順序の誤りを修正し、デバッグ情報が常に現在のPCに対応する正確なSPオフセットを反映するようにすることで、デバッグ体験の信頼性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念について知っておく必要があります。

  • DWARF (Debugging With Attributed Record Formats): コンパイラやデバッガが使用する標準的なデバッグ情報フォーマットです。実行可能ファイル内に、プログラムの構造(変数、型、関数、スタックフレームなど)を記述するためのメタデータを提供します。これにより、デバッガはソースコードレベルでのデバッグを可能にします。

  • Call Frame Information (CFI): DWARFの一部であり、関数の呼び出し規約やスタックフレームのレイアウトに関する情報を提供します。デバッガはCFIを使用して、スタックを巻き戻し(unwind)、呼び出し履歴を再構築し、各スタックフレーム内のレジスタや変数の値を特定します。CFIは通常、実行ファイルの.debug_frameセクションに格納されます。

  • .debug_frame セクション: 実行可能ファイル内のセクションの一つで、CFIが格納されています。このセクションは、Frame Description Entry (FDE) と Common Information Entry (CIE) のシーケンスで構成されます。

    • CIE (Common Information Entry): 複数のFDEに共通するプロパティ(例:命令セット、データエンコーディング)を記述します。
    • FDE (Frame Description Entry): 特定の関数の特定のPC範囲におけるスタックフレームのアンワインド方法を記述します。
  • PC (Program Counter): 次に実行される命令のアドレスを保持するCPUレジスタです。

  • SP (Stack Pointer): 現在のスタックフレームの最上位(または最下位、アーキテクチャによる)のアドレスを指すCPUレジスタです。スタックフレームは、関数呼び出し時にローカル変数、引数、リターンアドレスなどを格納するために使用されるメモリ領域です。

  • CFA (Canonical Frame Address): DWARFにおける概念で、スタックフレーム内の固定されたアドレスを指します。通常、SPと固定オフセットの組み合わせで定義されます。デバッガはCFAを基準に、他のレジスタや変数の位置を計算します。

  • DWARF CFA命令: CFI内で使用される命令で、スタックフレームの状態変化を記述します。

    • DW_CFA_advance_loc: プログラムカウンタ (PC) を指定されたバイト数だけ進めます。この命令は、コードの実行が進むにつれて、CFIが適用されるPC範囲を更新するために使用されます。
    • DW_CFA_def_cfa_offset_sf: CFAをスタックポインタ (SP) と指定されたオフセットの組み合わせとして定義します。_sfサフィックスは「scaled offset」を意味し、オフセットがデータアライメントファクタ(通常は命令のバイトサイズ)でスケーリングされることを示します。
  • cmd/ld: Go言語のリンカです。Goのソースコードがコンパイルされて生成されたオブジェクトファイル(.oファイル)を結合し、実行可能なバイナリファイルを作成します。この過程で、デバッグ情報(DWARFなど)もバイナリに埋め込みます。

  • cput / sleb128put / LPUT: これらはGoリンカの内部関数で、DWARF命令やそのオペランドをバイナリ形式で出力するために使用されます。

    • cput: 1バイトのDWARF命令コードを出力します。
    • sleb128put: 符号付きLEB128形式で数値をエンコードして出力します。LEB128は、可変長で数値を表現するためのエンコーディング方式で、DWARFでよく使用されます。
    • LPUT: 4バイトの数値をリトルエンディアン形式で出力します。

技術的詳細

このコミットが修正する問題は、GoリンカがDWARFのフレーム情報を生成する際の、putpccfadelta関数内の命令出力順序の誤りにありました。

putpccfadelta関数は、PCのデルタ(deltapc)とCFAのオフセット(cfa)に基づいて、DWARFフレーム情報を出力します。本来、デバッガが特定のPCにおけるスタックフレームの状態を正確に把握するためには、そのPCが指す命令が実行された時点でのSPオフセットが記述されている必要があります。

修正前のコードでは、putpccfadelta関数内で以下の順序でDWARF命令が出力されていました。

  1. DW_CFA_advance_loc (PCの進行)
  2. DW_CFA_def_cfa_offset_sf (SPオフセットの設定)

この順序だと、PCが新しい位置に進んだ「後」に、その新しいPC位置でのSPオフセットが設定されることになります。しかし、デバッガが参照したいのは、PCが「現在の」ブロック内にある時点でのSPオフセットです。結果として、デバッガは常に「次の」PC範囲に適用されるべきSPオフセットを参照してしまい、オフバイワンのエラーが発生していました。これは、デバッガがスタックトレースを生成する際に、誤ったスタックポインタの値を計算してしまう原因となります。

このコミットでは、この命令の出力順序を単純に反転させることで問題を解決しています。

修正後の順序は以下の通りです。

  1. DW_CFA_def_cfa_offset_sf (SPオフセットの設定)
  2. DW_CFA_advance_loc (PCの進行)

これにより、まず現在のPC範囲に適用されるべきSPオフセットが設定され、その後にPCが進行するという論理的な順序になります。デバッガは、PCが特定のコードブロック内にある間、そのブロックに正しいSPオフセットが関連付けられていることを保証できます。

コミットメッセージで言及されている「One delicate point」は、この順序変更によって、関数の一番最後の命令(通常はRET)の後にパディング命令が存在する場合に発生する可能性のある副作用についてです。修正後、putpccfadelta関数が最後に発行する命令はDW_CFA_advance_locになります。もしRETの後に実行されないパディング命令が続く場合、そのパディング命令のPC範囲に対して、非ゼロのSPオフセットが関連付けられているように見える可能性があります。しかし、コミットメッセージが指摘するように、そのPC範囲は実行可能なコードを含まないため、デバッガがそこに到達することは通常ありません。もしデバッガがそこに到達したとすれば、それは既にスタックが破損している状態であり、このDWARF情報の不正確さは最も小さな問題であるため、実質的に無害であると判断されています。

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

変更は src/cmd/ld/dwarf.c ファイルの putpccfadelta 関数内で行われています。

--- a/src/cmd/ld/dwarf.c
+++ b/src/cmd/ld/dwarf.c
@@ -1696,6 +1696,9 @@ enum
 static void
 putpccfadelta(vlong deltapc, vlong cfa)
 {\n+\tcput(DW_CFA_def_cfa_offset_sf);\n+\tsleb128put(cfa / DATAALIGNMENTFACTOR);\n+\n \tif (deltapc < 0x40) {\n \t\tcput(DW_CFA_advance_loc + deltapc);\n \t} else if (deltapc < 0x100) {\n@@ -1708,9 +1711,6 @@ putpccfadelta(vlong deltapc, vlong cfa)\n \t\tcput(DW_CFA_advance_loc4);\n \t\tLPUT(deltapc);\n \t}\n-\n-\tcput(DW_CFA_def_cfa_offset_sf);\n-\tsleb128put(cfa / DATAALIGNMENTFACTOR);\n }\n \n static void

具体的には、以下の3行がファイルの先頭付近に移動しました。

	cput(DW_CFA_def_cfa_offset_sf);
	sleb128put(cfa / DATAALIGNMENTFACTOR);

そして、元の位置にあった同じ3行が削除されました。

コアとなるコードの解説

putpccfadelta 関数は、GoリンカがDWARFデバッグ情報の.debug_frameセクションに、PCの進行とCFA(Canonical Frame Address)のオフセット変更を記述するための命令を出力する役割を担っています。

  • deltapc: プログラムカウンタ (PC) がどれだけ進んだかを示すデルタ値です。
  • cfa: 新しいCFAオフセット値です。

修正前のコードでは、この関数はまずPCの進行を示すDW_CFA_advance_loc命令を出力し、その後にCFAオフセットを設定するDW_CFA_def_cfa_offset_sf命令を出力していました。

// 修正前 (元の位置)
	// PCの進行命令を出力
	if (deltapc < 0x40) {
		cput(DW_CFA_advance_loc + deltapc);
	} else if (deltapc < 0x100) {
		// ... (他の advance_loc 命令のバリエーション)
	}
	// CFAオフセット設定命令を出力
	cput(DW_CFA_def_cfa_offset_sf);
	sleb128put(cfa / DATAALIGNMENTFACTOR);

この順序では、デバッガが特定のPC位置でスタックフレーム情報を参照しようとした際に、そのPC位置に対応するSPオフセットではなく、PCが「進んだ後」のSPオフセットが提供されてしまうという問題がありました。これは、デバッグ情報が現在のコードブロックの状態を正確に反映していないことを意味します。

このコミットによる修正は、DW_CFA_def_cfa_offset_sf命令とそのオペランドの出力コードを、DW_CFA_advance_loc命令の出力よりも前に移動させるという非常にシンプルなものです。

// 修正後 (新しい位置)
	// CFAオフセット設定命令をまず出力
	cput(DW_CFA_def_cfa_offset_sf);
	sleb128put(cfa / DATAALIGNMENTFACTOR);

	// その後、PCの進行命令を出力
	if (deltapc < 0x40) {
		cput(DW_CFA_advance_loc + deltapc);
	} else if (deltapc < 0x100) {
		// ... (他の advance_loc 命令のバリエーション)
	}

この変更により、putpccfadelta関数が呼び出された時点で、まず現在のPC範囲に適用されるべきCFAオフセットが正確に設定され、その後にPCが次の命令範囲に進むという論理的な流れが確立されます。これにより、デバッガは常に現在のPCに対応する正しいスタックフレーム情報を取得できるようになり、デバッグ情報の正確性が向上します。

cfa / DATAALIGNMENTFACTOR の部分は、DW_CFA_def_cfa_offset_sf 命令が受け取るオフセットが、DATAALIGNMENTFACTOR でスケーリングされた値であることを示しています。これはDWARFの仕様によるもので、オフセットをよりコンパクトに表現するための工夫です。

関連リンク

参考にした情報源リンク