[インデックス 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_HEADER
と PE64_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
として定義し、区別しています。特にImageBase
やSizeOfStackReserve
などが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ビット構造で読み込むと、フィールドのオフセットがずれ、特に ImageBase
や AddressOfEntryPoint
といった重要な値が誤って解釈されていました。
修正では、以下のステップが導入されました。
PE64_IMAGE_OPTIONAL_HEADER
構造体の導入: 64ビットPEファイルのオプションヘッダの正確な構造を反映するために、新しい構造体PE64_IMAGE_OPTIONAL_HEADER
が定義されました。これにより、64ビットフィールドが正しくパースできるようになります。- マジックナンバーによるヘッダタイプの判別:
IMAGE_OPTIONAL_HEADER
のMagic
フィールドを読み取り、0x10b
(PE32) または0x20b
(PE32+) に基づいて、適切なヘッダ構造を読み込むように分岐します。 - PE32+の場合の再読み込み:
Magic
が0x20b
(PE32+) の場合、ファイルポインタをオプションヘッダの開始位置に戻し、新たに定義したPE64_IMAGE_OPTIONAL_HEADER
構造体を使ってヘッダを再読み込みします。これにより、64ビットフィールドを含む正しい構造でデータが取得されます。 - エントリポイントとイメージベースの取得: 各ヘッダタイプに応じて、正しい構造体 (
oh
またはoh64
) からImageBase
とAddressOfEntryPoint
を取得し、それぞれib
とentry
変数に格納します。 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->type
をFI386
に設定し、oh
からImageBase
とAddressOfEntryPoint
を取得します。0x20b
(PE32+) の場合:fp->type
をFAMD64
に設定します。ここで重要なのは、ファイルポインタをohoffset
に戻し、PE64_IMAGE_OPTIONAL_HEADER
のサイズでoh64
にデータを再読み込みすることです。これにより、64ビットPEヘッダの正しい構造でデータがパースされます。その後、oh64
からImageBase
とAddressOfEntryPoint
を取得します。- その他のマジックナンバーの場合: エラーを報告して処理を終了します。
4. 不要なコードの削除
-\tib=leswal(oh.ImageBase);\
-\tseek(fd, start+sizeof(magic)+sizeof(fh)+leswab(fh.SizeOfOptionalHeader), 0);\
この行は、PE32+のケースで ImageBase
を誤って取得していた部分と、セクションヘッダの開始位置へのシークが重複していた部分を削除しています。新しいロジックでは、switch
文内で ib
と entry
が適切に設定されるため、この行は不要になります。
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のツールチェインがこれらのファイルを正しく扱えるようになりました。