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

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

このコミットは、Go言語のツールチェインの一部である libmach ライブラリにおける、AMD64アーキテクチャ向けのPE (Portable Executable) ファイルのハンドリングに関するバグ修正です。具体的には、PE32+ (64ビットPE) 形式の実行可能ファイルからエントリポイントアドレスとイメージベースアドレスを正しく読み取れない問題を解決します。

コミット

commit e44f42e05696de40b0ebed3c480b628219e9e036
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Wed Mar 6 15:14:07 2013 -0500

    libmach: fix amd64 pe handling
    
    Fixes #4841.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/7475046

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

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

元コミット内容

このコミットの目的は、libmach がAMD64アーキテクチャのPEファイル、特にPE32+形式の実行可能ファイルを正しく解析できるようにすることです。以前のコードでは、PE32+ヘッダの構造を適切に扱っておらず、エントリポイントやイメージベースといった重要な情報を誤って読み取っていました。これにより、GoのツールチェインがWindows上の64ビット実行可能ファイルをデバッグしたり、情報を抽出したりする際に問題が発生していました。

変更の背景

この変更は、Go Issue #4841「libmach: fix amd64 pe handling」を修正するために行われました。このIssueは、libmach がWindowsの64ビットPE (PE32+) 実行可能ファイルを解析する際に、IMAGE_OPTIONAL_HEADER の構造の違いにより、エントリポイントやイメージベースなどの重要なフィールドを正しく読み取れないというバグを報告していました。

PEファイルは、Windowsオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、DLLなどの形式です。32ビット版 (PE32) と64ビット版 (PE32+) があり、それぞれ IMAGE_OPTIONAL_HEADER の構造が異なります。特に、64ビット版では一部のフィールドのサイズが uint32 から uint64 に変更されており、オフセットもずれています。

libmach は、Goのデバッガやその他のツールが実行可能ファイルの情報を読み取るために使用されるライブラリです。このバグが存在すると、GoのツールがWindowsの64ビット実行可能ファイルを正しく扱えず、デバッグや分析に支障をきたす可能性がありました。

前提知識の解説

PE (Portable Executable) フォーマット

PEは、Microsoft Windowsオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、DLL、FONドライバなどのファイル形式です。COFF (Common Object File Format) のデータ構造に基づいています。PEファイルは、ヘッダ、セクション、データディレクトリなどから構成され、プログラムの実行に必要な情報(コード、データ、リソース、インポート/エクスポート関数など)を含んでいます。

IMAGE_OPTIONAL_HEADERPE64_IMAGE_OPTIONAL_HEADER

PEファイルのヘッダ構造の一部に IMAGE_OPTIONAL_HEADER があります。このヘッダには、実行可能ファイルのロードアドレス、エントリポイント、セクションアライメント、ファイルアライメントなどの重要な情報が含まれています。

  • PE32 (32ビットPE): IMAGE_OPTIONAL_HEADER 構造体を使用します。この構造体内のアドレスやサイズに関するフィールドは32ビット (uint32) です。
  • PE32+ (64ビットPE): IMAGE_OPTIONAL_HEADER の拡張版であり、64ビットアーキテクチャに対応するために一部のフィールドが64ビット (uint64) に拡張されています。このコミットでは、この64ビット版のヘッダを PE64_IMAGE_OPTIONAL_HEADER として定義し、区別しています。特に ImageBaseSizeOfStackReserve などが uint64 になっています。

libmach

libmach は、Go言語のツールチェインの一部であり、様々なアーキテクチャの実行可能ファイル(Mach-O, ELF, PEなど)を解析するためのライブラリです。デバッガやプロファイラなどのツールが、実行可能ファイルからシンボル情報、セクション情報、エントリポイントなどを抽出するために利用します。

Magic ナンバー

IMAGE_OPTIONAL_HEADER の先頭には Magic フィールドがあり、これがPEファイルのタイプ(PE32かPE32+か)を示します。

  • 0x10b: PE32 (32ビット実行可能ファイル)
  • 0x20b: PE32+ (64ビット実行可能ファイル)

このマジックナンバーを読み取ることで、後続のヘッダ構造をどのように解釈すべきかを判断します。

技術的詳細

このコミットの核心は、PE32とPE32+の IMAGE_OPTIONAL_HEADER の構造の違いを適切に処理することです。

元のコードでは、PE32+ファイルであっても IMAGE_OPTIONAL_HEADER (32ビット構造) を読み込もうとしていました。しかし、PE32+の IMAGE_OPTIONAL_HEADER は、ImageBase やスタック/ヒープ関連のフィールドが64ビットに拡張されており、全体のサイズも異なります。このため、32ビット構造で読み込むと、フィールドのオフセットがずれ、特に ImageBaseAddressOfEntryPoint といった重要な値が誤って解釈されていました。

