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

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

このコミットは、Go言語のリンカ (cmd/ld) におけるPE (Portable Executable) フォーマットのシンボルテーブルにおけるセクション番号の扱いに関する修正です。具体的には、シンボルが属するセクションの決定ロジックと、シンボル値の計算方法が改善されています。

コミット

commit cf78f96244ec8e3a69f10069fb43dd720f1556b0
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Tue May 6 11:40:43 2014 +1000

    cmd/ld: correct pe section number in symbol table

    Update #7899

    LGTM=iant
    R=golang-codereviews, rsc, iant
    CC=golang-codereviews
    https://golang.org/cl/97920044

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

https://github.com/golang/go/commit/cf78f96244ec8e3a69f10069fb43dd720f1556b0

元コミット内容

cmd/ld: correct pe section number in symbol table

Update #7899

変更の背景

このコミットは、Go言語のリンカがWindows向けの実行可能ファイル(PEフォーマット)を生成する際に、シンボルテーブル内のセクション番号が正しく設定されないという問題(Issue #7899)を修正するために行われました。

PEフォーマットでは、実行可能ファイルは複数のセクション(例: コードセクション、データセクション)に分割されます。シンボル(関数名や変数名など)は、そのシンボルが定義されているセクションへの参照を持つ必要があります。以前の実装では、シンボルが属するセクションの決定ロジックが不正確であり、特にデータセクション内のシンボルに対して問題が発生していました。これにより、生成された実行可能ファイルのデバッグ情報やシンボル解決に不整合が生じる可能性がありました。

Issue #7899の報告によると、この問題は特にWindows環境でのGoプログラムのデバッグにおいて、シンボル情報が正しく表示されない、あるいはシンボル解決が失敗するといった形で現れていました。この修正は、PEフォーマットの仕様に厳密に従い、シンボルがそのアドレスに基づいて正しいセクションにマッピングされるようにすることで、これらの問題を解決することを目的としています。

前提知識の解説

  • リンカ (Linker): コンパイラによって生成されたオブジェクトファイル(機械語コードとデータを含む)を結合し、実行可能なプログラムやライブラリを作成するツールです。リンカは、異なるオブジェクトファイル間の参照(例えば、あるファイルで定義された関数を別のファイルから呼び出す場合)を解決し、最終的な実行可能ファイルのメモリレイアウトを決定します。Go言語では、cmd/ld がこのリンカの役割を担っています。

  • PE (Portable Executable) フォーマット: Microsoft Windowsオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、DLL (Dynamic Link Library) などのファイル形式です。PEファイルは、ヘッダ、セクションテーブル、および複数のセクション(例: .text (コード), .data (初期化済みデータ), .rdata (読み取り専用データ) など)で構成されます。

  • シンボルテーブル (Symbol Table): 実行可能ファイルやオブジェクトファイル内に含まれるデータ構造で、プログラム内のシンボル(関数名、変数名、ラベルなど)とそのアドレスや型などの情報が格納されています。デバッガやリンカは、このシンボルテーブルを利用して、プログラムの実行中に特定のコードやデータにアクセスしたり、デバッグ情報を表示したりします。

  • セクション (Section): PEファイル内の論理的なブロックで、特定の種類のデータやコードを格納します。例えば、.text セクションには実行可能なコードが、.data セクションには初期化されたグローバル変数や静的変数が、.rdata セクションには読み取り専用データ(文字列リテラルなど)が格納されます。各セクションは、ファイル内で特定のオフセットとサイズを持ち、メモリにロードされる際にも特定の仮想アドレスにマッピングされます。

  • セクション番号 (Section Number): シンボルテーブルのエントリには、そのシンボルが属するセクションを示す番号が含まれることがあります。これは、シンボルがどのセクションに定義されているかをリンカやデバッガが識別するために使用されます。

  • LSym 構造体: Go言語のリンカ内部で使用されるシンボルを表す構造体です。シンボルの名前、値(アドレス)、型、サイズなどの情報が含まれます。

  • COFFSym 構造体: PEフォーマットのシンボルテーブルエントリに対応する内部構造体です。このコミットでは、COFFSymvalue フィールドが追加され、シンボルのセクション内オフセットを保持するようになりました。

技術的詳細

このコミットの主要な変更点は、src/cmd/ld/pe.c ファイル内の addsym 関数と、それに付随するシンボル値の計算ロジックです。

以前の実装では、addsym 関数内でシンボルがテキストセクション (textsect) に属するかデータセクション (datasect) に属するかを、シンボルの type フィールドに基づいて単純に判断していました。しかし、これはシンボルの実際のアドレスとセクションの仮想アドレス範囲との整合性を保証するものではありませんでした。特に、データセクション内のシンボルが正しく識別されないケースがありました。

新しい実装では、COFFSym 構造体に value フィールドが追加されました。この value フィールドは、シンボルが属するセクションの先頭からのオフセットを格納するために使用されます。

addsym 関数内では、シンボルの s->value (シンボルの仮想アドレス) を segdata.vaddr (データセクションの仮想アドレス) および segtext.vaddr (テキストセクションの仮想アドレス) と比較することで、シンボルがどのセクションに属するかをより正確に判断するようになりました。

  1. s->value >= segdata.vaddr の場合: シンボルはデータセクションに属すると判断され、cs->values->value - segdata.vaddr として計算されます。これは、シンボルのアドレスからデータセクションの開始アドレスを引くことで、データセクション内でのオフセットを求めるものです。cs->sectdatasect に設定されます。
  2. s->value >= segtext.vaddr の場合: シンボルはテキストセクションに属すると判断され、cs->values->value - segtext.vaddr として計算されます。これは、シンボルのアドレスからテキストセクションの開始アドレスを引くことで、テキストセクション内でのオフセットを求めるものです。cs->secttextsect に設定されます。
  3. 上記いずれにも該当しない場合: シンボルはどの既知のセクションにも属さないと判断され、cs->valuecs->sect は0に設定され、診断メッセージ (diag) が出力されます。

また、以前存在した datoffsect 関数は削除されました。この関数はシンボルのアドレスからセクションオフセットを計算しようとしていましたが、新しい addsym 関数内のロジックに置き換えられ、より正確なセクション判定とオフセット計算が可能になりました。

最終的に、addsymtable 関数内でシンボルテーブルエントリを書き込む際に、以前は datoffsect(s->sym->value) を使用していた箇所が、新しく計算された s->value (COFFSymのvalueフィールド) を直接使用するように変更されました。これにより、PEシンボルテーブルの Value フィールドに、シンボルが属するセクションの先頭からの正しいオフセットが書き込まれるようになります。

この変更により、WindowsのPEフォーマットにおけるシンボルテーブルのセクション情報が正確になり、デバッガがGoプログラムのシンボルを正しく解決できるようになります。

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

src/cmd/ld/pe.c ファイルが変更されています。

--- a/src/cmd/ld/pe.c
+++ b/src/cmd/ld/pe.c
@@ -84,6 +84,7 @@ struct COFFSym
 	LSym* sym;
 	int strtbloff;
 	int sect;
+	vlong value; // 追加
 };
 
 static COFFSym* coffsym;
