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

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

このコミットは、Go言語の debug/pe パッケージにCOFF (Common Object File Format) シンボルテーブルの処理サポートを追加するものです。これにより、Windowsの実行可能ファイル(PEファイル)からシンボル情報を読み取り、解析する機能が強化されます。

コミット

commit 5373e8a8d706b47c5585226d16e5d5bdf4db825a
Author: Joel Sing <jsing@google.com>
Date:   Sat Sep 22 17:56:49 2012 +1000

    debug/pe: add symbol support
    
    Add support for processing the COFF symbol table.
    
    R=alex.brainman
    CC=golang-dev
    https://golang.org/cl/6551045
---
 src/pkg/debug/pe/file.go      | 48 ++++++++++++++++++++++++++++++++++++++++---
 src/pkg/debug/pe/file_test.go | 30 ++++++++++++++++++++++++++-\
 src/pkg/debug/pe/pe.go        | 11 ++++++++++\
 3 files changed, 85 insertions(+), 4 deletions(-)

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

https://github.com/golang/go/commit/5373e8a8d706b47c5585226d16e5d5bdf4db825a

元コミット内容

debug/pe パッケージにCOFFシンボルテーブルを処理する機能を追加します。

変更の背景

PE (Portable Executable) ファイルは、Windowsオペレーティングシステムで使用される実行可能ファイル、DLL、オブジェクトファイルなどの形式です。これらのファイルには、プログラムの構造やデバッグ情報を含む様々なデータが格納されています。COFF (Common Object File Format) シンボルテーブルは、特にオブジェクトファイルや一部の実行可能ファイルにおいて、関数名、変数名、セクション情報などのシンボル情報を格納するために使用されます。

このコミット以前の debug/pe パッケージは、PEファイルのヘッダやセクション情報の一部を解析する機能を持っていましたが、COFFシンボルテーブルの解析機能は含まれていませんでした。デバッガやプロファイラ、あるいはバイナリ解析ツールがPEファイルをより詳細に理解するためには、シンボル情報の取得が不可欠です。

この変更は、debug/pe パッケージの機能を拡張し、PEファイルからシンボル情報を抽出できるようにすることで、Go言語で書かれたツールがWindowsバイナリをより深く解析できるようにすることを目的としています。これにより、Go言語のエコシステムにおけるWindowsバイナリ解析の能力が向上します。

前提知識の解説

PE (Portable Executable) ファイルフォーマット

PEファイルフォーマットは、Microsoft Windowsオペレーティングシステムで使用される実行可能ファイル、DLL (Dynamic Link Library)、オブジェクトコード、FON (フォント) ファイルなどの標準ファイル形式です。PEファイルは、COFF (Common Object File Format) のデータ構造に基づいています。

PEファイルの主要な構成要素は以下の通りです。

  • DOS Header: 互換性のために存在する古いDOS実行可能ファイルのヘッダ。
  • DOS Stub: DOS環境で実行された場合に表示される小さなプログラム。
  • PE Signature: "PE\0\0" というシグネチャ。
  • COFF File Header: PEファイルの基本的な情報(CPUアーキテクチャ、セクション数、シンボルテーブルへのポインタなど)を格納します。
  • Optional Header: 実行可能ファイルに特有の情報(エントリポイント、イメージベースアドレス、セクションのアライメントなど)を格納します。
  • Section Table: 各セクション(コード、データ、リソースなど)の名前、サイズ、仮想アドレス、ファイル内のオフセットなどの情報を格納します。
  • Sections: 実際のコード、初期化済みデータ、未初期化データ、リソースなどが格納される領域。

COFF (Common Object File Format)

COFFは、Unix系システムで広く使われていたオブジェクトファイル形式であり、PEファイルフォーマットの基盤となっています。COFFは、コンパイラやアセンブラが出力するオブジェクトファイルや、リンカが生成する実行可能ファイルで使用されます。

COFFの主要な特徴は以下の通りです。

  • ファイルヘッダ: ファイルの基本的な属性(マシンタイプ、セクション数、シンボルテーブルのオフセットなど)を定義します。
  • セクションヘッダ: 各セクション(コード、データなど)の属性を定義します。
  • シンボルテーブル: プログラム内のシンボル(関数名、変数名など)とその属性(アドレス、型、ストレージクラスなど)を格納します。
  • リロケーションエントリ: コードやデータ内のアドレス参照を、リンカが最終的なアドレスに解決するために必要な情報を提供します。
  • ライン番号情報: ソースコードの行番号と機械語命令のアドレスのマッピングを提供し、デバッグを可能にします。

