[インデックス 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ファイルからシンボル情報を抽出できるようにします。
主な変更点は以下の通りです。
File
構造体の拡張:File
構造体にSymbols []*Symbol
フィールドが追加され、解析されたシンボル情報が格納されるようになります。Symbol
構造体の定義: 新たにSymbol
構造体が定義され、COFFシンボルテーブルから読み取られた個々のシンボルの属性(名前、値、セクション番号、型、ストレージクラス)を保持します。COFFSymbol
構造体の定義: COFFシンボルテーブルの生のエントリ形式に対応するCOFFSymbol
構造体が定義されます。これは、ファイルからバイナリデータを直接読み取るためのものです。Name [8]uint8
: シンボル名。8バイト以下なら直接格納、それ以上なら文字列テーブルへのオフセット。Value uint32
: シンボルの値(アドレスなど)。SectionNumber int16
: シンボルが定義されているセクションの番号。Type uint16
: シンボルの型(関数、データなど)。StorageClass uint8
: シンボルのストレージクラス(外部、静的など)。NumberOfAuxSymbols uint8
: このシンボルに続く補助シンボルの数。
- シンボルテーブルの読み込みロジック:
NewFile
関数内で、COFFファイルヘッダからPointerToSymbolTable
とNumberOfSymbols
の情報を利用して、シンボルテーブルと文字列テーブルを読み込むロジックが追加されます。- まず、文字列テーブルのサイズを読み込み、文字列テーブル全体をメモリに読み込みます。
- 次に、シンボルテーブルの先頭にシークし、
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シンボルテーブルとそれに続く文字列テーブルを読み込み、解析する中心的なロジックです。
-
文字列テーブルの読み込み:
f.FileHeader.PointerToSymbolTable
とf.FileHeader.NumberOfSymbols
を使って、文字列テーブルの開始オフセットを計算します。COFFシンボルエントリのサイズCOFFSymbolSize
(18バイト) をNumberOfSymbols
に掛けて、シンボルテーブルの総バイト数を求め、それをPointerToSymbolTable
に加算します。- その位置から最初の4バイトを
l
(uint32) として読み込み、これが文字列テーブルの全長となります。 make([]byte, l)
でそのサイズのバイトスライスss
を作成し、r.ReadAt
で文字列テーブル全体をss
に読み込みます。
-
シンボルテーブルの読み込みと解析:
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.Uint32
でsi
(文字列テーブル内のオフセット) に変換し、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ファイルからシンボル情報を抽出し、デバッグや解析ツールで利用できる形式で提供する能力を獲得しました。