[インデックス 14091] ファイルの概要
このコミットは、Go言語のdebug/pe
パッケージにおけるPortable Executable (PE) ファイルの解析に関する改善とバグ修正を扱っています。具体的には、シンボルテーブルを持たないPEファイルのサポートと、DOSヘッダー内のe_lfanew
フィールドの解釈の修正が主な変更点です。
コミット
commit e9f0fc8823178470fa429379ba873567b8496f8c
Author: Robin Eklind <r.eklind.87@gmail.com>
Date: Tue Oct 9 11:15:53 2012 +1100
debug/pe: support PE files which contain no symbol table (if NumberOfSymbols is equal to 0 in the IMAGE_FILE_HEADER structure).
No longer assume that e_lfanew (in the IMAGE_DOS_HEADER strcuture) is always one byte. It is now regarded as a 4 byte uint32.
Fixes #4177.
R=golang-dev, alex.brainman, dave, minux.ma
CC=golang-dev
https://golang.org/cl/6587048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e9f0fc8823178470fa429379ba873567b8496f8c
元コミット内容
debug/pe
: IMAGE_FILE_HEADER
構造体内のNumberOfSymbols
が0の場合、シンボルテーブルを含まないPEファイルをサポートします。
IMAGE_DOS_HEADER
構造体内のe_lfanew
が常に1バイトであるという仮定を廃止しました。これは現在、4バイトのuint32
として扱われます。
Issue #4177を修正します。
変更の背景
このコミットは、主に2つの問題に対処しています。
-
シンボルテーブルを持たないPEファイルのサポート: 以前の
debug/pe
パッケージの実装では、PEファイルが常にシンボルテーブルを持つことを前提としていました。しかし、すべてのPEファイルがシンボルテーブルを持つわけではありません。特に、デバッグ情報が不要な場合や、サイズを最小限に抑えたい場合には、シンボルテーブルが省略されることがあります。このようなファイルに対して、既存のパーサーがエラーを発生させるか、正しく解析できない問題がありました。この変更により、IMAGE_FILE_HEADER
のNumberOfSymbols
フィールドが0であるPEファイルも正しく処理できるようになります。 -
e_lfanew
フィールドの誤った解釈: PEファイルは、MS-DOS互換のヘッダー(IMAGE_DOS_HEADER
)で始まります。このヘッダーには、PEヘッダーへのオフセットを示すe_lfanew
というフィールドがあります。以前の実装では、このフィールドが常に1バイトであると誤って仮定されていました。しかし、PEファイルフォーマットの仕様では、これは4バイトのuint32
として定義されています。この誤った解釈が、特定のPEファイルの解析時に問題を引き起こしていました。コミットメッセージで言及されているFixes #4177
は、このe_lfanew
の誤った解釈に起因するバグを指していると考えられます。
これらの問題は、debug/pe
パッケージがより広範な種類のPEファイルを正確に解析できるようにするために修正する必要がありました。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
- Portable Executable (PE) フォーマット: PEフォーマットは、Windowsオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、DLLなどのファイル形式です。これは、MicrosoftのCommon Object File Format (COFF) のデータ構造に基づいています。PEファイルは、ヘッダー、セクション、データディレクトリなど、複数の構造化されたコンポーネントで構成されています。
IMAGE_DOS_HEADER
: PEファイルの先頭に位置するMS-DOS互換のヘッダーです。これは、PEファイルがMS-DOS環境でも実行可能であることを保証するためのもので、古いDOSプログラムがPEファイルを認識できない場合に「This program cannot be run in DOS mode.」のようなメッセージを表示するために使用されます。重要なフィールドの一つにe_lfanew
があります。e_lfanew
:IMAGE_DOS_HEADER
内のフィールドで、PEヘッダー(IMAGE_NT_HEADERS
)のファイル先頭からのオフセット(バイト単位)を示します。このオフセットは、PEヘッダーの開始位置を特定するために使用されます。IMAGE_FILE_HEADER
: PEヘッダーの一部であり、PEファイルの基本的な属性(マシンタイプ、セクション数、タイムスタンプなど)を定義します。このヘッダーには、シンボルテーブルに関する情報も含まれています。NumberOfSymbols
:IMAGE_FILE_HEADER
内のフィールドで、COFFシンボルテーブル内のシンボルの数を指定します。この値が0の場合、ファイルにはCOFFシンボルテーブルが存在しないことを意味します。- COFF (Common Object File Format): Unix系のシステムで広く使われているオブジェクトファイルフォーマットです。PEフォーマットはCOFFの概念を多く取り入れています。COFFシンボルテーブルは、プログラム内の関数、変数、セクションなどのシンボル情報を含みます。
io.ReaderAt
: Go言語のインターフェースで、任意のオフセットからデータを読み取る機能を提供します。ファイルのようなランダムアクセス可能なデータソースを扱う際に便利です。binary.LittleEndian
: Go言語のencoding/binary
パッケージで提供されるバイトオーダー(エンディアン)の指定です。WindowsのPEファイルはリトルエンディアンでデータを格納するため、この指定が必要です。os.SEEK_SET
:io.Seeker
インターフェースで使用される定数で、シーク操作の基準位置をファイルの先頭に設定します。
技術的詳細
このコミットは、src/pkg/debug/pe/file.go
ファイル内のNewFile
関数に焦点を当てています。この関数は、io.ReaderAt
インターフェースを実装するデータソースからPEファイルを解析し、*File
構造体を構築します。
変更点は大きく2つあります。
-
e_lfanew
の解釈の修正:- 以前のコードでは、
dosheader[0x3c]
からPEヘッダーのオフセットを直接1バイトとして読み取ろうとしていました。これは、dosheader
がバイトスライスであり、dosheader[0x3c]
がそのオフセットのバイト値を示すという誤解に基づいていた可能性があります。 - 修正後、
int64(binary.LittleEndian.Uint32(dosheader[0x3c:]))
という行が追加されました。これは、dosheader
のオフセット0x3c
から始まる4バイトをリトルエンディアンのuint32
として読み取り、それをint64
にキャストしてsignoff
変数に格納します。このsignoff
がPEヘッダーの正確なオフセットとなります。 - これにより、
base
変数の計算もbase = signoff + 4
に変更され、PEシグネチャ("PE\0\0")の直後からPEヘッダーの解析を開始するようになります。
- 以前のコードでは、
-
シンボルテーブルの存在チェックの追加:
- 以前のコードでは、
f.FileHeader.PointerToSymbolTable
とf.FileHeader.NumberOfSymbols
を使用して、無条件にCOFFシンボルテーブルと文字列テーブルを読み込もうとしていました。NumberOfSymbols
が0の場合、シンボルテーブルは存在しないため、この処理はエラーを引き起こすか、無駄な処理となっていました。 - 修正後、
if f.FileHeader.NumberOfSymbols > 0 { ... }
という条件分岐が追加されました。これにより、NumberOfSymbols
が0より大きい場合にのみ、シンボルテーブルと文字列テーブルの読み込みおよび処理が行われるようになります。 - この変更により、シンボルテーブルを持たないPEファイルが渡された場合でも、
debug/pe
パッケージがクラッシュすることなく、正しくファイルを解析できるようになります。
- 以前のコードでは、
これらの変更は、PEファイルフォーマットの仕様に厳密に準拠し、より堅牢なPEファイルパーサーを構築するために不可欠です。
コアとなるコードの変更箇所
src/pkg/debug/pe/file.go
のNewFile
関数内の変更点です。
--- a/src/pkg/debug/pe/file.go
+++ b/src/pkg/debug/pe/file.go
@@ -131,12 +131,13 @@ func NewFile(r io.ReaderAt) (*File, error) {
}
var base int64
if dosheader[0] == 'M' && dosheader[1] == 'Z' {
+ signoff := int64(binary.LittleEndian.Uint32(dosheader[0x3c:]))
var sign [4]byte
- r.ReadAt(sign[0:], int64(dosheader[0x3c]))
+ r.ReadAt(sign[:], signoff)
if !(sign[0] == 'P' && sign[1] == 'E' && sign[2] == 0 && sign[3] == 0) {
return nil, errors.New("Invalid PE File Format.")
}
- base = int64(dosheader[0x3c]) + 4
+ base = signoff + 4
} else {
base = int64(0)
}
@@ -148,45 +149,48 @@ func NewFile(r io.ReaderAt) (*File, error) {
return nil, errors.New("Invalid PE File Format.")
}
- // Get COFF string table, which is located at the end of the COFF symbol table.
- 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)
- if _, err := r.ReadAt(ss, int64(f.FileHeader.PointerToSymbolTable+COFFSymbolSize*f.FileHeader.NumberOfSymbols)); err != nil {
- return nil, err
- }
-
- // Process COFF symbol table.
- sr.Seek(int64(f.FileHeader.PointerToSymbolTable), os.SEEK_SET)
- aux := uint8(0)
- for i := 0; i < int(f.FileHeader.NumberOfSymbols); i++ {\n-\t\tcs := new(COFFSymbol)\n-\t\tif err := binary.Read(sr, binary.LittleEndian, cs); err != nil {\n+\tvar ss []byte
+\tif f.FileHeader.NumberOfSymbols > 0 {
+\t\t// Get COFF string table, which is located at the end of the COFF symbol table.
+\t\tsr.Seek(int64(f.FileHeader.PointerToSymbolTable+COFFSymbolSize*f.FileHeader.NumberOfSymbols), os.SEEK_SET)
+\t\tvar l uint32
+\t\tif err := binary.Read(sr, binary.LittleEndian, &l); err != nil {
+\t\t\treturn nil, err
+\t\t}
+\t\tss = make([]byte, l)
+\t\tif _, err := r.ReadAt(ss, int64(f.FileHeader.PointerToSymbolTable+COFFSymbolSize*f.FileHeader.NumberOfSymbols)); err != nil {
+\t\t\treturn nil, err
+\t\t}
+
+\t\t// Process COFF symbol table.
+\t\tsr.Seek(int64(f.FileHeader.PointerToSymbolTable), os.SEEK_SET)
+\t\taux := uint8(0)
+\t\tfor i := 0; i < int(f.FileHeader.NumberOfSymbols); i++ {
+\t\t\tcs := new(COFFSymbol)
+\t\t\tif err := binary.Read(sr, binary.LittleEndian, cs); err != nil {
+\t\t\t\treturn nil, err
+\t\t\t}
+\t\t\tif aux > 0 {
+\t\t\t\taux--
+\t\t\t\tcontinue
+\t\t\t}
+\t\t\tvar name string
+\t\t\tif cs.Name[0] == 0 && cs.Name[1] == 0 && cs.Name[2] == 0 && cs.Name[3] == 0 {
+\t\t\t\tsi := int(binary.LittleEndian.Uint32(cs.Name[4:]))
+\t\t\t\tname, _ = getString(ss, si)
+\t\t\t} else {
+\t\t\t\tname = cstring(cs.Name[:])
+\t\t\t}
+\t\t\taux = cs.NumberOfAuxSymbols
+\t\t\ts := &Symbol{
+\t\t\t\tName: name,
+\t\t\t\tValue: cs.Value,
+\t\t\t\tSectionNumber: cs.SectionNumber,
+\t\t\t\tType: cs.Type,
+\t\t\t\tStorageClass: cs.StorageClass,
+\t\t\t}
+\t\t\tf.Symbols = append(f.Symbols, s)
+\t\t}
}
- 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 {
- si := int(binary.LittleEndian.Uint32(cs.Name[4:]))
- name, _ = getString(ss, si)
- } else {
- name = cstring(cs.Name[:])
- }
- aux = cs.NumberOfAuxSymbols
- s := &Symbol{
- Name: name,
- Value: cs.Value,
- SectionNumber: cs.SectionNumber,
- Type: cs.Type,
- StorageClass: cs.StorageClass,
- }
- f.Symbols = append(f.Symbols, s)
- }
// Process sections.
コアとなるコードの解説
-
e_lfanew
の修正:signoff := int64(binary.LittleEndian.Uint32(dosheader[0x3c:]))
dosheader[0x3c:]
は、dosheader
バイトスライスのオフセット0x3c
から最後までを指します。binary.LittleEndian.Uint32()
は、このバイトスライスの先頭4バイトをリトルエンディアンとして解釈し、uint32
型の整数に変換します。- この
uint32
値は、PEヘッダーのオフセットを示しています。 - 結果は
int64
にキャストされ、signoff
変数に格納されます。
r.ReadAt(sign[:], signoff)
- PEシグネチャ("PE\0\0")を読み取る際、以前は
int64(dosheader[0x3c])
という誤ったオフセットを使用していましたが、修正後は正確なsignoff
オフセットを使用するようになりました。
- PEシグネチャ("PE\0\0")を読み取る際、以前は
base = signoff + 4
- PEシグネチャの4バイト分をスキップし、PEヘッダーの実際の開始位置を
base
に設定します。
- PEシグネチャの4バイト分をスキップし、PEヘッダーの実際の開始位置を
-
シンボルテーブル処理の条件分岐:
var ss []byte
- COFF文字列テーブルを格納するバイトスライス
ss
が、シンボルテーブルの処理ブロックの外で宣言されるようになりました。これは、NumberOfSymbols
が0の場合にss
が初期化されないことを許容するためです。
- COFF文字列テーブルを格納するバイトスライス
if f.FileHeader.NumberOfSymbols > 0 { ... }
- この条件文が追加されたことで、
f.FileHeader.NumberOfSymbols
が0より大きい場合にのみ、以下のシンボルテーブル関連の処理が実行されるようになりました。 - COFF文字列テーブルの読み込み:
sr.Seek
とbinary.Read
による文字列テーブルのサイズl
の読み込み、およびr.ReadAt
による文字列テーブル本体ss
の読み込みがこのブロック内に移動しました。 - COFFシンボルテーブルの処理:
sr.Seek
によるシンボルテーブルの先頭へのシーク、およびfor
ループによる各COFFSymbol
の読み込みとSymbol
構造体への変換処理もこのブロック内に移動しました。
- この条件文が追加されたことで、
これらの変更により、debug/pe
パッケージは、PEファイルフォーマットの多様なバリエーション(特にシンボルテーブルの有無)に対してより堅牢に対応できるようになりました。
関連リンク
- Go Issue #4177: https://github.com/golang/go/issues/4177 (このコミットが修正したバグの詳細が記載されている可能性があります)
- Go CL 6587048: https://golang.org/cl/6587048 (このコミットに対応するGoのコードレビューリンク)
参考にした情報源リンク
- Microsoft Docs - PE Format: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format (PEファイルフォーマットの公式ドキュメント)
- Wikipedia - Portable Executable: https://en.wikipedia.org/wiki/Portable_Executable
- Wikipedia - Common Object File Format: https://en.wikipedia.org/wiki/Common_Object_File_Format
- Go言語の
encoding/binary
パッケージドキュメント: https://pkg.go.dev/encoding/binary - Go言語の
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語の
os
パッケージドキュメント: https://pkg.go.dev/os - Go言語の
debug/pe
パッケージドキュメント: https://pkg.go.dev/debug/pe (コミット当時のバージョンとは異なる可能性がありますが、一般的な情報源として)