COFF シンボルテーブル

COFFシンボルテーブルは、PEファイル(特にオブジェクトファイル)内に存在する、プログラムのシンボル情報を格納するためのデータ構造です。各シンボルは、その名前、値、セクション番号、型、ストレージクラスなどの属性を持ちます。

  • シンボルテーブルの場所: COFFファイルヘッダの PointerToSymbolTable フィールドがシンボルテーブルの開始位置を示します。
  • シンボル数: COFFファイルヘッダの NumberOfSymbols フィールドがシンボルテーブル内のエントリ数を示します。
  • シンボルエントリのサイズ: 各シンボルエントリは固定長(通常18バイト)です。
  • シンボル名: シンボル名が8バイト以下の場合は、シンボルエントリ内に直接格納されます。8バイトを超える長い名前は、COFF文字列テーブルに格納され、シンボルエントリには文字列テーブル内のオフセットが記録されます。
  • 補助シンボル (Auxiliary Symbols): 一部のシンボル(例えば、関数定義や構造体定義)は、追加情報を持つ補助シンボルエントリを伴うことがあります。これらの補助シンボルは、直前の標準シンボルエントリの直後に配置されます。

COFF 文字列テーブル

COFF文字列テーブルは、COFFシンボルテーブルの直後に配置される可変長文字列の集合です。シンボル名が8バイトを超える場合、その名前は文字列テーブルに格納され、シンボルエントリからは文字列テーブル内のオフセットで参照されます。

  • 場所: シンボルテーブルの終了位置の直後。
  • サイズ: 文字列テーブルの最初の4バイトは、テーブル全体のサイズ(サイズフィールド自体を含む)を示します。
  • 形式: 各文字列はヌル終端されます。

技術的詳細

このコミットは、debug/pe パッケージにCOFFシンボルテーブルの解析ロジックを組み込むことで、PEファイルからシンボル情報を抽出できるようにします。

主な変更点は以下の通りです。

  1. File 構造体の拡張: File 構造体に Symbols []*Symbol フィールドが追加され、解析されたシンボル情報が格納されるようになります。
  2. Symbol 構造体の定義: 新たに Symbol 構造体が定義され、COFFシンボルテーブルから読み取られた個々のシンボルの属性(名前、値、セクション番号、型、ストレージクラス)を保持します。
  3. COFFSymbol 構造体の定義: COFFシンボルテーブルの生のエントリ形式に対応する COFFSymbol 構造体が定義されます。これは、ファイルからバイナリデータを直接読み取るためのものです。
    • Name [8]uint8: シンボル名。8バイト以下なら直接格納、それ以上なら文字列テーブルへのオフセット。
    • Value uint32: シンボルの値(アドレスなど)。
    • SectionNumber int16: シンボルが定義されているセクションの番号。
    • Type uint16: シンボルの型(関数、データなど)。
    • StorageClass uint8: シンボルのストレージクラス(外部、静的など)。
    • NumberOfAuxSymbols uint8: このシンボルに続く補助シンボルの数。
  4. シンボルテーブルの読み込みロジック: NewFile 関数内で、COFFファイルヘッダから PointerToSymbolTableNumberOfSymbols の情報を利用して、シンボルテーブルと文字列テーブルを読み込むロジックが追加されます。
    • まず、文字列テーブルのサイズを読み込み、文字列テーブル全体をメモリに読み込みます。
    • 次に、シンボルテーブルの先頭にシークし、NumberOfSymbols の数だけ COFFSymbol エントリをループで読み込みます。
    • COFFSymbol について、名前の処理を行います。
      • cs.Name の最初の4バイトがゼロの場合、残りの4バイトが文字列テーブルへのオフセットとして解釈され、文字列テーブルから名前を読み取ります。
      • それ以外の場合、cs.Name の内容が直接シンボル名として使用されます(ヌル終端されたC文字列として解釈)。
    • NumberOfAuxSymbols を確認し、補助シンボルが存在する場合は、それらをスキップします(現在の実装では補助シンボルの内容は解析せず、単に読み飛ばす)。
    • 解析されたシンボル情報を Symbol 構造体にマッピングし、File.Symbols スライスに追加します。

