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

[インデックス 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つの問題に対処しています。

  1. シンボルテーブルを持たないPEファイルのサポート: 以前のdebug/peパッケージの実装では、PEファイルが常にシンボルテーブルを持つことを前提としていました。しかし、すべてのPEファイルがシンボルテーブルを持つわけではありません。特に、デバッグ情報が不要な場合や、サイズを最小限に抑えたい場合には、シンボルテーブルが省略されることがあります。このようなファイルに対して、既存のパーサーがエラーを発生させるか、正しく解析できない問題がありました。この変更により、IMAGE_FILE_HEADERNumberOfSymbolsフィールドが0であるPEファイルも正しく処理できるようになります。

  2. 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つあります。

  1. 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ヘッダーの解析を開始するようになります。
  2. シンボルテーブルの存在チェックの追加:

    • 以前のコードでは、f.FileHeader.PointerToSymbolTablef.FileHeader.NumberOfSymbolsを使用して、無条件にCOFFシンボルテーブルと文字列テーブルを読み込もうとしていました。NumberOfSymbolsが0の場合、シンボルテーブルは存在しないため、この処理はエラーを引き起こすか、無駄な処理となっていました。
    • 修正後、if f.FileHeader.NumberOfSymbols > 0 { ... }という条件分岐が追加されました。これにより、NumberOfSymbolsが0より大きい場合にのみ、シンボルテーブルと文字列テーブルの読み込みおよび処理が行われるようになります。
    • この変更により、シンボルテーブルを持たないPEファイルが渡された場合でも、debug/peパッケージがクラッシュすることなく、正しくファイルを解析できるようになります。

これらの変更は、PEファイルフォーマットの仕様に厳密に準拠し、より堅牢なPEファイルパーサーを構築するために不可欠です。

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

src/pkg/debug/pe/file.goNewFile関数内の変更点です。

--- 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.

コアとなるコードの解説

  1. 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オフセットを使用するようになりました。
    • base = signoff + 4
      • PEシグネチャの4バイト分をスキップし、PEヘッダーの実際の開始位置をbaseに設定します。
  2. シンボルテーブル処理の条件分岐:

    • var ss []byte
      • COFF文字列テーブルを格納するバイトスライスssが、シンボルテーブルの処理ブロックの外で宣言されるようになりました。これは、NumberOfSymbolsが0の場合にssが初期化されないことを許容するためです。
    • if f.FileHeader.NumberOfSymbols > 0 { ... }
      • この条件文が追加されたことで、f.FileHeader.NumberOfSymbolsが0より大きい場合にのみ、以下のシンボルテーブル関連の処理が実行されるようになりました。
      • COFF文字列テーブルの読み込み: sr.Seekbinary.Readによる文字列テーブルのサイズlの読み込み、およびr.ReadAtによる文字列テーブル本体ssの読み込みがこのブロック内に移動しました。
      • COFFシンボルテーブルの処理: sr.Seekによるシンボルテーブルの先頭へのシーク、およびforループによる各COFFSymbolの読み込みとSymbol構造体への変換処理もこのブロック内に移動しました。

これらの変更により、debug/peパッケージは、PEファイルフォーマットの多様なバリエーション(特にシンボルテーブルの有無)に対してより堅牢に対応できるようになりました。

関連リンク

参考にした情報源リンク