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

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

このコミットは、Go言語のリンカ(ld)が生成するDWARFデバッグ情報の出力形式に関する変更です。具体的には、構造体(struct)のフィールドオフセットを表現する方法が修正されています。変更対象のファイルは src/cmd/ld/dwarf.c であり、これはGoリンカにおけるDWARF情報の生成ロジックを司る部分です。

コミット

commit 125d1e9269572135d078ee128b239280c7101e6c
Author: Rob Pike <r@golang.org>
Date:   Fri Jul 11 15:16:00 2014 +0000

    ld: change DWARF output for structs
    The debug/dwarf package cannot parse the format generated here,
    but the format can be changed so it does.
    After this edit, tweaking the expression defining the offset
    of a struct field, the dwarf package can parse the tables (again?).
    
    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/105710043

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

https://github.com/golang/go/commit/125d1e9269572135d078ee128b239280c7101e6c

元コミット内容

Goリンカ(ld)が構造体のDWARF出力を変更する。 現在生成されているフォーマットは debug/dwarf パッケージでパースできないが、変更することでパース可能になる。 この編集後、構造体フィールドのオフセットを定義する式を微調整することで、dwarf パッケージはテーブルを(再び?)パースできるようになる。

変更の背景

このコミットの主な背景は、Go言語自身のデバッグ情報解析パッケージである debug/dwarf が、Goリンカが生成するDWARF情報の一部、特に構造体フィールドのオフセット表現を正しく解釈できないという互換性の問題にありました。

デバッグ情報は、プログラムの実行中にデバッガが変数、型、関数、ソースコードの行番号などを理解するために不可欠です。Go言語では、コンパイルされたバイナリにDWARF形式のデバッグ情報が埋め込まれます。しかし、リンカが生成する特定のDWARF表現が、Go標準ライブラリの debug/dwarf パッケージの期待する形式と異なっていたため、Goツールチェイン内でデバッグ情報の自己解析に問題が生じていました。

コミットメッセージにある「(再び?)」という記述は、以前は正しくパースできていた時期があったか、あるいはこの問題がリグレッションによって発生した可能性を示唆しています。この変更は、GoのデバッグツールがGo自身によって生成されたデバッグ情報を確実に利用できるようにするための修正であり、Go開発者やデバッガ利用者にとって重要な改善となります。

前提知識の解説

DWARF (Debugging With Attributed Record Formats)

DWARFは、ソースレベルデバッガがプログラムの実行を分析するために必要な情報を提供する標準的なデバッグ情報形式です。コンパイルされた実行可能ファイルに埋め込まれ、変数名、型情報、関数、ソースコードの行番号と実行可能コードのアドレスのマッピング、スタックフレームの構造など、多岐にわたる情報を含みます。デバッガはこれらの情報を用いて、ユーザーがソースコードレベルでプログラムをステップ実行したり、変数の値を検査したりすることを可能にします。

Goリンカ (cmd/ld)

Go言語のビルドプロセスにおいて、リンカ(cmd/ld)は重要な役割を担います。コンパイラによって生成されたオブジェクトファイル(.o)を結合し、必要なライブラリとリンクして、最終的な実行可能バイナリを生成します。この過程で、リンカはデバッグ情報(DWARF)も生成し、バイナリに埋め込みます。

構造体 (Structs) とフィールドオフセット

Go言語における構造体は、異なる型のフィールドをまとめた複合データ型です。例えば、type Person struct { Name string; Age int } のような構造体は、NameAge というフィールドを持ちます。メモリ上では、これらのフィールドは構造体の先頭から一定のオフセット(バイト単位の距離)に配置されます。デバッガが構造体の特定のフィールドの値を読み取るためには、そのフィールドが構造体のベースアドレスからどれだけ離れているか、つまりフィールドオフセットを知る必要があります。このオフセット情報はDWARFによって提供されます。

DWARF式 (DWARF Expressions) と DW_OP_ オペコード

DWARFは、変数やメモリ位置を記述するために「DWARF式」と呼ばれるバイトコードのシーケンスを使用します。これらの式は、スタックベースの仮想マシン上で評価され、目的のメモリ位置や値を計算します。式は DW_OP_ で始まるオペコード(操作コード)と、それに続くオペランドで構成されます。

  • DW_OP_consts: スタックに符号付き定数(signed constant)をプッシュします。定数はLEB128形式でエンコードされます。
  • DW_OP_plus: スタックから2つの値をポップし、それらを加算した結果をスタックにプッシュします。
  • DW_OP_plus_uconst: スタックから1つの値をポップし、それにオペランドとして与えられた符号なし定数(unsigned constant)を加算した結果をスタックにプッシュします。このオペコードは、特にオフセットの加算など、単一の加算操作をよりコンパクトに表現するために導入されました。

LEB128 (Little Endian Base 128) エンコーディング

LEB128は、可変長整数エンコーディングの一種で、DWARFを含む多くのバイナリフォーマットで数値のコンパクトな表現に使用されます。数値の絶対値が小さいほど、より少ないバイト数で表現できます。符号付き(sleb128enc)と符号なし(uleb128enc)のバリアントがあります。