この変更により、debug/pe パッケージはPEファイルからシンボル情報を効率的に抽出し、Goプログラムで利用可能な形式で提供できるようになります。

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

src/pkg/debug/pe/file.go

  • File 構造体に Symbols []*Symbol フィールドが追加されました。
  • Symbol 構造体が新しく定義されました。
  • NewFile 関数内で、COFFシンボルテーブルと文字列テーブルを読み込み、解析するロジックが追加されました。
    • sr.Seek を使用して、シンボルテーブルと文字列テーブルの開始位置に移動します。
    • binary.Read を使用して、バイナリデータを COFFSymbol 構造体に読み込みます。
    • シンボル名の処理ロジック(短い名前と文字列テーブルからの長い名前の読み込み)が実装されました。
    • 補助シンボルのスキップロジックが追加されました。
    • 解析された Symbol オブジェクトが f.Symbols に追加されます。

src/pkg/debug/pe/file_test.go

  • fileTest 構造体に symbols []*Symbol フィールドが追加されました。
  • テストデータ fileTests に、期待されるシンボル情報の配列が追加されました。これにより、シンボル解析機能が正しく動作するかを検証できます。
  • TestOpen 関数内で、解析されたシンボルが期待されるシンボルと一致するかを reflect.DeepEqual を使用して比較するアサーションが追加されました。

src/pkg/debug/pe/pe.go

  • COFFSymbolSize 定数(18バイト)が定義されました。
  • COFFSymbol 構造体が新しく定義されました。これは、COFFシンボルテーブルの各エントリのバイナリレイアウトを正確に反映しています。

コアとなるコードの解説

src/pkg/debug/pe/file.go の変更点

// File 構造体に Symbols フィールドを追加
type File struct {
	FileHeader
	Sections []*Section
	Symbols  []*Symbol // 新しく追加されたシンボル情報
	closer io.Closer
}

// Symbol 構造体の定義
type Symbol struct {
	Name          string
	Value         uint32
	SectionNumber int16
	Type          uint16
	StorageClass  uint8
}

// NewFile 関数内のシンボル解析ロジック
func NewFile(r io.ReaderAt) (*File, error) {
	// ... (既存のヘッダ読み込みロジック) ...

	// COFF文字列テーブルの取得
	// PointerToSymbolTable + COFFSymbolSize * NumberOfSymbols の位置に文字列テーブルが始まる
	sr.Seek(int64(f.FileHeader.PointerToSymbolTable+COFFSymbolSize*f.FileHeader.NumberOfSymbols), os.SEEK_SET)
	var l uint32
	if err := binary.Read(sr, binary.LittleEndian, &l); err != nil {
		return nil, err
	}
	ss := make([]byte, l) // 文字列テーブルのサイズ l を持つバイトスライス
	// 文字列テーブル全体を読み込む
	if _, err := r.ReadAt(ss, int64(f.FileHeader.PointerToSymbolTable+COFFSymbolSize*f.FileHeader.NumberOfSymbols)); err != nil {
		return nil, err
	}

	// COFFシンボルテーブルの処理
	sr.Seek(int64(f.FileHeader.PointerToSymbolTable), os.SEEK_SET) // シンボルテーブルの先頭にシーク
	aux := uint8(0) // 補助シンボルのカウンタ
	for i := 0; i < int(f.FileHeader.NumberOfSymbols); i++ {
		cs := new(COFFSymbol)
		if err := binary.Read(sr, binary.LittleEndian, cs); err != nil {
			return nil, err
		}
		if aux > 0 { // 補助シンボルであればスキップ
			aux--
			continue
		}
		var name string
		// シンボル名の処理: 短い名前か、文字列テーブルからの長い名前か
		if cs.Name[0] == 0 && cs.Name[1] == 0 && cs.Name[2] == 0 && cs.Name[3] == 0 {
			// 最初の4バイトがゼロの場合、残りの4バイトが文字列テーブルへのオフセット
			si := int(binary.LittleEndian.Uint32(cs.Name[4:]))
			name, _ = getString(ss, si) // 文字列テーブルから名前を取得
		} else {
			// それ以外の場合、Nameフィールドが直接シンボル名
			name = cstring(cs.Name[:]) // C文字列として解釈
		}
		aux = cs.NumberOfAuxSymbols // 次の補助シンボル数を設定

		// Symbol 構造体を作成し、File.Symbols に追加
		s := &Symbol{
			Name:          name,
			Value:         cs.Value,
			SectionNumber: cs.SectionNumber,
			Type:          cs.Type,
			StorageClass:  cs.StorageClass,
		}
		f.Symbols = append(f.Symbols, s)
	}

	// ... (既存のセクション処理ロジック) ...
}