修正では、以下のステップが導入されました。

  1. PE64_IMAGE_OPTIONAL_HEADER 構造体の導入: 64ビットPEファイルのオプションヘッダの正確な構造を反映するために、新しい構造体 PE64_IMAGE_OPTIONAL_HEADER が定義されました。これにより、64ビットフィールドが正しくパースできるようになります。
  2. マジックナンバーによるヘッダタイプの判別: IMAGE_OPTIONAL_HEADERMagic フィールドを読み取り、0x10b (PE32) または 0x20b (PE32+) に基づいて、適切なヘッダ構造を読み込むように分岐します。
  3. PE32+の場合の再読み込み: Magic0x20b (PE32+) の場合、ファイルポインタをオプションヘッダの開始位置に戻し、新たに定義した PE64_IMAGE_OPTIONAL_HEADER 構造体を使ってヘッダを再読み込みします。これにより、64ビットフィールドを含む正しい構造でデータが取得されます。
  4. エントリポイントとイメージベースの取得: 各ヘッダタイプに応じて、正しい構造体 (oh または oh64) から ImageBaseAddressOfEntryPoint を取得し、それぞれ ibentry 変数に格納します。
  5. settext 関数の引数変更: settext 関数に渡すエントリポイントアドレスが、常に正しい entry 変数から取得されるように変更されました。

この修正により、libmach はPE32とPE32+の両方の形式の実行可能ファイルから、エントリポイントやイメージベースなどの情報を正確に抽出できるようになり、Windows上の64ビットGoプログラムのデバッグや分析が正しく行えるようになりました。

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

diff --git a/src/libmach/executable.c b/src/libmach/executable.c
index a93a8c268c..221e56cdef 100644
--- a/src/libmach/executable.c
+++ b/src/libmach/executable.c
@@ -1335,13 +1335,45 @@ typedef struct {
 	IMAGE_DATA_DIRECTORY DataDirectory[16];
 } IMAGE_OPTIONAL_HEADER;
 
+typedef struct {
+	uint16 Magic;
+	uint8  MajorLinkerVersion;
+	uint8  MinorLinkerVersion;
+	uint32 SizeOfCode;
+	uint32 SizeOfInitializedData;
+	uint32 SizeOfUninitializedData;
+	uint32 AddressOfEntryPoint;
+	uint32 BaseOfCode;
+	uint64 ImageBase;
+	uint32 SectionAlignment;
+	uint32 FileAlignment;
+	uint16 MajorOperatingSystemVersion;
+	uint16 MinorOperatingSystemVersion;
+	uint16 MajorImageVersion;
+	uint16 MinorImageVersion;
+	uint16 MajorSubsystemVersion;
+	uint16 MinorSubsystemVersion;
+	uint32 Win32VersionValue;
+	uint32 SizeOfImage;
+	uint32 SizeOfHeaders;
+	uint32 CheckSum;
+	uint16 Subsystem;
+	uint16 DllCharacteristics;
+	uint64 SizeOfStackReserve;
+	uint64 SizeOfStackCommit;
+	uint64 SizeOfHeapReserve;
+	uint64 SizeOfHeapCommit;
+	uint32 LoaderFlags;
+	uint32 NumberOfRvaAndSizes;
+	IMAGE_DATA_DIRECTORY DataDirectory[16];
+} PE64_IMAGE_OPTIONAL_HEADER;
+
 static int
 match8(void *buf, char *cmp)
 {
 	return strncmp((char*)buf, cmp, 8) == 0;
 }
 
-/* TODO(czaplinski): 64b windows? */
 /*
  * Read from Windows PE/COFF .exe file image.
  */
@@ -1353,9 +1385,10 @@ pedotout(int fd, Fhdr *fp, ExecHdr *hp)
 	IMAGE_FILE_HEADER fh;\n 	IMAGE_SECTION_HEADER sh;\n 	IMAGE_OPTIONAL_HEADER oh;\n+\tPE64_IMAGE_OPTIONAL_HEADER oh64;\n 	uint8 sym[18];\n-\tuint32 *valp, ib;\n-\tint i;\n+\tuint32 *valp, ib, entry;\n+\tint i, ohoffset;\n \n 	USED(hp);\n 	seek(fd, 0x3c, 0);\