技術的詳細

このコミットの技術的な核心は、構造体フィールドのオフセットを表現するDWARF式の変更にあります。

変更前は、Goリンカは構造体フィールドのオフセットを表現するために、DW_OP_constsDW_OP_plus の2つのオペコードの組み合わせを使用していました。

  1. DW_OP_consts: フィールドのオフセット値を符号付き定数としてスタックにプッシュします。
  2. DW_OP_plus: スタックのトップにある2つの値(この場合、ベースアドレスとプッシュされたオフセット値)をポップし、それらを加算して結果をスタックにプッシュします。

この2段階のアプローチは機能的には正しいものの、Goの debug/dwarf パッケージがこの特定のシーケンスを期待通りにパースできないという問題がありました。

変更後は、DW_OP_plus_uconst という単一のオペコードを使用するように修正されました。

  1. DW_OP_plus_uconst: スタックから1つの値(ベースアドレス)をポップし、そのオペコードの直後に続くオペランドとしてエンコードされた符号なし定数(フィールドオフセット)を加算します。結果はスタックにプッシュされます。

DW_OP_plus_uconst は、オフセットのような符号なしの定数を加算する際に、より効率的でコンパクトな表現を提供します。これは、DW_OP_constsDW_OP_plus の組み合わせが2つのオペコードと1つのオペランドを必要とするのに対し、DW_OP_plus_uconst は1つのオペコードと1つのオペランドで同じ操作を完了できるためです。

この変更により、Goリンカが生成するDWARFのフィールドオフセット表現が、debug/dwarf パッケージが期待する形式と一致するようになり、互換性の問題が解消されました。また、sleb128enc (signed LEB128 encoding) から uleb128enc (unsigned LEB128 encoding) への変更は、DW_OP_plus_uconst が符号なし定数をオペランドとして取ることに対応しています。構造体フィールドのオフセットは通常、非負の値であるため、符号なしエンコーディングは適切です。

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

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

--- a/src/cmd/ld/dwarf.c
+++ b/src/cmd/ld/dwarf.c
@@ -835,11 +835,8 @@ newmemberoffsetattr(DWDie *die, int32 offs)\n 	int i;\n \n 	i = 0;\n-	if (offs != 0) {\n-		block[i++] = DW_OP_consts;\n-		i += sleb128enc(offs, block+i);\n-		block[i++] = DW_OP_plus;\n-	}\n+	block[i++] = DW_OP_plus_uconst;\n+	i += uleb128enc(offs, block+i);\n 	newattr(die, DW_AT_data_member_location, DW_CLS_BLOCK, i, mal(i));\n 	memmove(die->attr->data, block, i);\n }\n```

## コアとなるコードの解説

`newmemberoffsetattr` 関数は、DWARFの `DW_AT_data_member_location` 属性を生成する役割を担っています。この属性は、構造体のメンバー(フィールド)が、その構造体のベースアドレスからどれだけ離れた位置にあるか(オフセット)を記述するために使用されます。

変更前のコードでは、`offs` (オフセット値) が0でない場合にのみ、以下のDWARF式を生成していました。

```c
		block[i++] = DW_OP_consts; // オフセット値をスタックにプッシュ
		i += sleb128enc(offs, block+i); // オフセット値をLEB128でエンコード
		block[i++] = DW_OP_plus;   // スタックの2つの値を加算

これは、オフセットが0の場合は何もせず、0でない場合に DW_OP_constsDW_OP_plus を使ってオフセットを加算するロジックでした。

変更後のコードでは、if (offs != 0) の条件分岐が削除され、常に以下のDWARF式を生成するようになりました。

	block[i++] = DW_OP_plus_uconst; // ベースアドレスにオフセットを加算
	i += uleb128enc(offs, block+i); // オフセット値を符号なしLEB128でエンコード

これにより、オフセットが0の場合でも DW_OP_plus_uconst が使用されます。DW_OP_plus_uconst は、オペランドとして与えられた符号なし定数をスタックのトップの値に加算するため、オフセットが0であれば結果は元の値のままとなり、論理的には問題ありません。

また、オフセット値のエンコーディングが sleb128enc (signed LEB128) から uleb128enc (unsigned LEB128) に変更されています。これは、DW_OP_plus_uconst が符号なし定数を期待するためです。構造体フィールドのオフセットは通常、非負の値であるため、この変更は適切であり、DWARFの仕様に準拠しています。

この修正により、Goリンカが生成するDWARFデバッグ情報が、Go自身の debug/dwarf パッケージによって正しく解釈されるようになり、デバッグツールの互換性と信頼性が向上しました。

関連リンク

参考にした情報源リンク

  • DWARF Standard (特にセクション 2.5.1.2 "Location Expressions" および 7.7 "Location List Entries"): https://dwarfstd.org/doc/DWARF5.pdf
  • Web search results for "DWARF DW_OP_plus_uconst vs DW_OP_consts DW_OP_plus"
  • Go source code for src/cmd/ld/dwarf.c
  • Go source code for debug/dwarf (内部実装の理解のため)