このコードは、PEファイルのCOFFシンボルテーブルとそれに続く文字列テーブルを読み込み、解析する中心的なロジックです。

  1. 文字列テーブルの読み込み:

    • f.FileHeader.PointerToSymbolTablef.FileHeader.NumberOfSymbols を使って、文字列テーブルの開始オフセットを計算します。COFFシンボルエントリのサイズ COFFSymbolSize (18バイト) を NumberOfSymbols に掛けて、シンボルテーブルの総バイト数を求め、それを PointerToSymbolTable に加算します。
    • その位置から最初の4バイトを l (uint32) として読み込み、これが文字列テーブルの全長となります。
    • make([]byte, l) でそのサイズのバイトスライス ss を作成し、r.ReadAt で文字列テーブル全体を ss に読み込みます。
  2. シンボルテーブルの読み込みと解析:

    • sr.Seek でシンボルテーブルの先頭に戻ります。
    • f.FileHeader.NumberOfSymbols の数だけループを回し、各シンボルエントリを処理します。
    • binary.Read(sr, binary.LittleEndian, cs) で、18バイトの生データを COFFSymbol 構造体に読み込みます。
    • 補助シンボルのスキップ: aux > 0 の場合、現在のエントリは直前のシンボルの補助シンボルであるため、解析をスキップし、aux をデクリメントして次のループに進みます。
    • シンボル名の解決:
      • cs.Name の最初の4バイトがすべてゼロの場合、これは名前が文字列テーブルに格納されていることを示します。cs.Name[4:] の4バイトを binary.LittleEndian.Uint32si (文字列テーブル内のオフセット) に変換し、getString(ss, si) 関数(既存のヘルパー関数)を使って文字列テーブル ss から実際のシンボル名を取得します。
      • それ以外の場合、cs.Name フィールド自体にシンボル名が直接格納されています。cstring(cs.Name[:]) 関数(既存のヘルパー関数)を使って、バイトスライスをヌル終端されたC文字列としてGoの文字列に変換します。
    • aux = cs.NumberOfAuxSymbols で、現在のシンボルに続く補助シンボルの数を aux に設定します。これにより、次のループで補助シンボルが正しくスキップされます。
    • 最後に、解析されたシンボル情報(名前、値、セクション番号、型、ストレージクラス)を Symbol 構造体に格納し、f.Symbols スライスに追加します。

src/pkg/debug/pe/pe.go の変更点

const COFFSymbolSize = 18 // COFFシンボルエントリのサイズ (バイト)

// COFFSymbol 構造体の定義
type COFFSymbol struct {
	Name               [8]uint8 // シンボル名 (8バイト)
	Value              uint32   // シンボルの値
	SectionNumber      int16    // シンボルが定義されているセクションの番号
	Type               uint16   // シンボルの型
	StorageClass       uint8    // シンボルのストレージクラス
	NumberOfAuxSymbols uint8    // このシンボルに続く補助シンボルの数
}

この構造体は、PEファイル内のCOFFシンボルテーブルの各エントリのバイナリレイアウトを正確にマッピングしています。binary.Read 関数はこの構造体を利用して、ファイルから直接バイナリデータをGoの構造体に読み込みます。NumberOfAuxSymbols フィールドは、このシンボルに続く補助シンボルの数を指定し、解析時に補助シンボルをスキップするために使用されます。

これらの変更により、debug/pe パッケージはPEファイルからシンボル情報を抽出し、デバッグや解析ツールで利用できる形式で提供する能力を獲得しました。

関連リンク

参考にした情報源リンク