@@ -1384,6 +1417,7 @@ pedotout(int fd, Fhdr *fp, ExecHdr *hp)\n \t\treturn 0;\n \t}\n \n+\tohoffset = seek(fd, 0, 1);\n \tif (readn(fd, &oh, sizeof(oh)) != sizeof(oh)) {\n \t\twerrstr(\"crippled PE Optional Header\");\n \t\treturn 0;\
@@ -1392,17 +1426,24 @@ pedotout(int fd, Fhdr *fp, ExecHdr *hp)\n \tswitch(oh.Magic) {\n \tcase 0x10b:\t// PE32\n \t\tfp->type = FI386;\n+\t\tib = leswal(oh.ImageBase);\n+\t\tentry = leswal(oh.AddressOfEntryPoint);\n \t\tbreak;\n \tcase 0x20b:\t// PE32+\n \t\tfp->type = FAMD64;\n+\t\tseek(fd, ohoffset, 0);\n+\t\tif (readn(fd, &oh64, sizeof(oh64)) != sizeof(oh64)) {\n+\t\t\twerrstr(\"crippled PE32+ Optional Header\");\n+\t\t\treturn 0;\n+\t\t}\n+\t\tib = leswal(oh64.ImageBase);\n+\t\tentry = leswal(oh64.AddressOfEntryPoint);\n \t\tbreak;\n \tdefault:\n-\t\twerrstr(\"invalid PE Optional magic number\");\n+\t\twerrstr(\"invalid PE Optional Header magic number\");\n \t\treturn 0;\n \t}\n \n-\tib=leswal(oh.ImageBase);\n-\tseek(fd, start+sizeof(magic)+sizeof(fh)+leswab(fh.SizeOfOptionalHeader), 0);\n \tfp->txtaddr = 0;\n \tfp->dataddr = 0;\n \tfor (i=0; i<leswab(fh.NumberOfSections); i++) {\
@@ -1411,7 +1452,7 @@ pedotout(int fd, Fhdr *fp, ExecHdr *hp)\n \t\t\treturn 0;\n \t\t}\n \t\tif (match8(sh.Name, \".text\"))\n-\t\t\tsettext(fp, ib+leswal(oh.AddressOfEntryPoint), ib+leswal(sh.VirtualAddress), leswal(sh.VirtualSize), leswal(sh.PointerToRawData));\n+\t\t\tsettext(fp, ib+entry, ib+leswal(sh.VirtualAddress), leswal(sh.VirtualSize), leswal(sh.PointerToRawData));\n \t\tif (match8(sh.Name, \".data\"))\n \t\t\tsetdata(fp, ib+leswal(sh.VirtualAddress), leswal(sh.SizeOfRawData), leswal(sh.PointerToRawData), leswal(sh.VirtualSize)-leswal(sh.SizeOfRawData));\n \t}\

コアとなるコードの解説

1. PE64_IMAGE_OPTIONAL_HEADER 構造体の追加

+typedef struct {
+	uint16 Magic;
+	uint8  MajorLinkerVersion;
+	uint8  MinorLinkerVersion;
+	uint32 SizeOfCode;
+	uint32 SizeOfInitializedData;
+	uint32 SizeOfUninitializedData;
+	uint32 AddressOfEntryPoint;
+	uint32 BaseOfCode;
+	uint64 ImageBase; // <-- 32ビット版では uint32
+	uint32 SectionAlignment;
+	uint32 FileAlignment;
+	uint16 MajorOperatingSystemVersion;
+	uint16 MinorOperatingSystemVersion;
+	uint16 MajorImageVersion;
+	uint16 MinorImageVersion;
+	uint16 MajorSubsystemVersion;
+	uint16 MinorSubsystemVersion;
+	uint32 Win32VersionValue;
+	uint32 SizeOfImage;
+	uint32 SizeOfHeaders;
+	uint32 CheckSum;
+	uint16 Subsystem;
+	uint16 DllCharacteristics;
+	uint64 SizeOfStackReserve; // <-- 32ビット版では uint32
+	uint64 SizeOfStackCommit;  // <-- 32ビット版では uint32
+	uint64 SizeOfHeapReserve;  // <-- 32ビット版では uint32
+	uint64 SizeOfHeapCommit;   // <-- 32ビット版では uint32
+	uint32 LoaderFlags;
+	uint32 NumberOfRvaAndSizes;
+	IMAGE_DATA_DIRECTORY DataDirectory[16];
+} PE64_IMAGE_OPTIONAL_HEADER;

この変更は、64ビットPEファイルのオプションヘッダの正確なレイアウトを定義するために不可欠です。特に ImageBase やスタック/ヒープ関連のフィールドが uint64 になっている点が IMAGE_OPTIONAL_HEADER (32ビット版) と異なります。これにより、ファイルからこれらの値を読み込む際に、正しいバイト数を読み込み、正しいオフセットでアクセスできるようになります。

2. 変数の追加

 	IMAGE_OPTIONAL_HEADER oh;
+	PE64_IMAGE_OPTIONAL_HEADER oh64;
 	uint8 sym[18];
-\tuint32 *valp, ib;\n-\tint i;\n+\tuint32 *valp, ib, entry;\n+\tint i, ohoffset;\

oh64 はPE32+ヘッダを読み込むための新しい変数です。 entry はエントリポイントアドレスを格納するための変数で、PE32とPE32+の両方で共通して使用されます。 ohoffset は、オプションヘッダの開始位置を記録するための変数です。これは、PE32+の場合にヘッダを再読み込みするために必要となります。

3. オプションヘッダの読み込みとマジックナンバーによる分岐

+\tohoffset = seek(fd, 0, 1);\
 \tif (readn(fd, &oh, sizeof(oh)) != sizeof(oh)) {\
 \t\twerrstr(\"crippled PE Optional Header\");\
 \t\treturn 0;\
 \t}\
 \tswitch(oh.Magic) {\
 \tcase 0x10b:\t// PE32\
 \t\tfp->type = FI386;\
+\t\tib = leswal(oh.ImageBase);\
+\t\tentry = leswal(oh.AddressOfEntryPoint);\
 \t\tbreak;\
 \tcase 0x20b:\t// PE32+\
 \t\tfp->type = FAMD64;\
+\t\tseek(fd, ohoffset, 0);\
+\t\tif (readn(fd, &oh64, sizeof(oh64)) != sizeof(oh64)) {\
+\t\t\twerrstr(\"crippled PE32+ Optional Header\");\
+\t\t\treturn 0;\
+\t\t}\
+\t\tib = leswal(oh64.ImageBase);\
+\t\tentry = leswal(oh64.AddressOfEntryPoint);\
 \t\tbreak;\
 \tdefault:\
-\t\twerrstr(\"invalid PE Optional magic number\");\
+\t\twerrstr(\"invalid PE Optional Header magic number\");\
 \t\treturn 0;\
 \t}\
  • ohoffset = seek(fd, 0, 1); で、現在のファイルポインタ(オプションヘッダの開始位置)を ohoffset に保存します。これは、PE32+の場合にヘッダを再読み込みするために使用されます。
  • まず、一般的な IMAGE_OPTIONAL_HEADER (32ビット版) のサイズでデータを読み込み、その中の Magic フィールドをチェックします。
  • switch 文で oh.Magic の値に基づいて処理を分岐します。
    • 0x10b (PE32) の場合: fp->typeFI386 に設定し、oh から ImageBaseAddressOfEntryPoint を取得します。
    • 0x20b (PE32+) の場合: fp->typeFAMD64 に設定します。ここで重要なのは、ファイルポインタを ohoffset に戻し、PE64_IMAGE_OPTIONAL_HEADER のサイズで oh64 にデータを再読み込みすることです。これにより、64ビットPEヘッダの正しい構造でデータがパースされます。その後、oh64 から ImageBaseAddressOfEntryPoint を取得します。
    • その他のマジックナンバーの場合: エラーを報告して処理を終了します。

4. 不要なコードの削除

-\tib=leswal(oh.ImageBase);\
-\tseek(fd, start+sizeof(magic)+sizeof(fh)+leswab(fh.SizeOfOptionalHeader), 0);\

この行は、PE32+のケースで ImageBase を誤って取得していた部分と、セクションヘッダの開始位置へのシークが重複していた部分を削除しています。新しいロジックでは、switch 文内で ibentry が適切に設定されるため、この行は不要になります。

5. settext 関数の引数変更

 \t\tif (match8(sh.Name, \".text\"))\
-\t\t\tsettext(fp, ib+leswal(oh.AddressOfEntryPoint), ib+leswal(sh.VirtualAddress), leswal(sh.VirtualSize), leswal(sh.PointerToRawData));\
+\t\t\tsettext(fp, ib+entry, ib+leswal(sh.VirtualAddress), leswal(sh.VirtualSize), leswal(sh.PointerToRawData));\

settext 関数に渡すエントリポイントアドレスが、常に正しく取得された entry 変数から取得されるように変更されました。これにより、PE32とPE32+のどちらのファイル形式でも、正しいエントリポイントが settext に渡されることが保証されます。

これらの変更により、libmach はWindowsの32ビットおよび64ビットPE実行可能ファイルを正確に解析し、Goのツールチェインがこれらのファイルを正しく扱えるようになりました。

関連リンク

参考にした情報源リンク