@@ -476,6 +477,7 @@ newPEDWARFSection(char *name, vlong size)
 static void
 addsym(LSym *s, char *name, int type, vlong addr, vlong size, int ver, LSym *gotype)
 {
+	COFFSym *cs; // 追加
 	USED(name);
 	USED(addr);
 	USED(size);
@@ -498,28 +500,25 @@ addsym(LSym *s, char *name, int type, vlong addr, vlong size, int ver, LSym *got
 	}
 
 	if(coffsym) {
-		coffsym[ncoffsym].sym = s;
+		cs = &coffsym[ncoffsym]; // 変更: csポインタを使用
+		cs->sym = s;
 		if(strlen(s->name) > 8)
-			coffsym[ncoffsym].strtbloff = strtbladd(s->name);
-		if(type == 'T')
-			coffsym[ncoffsym].sect = textsect;
-		else
-			coffsym[ncoffsym].sect = datasect;
+			cs->strtbloff = strtbladd(s->name); // 変更: csポインタを使用
+		if(s->value >= segdata.vaddr) { // 変更: シンボル値に基づいてセクションを判定
+			cs->value = s->value - segdata.vaddr;
+			cs->sect = datasect;
+		} else if(s->value >= segtext.vaddr) {
+			cs->value = s->value - segtext.vaddr;
+			cs->sect = textsect;
+		} else {
+			cs->value = 0;
+			cs->sect = 0;
+			diag("addsym %#llx", addr);
+		}
 	}
 	ncoffsym++;
 }
 
-- static vlong // 削除: datoffsect関数
-- datoffsect(vlong addr)
-- {
-- 	if(addr >= segdata.vaddr)
-- 		return addr - segdata.vaddr;
-- 	if(addr >= segtext.vaddr)
-- 		return addr - segtext.vaddr;
-- 	diag("datoff %#llx", addr);
-- 	return 0;
-- }
--
 static void
 addsymtable(void)
 {
@@ -551,7 +549,7 @@ addsymtable(void)
 		lputl(0);
 		lputl(s->strtbloff);
 	}
-	lputl(datoffsect(s->sym->value)); // 変更: datoffsectの呼び出しを削除
+	lputl(s->value); // 変更: COFFSymのvalueフィールドを直接使用
 	wputl(s->sect);
 	wputl(0x0308);  // "array of structs"
 	cput(2);        // storage class: external

コアとなるコードの解説

  1. COFFSym 構造体への value フィールドの追加:

    struct COFFSym
    {
        LSym* sym;
        int strtbloff;
        int sect;
        vlong value; // 追加されたフィールド
    };
    

    この value フィールドは、PEシンボルテーブルの Value フィールドに対応し、シンボルが属するセクションの先頭からのオフセットを格納するために導入されました。これにより、シンボルのアドレスがセクションの仮想アドレス範囲内でどこに位置するかを正確に表現できるようになります。

  2. addsym 関数の変更:

    • COFFSym *cs; というポインタ変数が導入され、coffsym[ncoffsym] へのアクセスを簡潔にしています。
    • シンボルのセクション判定ロジックが大幅に変更されました。以前はシンボルの type に基づいていましたが、新しいロジックでは s->value (シンボルの仮想アドレス) を segdata.vaddr (データセクションの開始仮想アドレス) および segtext.vaddr (テキストセクションの開始仮想アドレス) と比較することで、シンボルがどのセクションに属するかを決定します。
      if(s->value >= segdata.vaddr) {
          cs->value = s->value - segdata.vaddr; // データセクション内のオフセットを計算
          cs->sect = datasect;
      } else if(s->value >= segtext.vaddr) {
          cs->value = s->value - segtext.vaddr; // テキストセクション内のオフセットを計算
          cs->sect = textsect;
      } else {
          cs->value = 0;
          cs->sect = 0;
          diag("addsym %#llx", addr); // どのセクションにも属さない場合の診断メッセージ
      }
      
      この変更により、シンボルが実際にメモリ上で配置されるアドレスに基づいて、正しいセクションとセクション内オフセットが決定されるようになりました。
  3. datoffsect 関数の削除: 以前のバージョンに存在した datoffsect 関数は、シンボルのアドレスからセクションオフセットを計算する役割を担っていましたが、そのロジックが addsym 関数内に統合され、より堅牢なセクション判定ロジックに置き換えられたため、削除されました。

  4. addsymtable 関数の変更: シンボルテーブルエントリをファイルに書き込む際、以前は datoffsect(s->sym->value) を呼び出してシンボル値を計算していましたが、このコミットでは s->value (COFFSym構造体の新しい value フィールド) を直接使用するように変更されました。

    lputl(s->value); // COFFSymのvalueフィールドを直接使用
    wputl(s->sect);
    

    これにより、addsym 関数で正確に計算されたセクション内オフセットが、PEシンボルテーブルの Value フィールドに正しく書き込まれることが保証されます。

これらの変更により、Goリンカが生成するWindows PE実行可能ファイルのシンボルテーブルの正確性が向上し、デバッグツールがシンボル情報をより信頼性高く利用できるようになりました。

関連リンク

参考にした情報源リンク