「Any CPU なのに x86 と判定される」――PE の Machine フィールドだけを見ていると必ずぶつかる壁です。本記事では、BinaryReader だけで CLR ヘッダーの CorFlags を読み取り、x86 / x64 / Itanium / ARM、そして Any CPU(Prefer 32 bit を含む)を誤判定なしで見分けるための実装と設計上の注意点を、動作原理からコード、検証手順まで徹底解説します。
.NET 実行ファイル(PE)からターゲット CPU を正確に判定するには
Visual Studio で「Any CPU」を選んでビルドした .NET アセンブリの多くは、COFF ヘッダーの Machine が 0x14C(I386)になります。ここだけを読んで「x86」と決め打ちすると、Any CPU を誤って x86 と判定してしまいます。正解は、CLR(.NET)ヘッダーにある CorFlags を読むことです。
結論:読むべきは CorFlags
CorFlags の主要ビット(ILONLY 0x1、32BITREQUIRED 0x2、32BITPREFERRED 0x20000)と Machine をあわせて判定すれば、リフレクションも CorFlags.exe も使わず BinaryReader だけで確実に見分けられます。
| 判定フラグ | 意味 | 判定結果 |
|---|---|---|
COMIMAGE_FLAGS_32BITREQUIRED (0x2) が 1 | 32 bit 固定 | x86 |
COMIMAGE_FLAGS_32BITPREFERRED (0x20000) が 1かつ 32BITREQUIRED が 0 | 32 bit 起動を優先(64 bit OS でも 32 bit で動く) | Any CPU(Prefer 32 bit) |
上記 2 つが 0 で ILONLY (0x1) が 1 | 完全なプロセッサ中立 | Any CPU |
Machine が 0x8664(AMD64)で 32 bit 関連フラグが 0 | 64 bit 固定 | x64 |
| それ以外 | 判定材料不足/非マネージ | Unknown |
なぜ Machine=I386 なのに Any CPU なのか
.NET の管理対象 PE は、歴史的事情から COFF ヘッダーの Machine を I386(0x14C)として埋めることがよくあります。しかし実際に 32/64 bit どちらとしてロードするかは、CLR ヘッダーの CorFlags と OS のビット数で決まります。つまり Machine だけでは不十分で、CorFlags が真の答えです。
アルゴリズム(概要)
- MZ 署名を確認し、
e_lfanew (0x3C)から PE ヘッダーのオフセットを得る。 - 「
PE\0\0」を読み飛ばし、Machine を控える。 - Optional Header の Data Directory[14](CLR Runtime Header)の RVA とサイズを読む。
- セクションテーブルを走査し、RVA → ファイルオフセットを変換する。
- CLR ヘッダーの +16 バイトにある
CorFlagsを読み、上表のロジックで列挙値を返す。
VB.NET 実装(核心部)
' ターゲット CPU の列挙
Public Enum CpuType
Unknown
x86
x64
AnyCPU ' 32/64 自動
AnyCPU_Prefer32 ' AnyCPU だが 32bit 起動を優先
End Enum
' Public API:パスから判定
Public Shared Function GetCpuType(path As String) As CpuType
Using fs As New FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read),
br As New BinaryReader(fs)
Return ReadCpuType(br, fs)
End Using
End Function
' 実体:BinaryReader/Stream から解析
Private Shared Function ReadCpuType(br As BinaryReader, fs As Stream) As CpuType
' --- DOS Header ---
fs.Seek(&H0, SeekOrigin.Begin)
If br.ReadUInt16() <> &H5A4DUS Then Return CpuType.Unknown ' "MZ"
fs.Seek(&H3C, SeekOrigin.Begin)
Dim peOffset As Integer = br.ReadInt32()
' --- PE Signature ---
fs.Seek(peOffset, SeekOrigin.Begin)
If br.ReadUInt32() <> &H4550UI Then Return CpuType.Unknown ' "PE\0\0"
' --- COFF Header ---
Dim machine As UShort = br.ReadUInt16()
Dim numberOfSections As UShort = br.ReadUInt16()
fs.Seek(12, SeekOrigin.Current) ' TimeDateStamp(4) + PointerToSymbolTable(4) + NumberOfSymbols(4)
Dim sizeOfOptionalHeader As UShort = br.ReadUInt16()
fs.Seek(2, SeekOrigin.Current) ' Characteristics
' --- Optional Header ---
Dim optStart As Long = fs.Position
Dim magic As UShort = br.ReadUInt16()
Dim isPE32Plus As Boolean = (magic = &H20BUS)
' DataDirectory の先頭へ(PE32: 0x60, PE32+: 0x70)
Dim dataDirStart As Long = optStart + If(isPE32Plus, &H70, &H60)
fs.Seek(dataDirStart + 8L * 14, SeekOrigin.Begin) ' [14] CLR Runtime Header
Dim clrRva As UInteger = br.ReadUInt32()
Dim clrSize As UInteger = br.ReadUInt32()
If clrRva = 0UI OrElse clrSize = 0UI Then Return CpuType.Unknown ' 非マネージ or apphost
' --- Section Table ---
Dim sectionTable As Long = optStart + sizeOfOptionalHeader
fs.Seek(sectionTable, SeekOrigin.Begin)
Dim clrFileOffset As Long = -1
For i = 0 To numberOfSections - 1
fs.Seek(8, SeekOrigin.Current) ' Name
Dim virtualSize As UInteger = br.ReadUInt32()
Dim virtualAddress As UInteger = br.ReadUInt32()
Dim sizeOfRawData As UInteger = br.ReadUInt32()
Dim pointerToRawData As UInteger = br.ReadUInt32()
fs.Seek(16, SeekOrigin.Current) ' 残りのヘッダー
Dim span As UInteger = If(virtualSize = 0UI, sizeOfRawData, Math.Max(virtualSize, sizeOfRawData))
If clrRva >= virtualAddress AndAlso clrRva < virtualAddress + span Then
clrFileOffset = CLng(pointerToRawData) + (CLng(clrRva) - CLng(virtualAddress))
Exit For
End If
Next
If clrFileOffset < 0 Then Return CpuType.Unknown
' --- CLR Header ---
fs.Seek(clrFileOffset + 16, SeekOrigin.Begin) ' Flags まで 16 バイト
Dim flags As UInteger = br.ReadUInt32()
' 判定(優先順位が重要)
Dim f32req As Boolean = (flags And &H2UI) <> 0UI
Dim f32pref As Boolean = (flags And &H20000UI) <> 0UI
Dim ilonly As Boolean = (flags And &H1UI) <> 0UI
If f32pref AndAlso Not f32req Then Return CpuType.AnyCPU_Prefer32
If f32req Then Return CpuType.x86
If machine = &H8664US Then Return CpuType.x64 ' AMD64
If ilonly Then Return CpuType.AnyCPU
Return CpuType.Unknown
End Function
C# 実装(そのまま貼って動くサンプル)
using System;
using System.IO;
public enum CpuType
{
Unknown,
x86,
x64,
AnyCPU,
AnyCPU_Prefer32
}
public static class PeCpuDetector
{
public static CpuType GetCpuType(string path)
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs);
return ReadCpuType(br, fs);
}
private static CpuType ReadCpuType(BinaryReader br, Stream fs)
{
// DOS Header
fs.Seek(0, SeekOrigin.Begin);
if (br.ReadUInt16() != 0x5A4D) return CpuType.Unknown; // "MZ"
fs.Seek(0x3C, SeekOrigin.Begin);
int peOffset = br.ReadInt32();
// PE Signature
fs.Seek(peOffset, SeekOrigin.Begin);
if (br.ReadUInt32() != 0x00004550) return CpuType.Unknown; // "PE\0\0"
// COFF Header
ushort machine = br.ReadUInt16();
ushort numberOfSections = br.ReadUInt16();
fs.Seek(12, SeekOrigin.Current);
ushort sizeOfOptionalHeader = br.ReadUInt16();
fs.Seek(2, SeekOrigin.Current);
// Optional Header
long optStart = fs.Position;
ushort magic = br.ReadUInt16();
bool isPE32Plus = (magic == 0x20B);
long dataDirStart = optStart + (isPE32Plus ? 0x70 : 0x60);
fs.Seek(dataDirStart + 8L * 14, SeekOrigin.Begin); // [14]
uint clrRva = br.ReadUInt32();
uint clrSize = br.ReadUInt32();
if (clrRva == 0 || clrSize == 0) return CpuType.Unknown;
// Section Table
long sectionTable = optStart + sizeOfOptionalHeader;
fs.Seek(sectionTable, SeekOrigin.Begin);
long clrFileOffset = -1;
for (int i = 0; i < numberOfSections; i++)
{
fs.Seek(8, SeekOrigin.Current); // Name
uint virtualSize = br.ReadUInt32();
uint virtualAddress = br.ReadUInt32();
uint sizeOfRawData = br.ReadUInt32();
uint pointerToRawData = br.ReadUInt32();
fs.Seek(16, SeekOrigin.Current);
uint span = virtualSize == 0 ? sizeOfRawData : Math.Max(virtualSize, sizeOfRawData);
if (clrRva >= virtualAddress && clrRva < virtualAddress + span)
{
clrFileOffset = pointerToRawData + (clrRva - virtualAddress);
break;
}
}
if (clrFileOffset < 0) return CpuType.Unknown;
// CLR Header
fs.Seek(clrFileOffset + 16, SeekOrigin.Begin);
uint flags = br.ReadUInt32();
bool f32req = (flags & 0x2) != 0;
bool f32pref = (flags & 0x20000) != 0;
bool ilonly = (flags & 0x1) != 0;
if (f32pref && !f32req) return CpuType.AnyCPU_Prefer32;
if (f32req) return CpuType.x86;
if (machine == 0x8664) return CpuType.x64; // AMD64
if (ilonly) return CpuType.AnyCPU;
return CpuType.Unknown;
}
}
プラットフォーム設定とフラグの関係
実際の Visual Studio 設定と CorFlags の関係を確認しておくと、デバッグ時の混乱が減ります。
| プロジェクト種別 | プラットフォーム ターゲット | Prefer 32 bit | CorFlags | 判定結果 |
|---|---|---|---|---|
| .exe | Any CPU | オフ | ILONLY=1, 32BITREQUIRED=0, 32BITPREFERRED=0 | Any CPU |
| .exe | Any CPU | オン | ILONLY=1, 32BITREQUIRED=0, 32BITPREFERRED=1 | Any CPU(Prefer 32 bit) |
| .exe / .dll | x86 | — | 32BITREQUIRED=1 | x86 |
| .exe / .dll | x64 | — | 32BITREQUIRED=0(Machine=0x8664) | x64 |
| .dll | Any CPU | — | ILONLY=1, 32BITREQUIRED=0, 32BITPREFERRED=0 | Any CPU |
ARM・Itanium などの拡張
Machine フィールドも補助的に使えば、ARM 系も検出可能です。必要に応じて列挙を増やせば OK です。
| Machine | CPU | 備考 |
|---|---|---|
0x14C | I386 | Any CPU でもこの値になることがある(要 CorFlags) |
0x8664 | AMD64 | x64 固定の指標 |
0x1C0 | ARM | ARM32 |
0xAA64 | ARM64 | Windows on ARM など |
0x200 | IA64 | Itanium(歴史的) |
堅牢化のポイント(失敗しない読み方)
- DataDirectory[14] の RVA が 0なら「CLR ヘッダーがない」=非マネージ/apphost の可能性。無理に判定しない。
- RVA→ファイルオフセット変換は、VirtualSize と SizeOfRawData の大きい方を使用して範囲判定する(ゼロ埋めやアライメントの揺れに強くなる)。
- PE32 と PE32+(32/64)の違いで DataDirectory のオフセットが 0x60 / 0x70に変わる点を間違えない。
- 例外は Unknown を返すなど、フェイルセーフに倒す。
- ストリームは Read-Only + Share.Read で開き、ロック競合を避ける。
.NET Framework と .NET(Core/5+)での注意
.NET Core 3.0 以降の self-contained / apphost では、配布される .exe が ネイティブホスト(非マネージ)になる場合があります。この .exe には CLR ヘッダーが無い(DataDirectory[14]==0)ため、上記ロジックは Unknown を返します。正しい判定をするには次のように対応します。
- フレームワーク依存(framework-dependent):エントリは
.dll。dotnet MyApp.dllで起動するタイプ。.dllに対して本ロジックを適用する。 - self-contained:
MyApp.exeは apphost(ネイティブ)。配布物内の*.dll(マネージ)へ本ロジックを適用する。 - Single-file:.NET 5+ の単一ファイルはバンドル内に IL を格納。実行ファイル単体では解析困難。展開後のファイルや 省略不可のメタデータにアクセスできる場合だけ判定する。
- ReadyToRun(R2R):R2R では ILONLY が 0 になることがあるため、32BITREQUIRED/32BITPREFERRED と Machine の組み合わせを優先し、ILONLY は補助的に扱う。
CLI ツールとの突き合わせ(任意)
Visual Studio の Developer Command Prompt から CorFlags <file> を実行すると、コードで読んでいるのと同じフラグを確認できます。数値の一致をチェックすれば、実装の妥当性を自動テストに落としこめます。
ユニットテスト例(C# / xUnit)
using Xunit;
public class PeCpuDetectorTests
{
[Theory]
[InlineData("bin\AnyCpu.exe", CpuType.AnyCPU)]
[InlineData("bin\AnyCpuPrefer32.exe", CpuType.AnyCPU_Prefer32)]
[InlineData("bin\x86.exe", CpuType.x86)]
[InlineData("bin\x64.exe", CpuType.x64)]
public void DetectsCpuType(string path, CpuType expected)
{
var actual = PeCpuDetector.GetCpuType(path);
Assert.Equal(expected, actual);
}
}
よくある落とし穴と対処
- Machine=I386 を見て x86 判定してしまう:必ず CorFlags を参照する。順序は「Prefer32 → 32bitRequired → Machine(AMD64) → ILONLY」。
- RVA→ファイルオフセットの計算ミス:セクションの範囲判定に VirtualSize と SizeOfRawData の最大値を使う。
- シングルファイル発行:展開前の単一 exe にマネージの CLR ヘッダーが無いことがある。配布ポリシーに応じて 展開後の対象を判定する。
- R2R で ILONLY=0:ILONLY は「純 IL だけか」の指標。Any CPU 判定は 32BITREQUIRED/32BITPREFERRED を優先。
- ARM64 Windows:Machine=0xAA64 の x64 エミュレーションに惑わされない。マネージの CorFlags は同じロジックで解釈できる。
拡張:よりリッチな戻り値設計
UI 表示や診断に便利なよう、列挙だけでなく詳細構造体で返すと後処理が楽です。
public record CpuDetection(
CpuType Type,
ushort Machine,
uint CorFlags,
bool IsManaged,
bool IsPrefer32,
bool Is32BitRequired,
bool IsILOnly);
public static CpuDetection GetCpuInfo(string path)
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs);
// ...前掲の読み取り...
// machine, flags, managed などを取得したと仮定
bool isManaged = clrRva != 0 && clrSize != 0;
bool f32req = (flags & 0x2) != 0;
bool f32pref = (flags & 0x20000) != 0;
bool ilonly = (flags & 0x1) != 0;
CpuType type = f32pref && !f32req ? CpuType.AnyCPU_Prefer32
: f32req ? CpuType.x86
: machine == 0x8664 ? CpuType.x64
: ilonly ? CpuType.AnyCPU
: CpuType.Unknown;
return new CpuDetection(type, machine, flags, isManaged, f32pref, f32req, ilonly);
}
パフォーマンスのヒント
- 必要最小の読み込み:ヘッダーと対象セクションのみ。数 KB で済む。
- 並列判定:大量スキャンでは
Parallel.ForEach。ただしファイル I/O 帯域がボトルネックなので、度を越した並列化は逆効果。 - ストリーム再利用:外部から
Streamを受け取るオーバーロードを用意すると、アーカイブ内やネットワーク越しの判定に流用できる。
実運用の指針
- 「何としても誤判定しない」:情報が足りなければ Unknown を返す。
- 表示文言:ユーザーには Any CPU(Prefer 32 bit) を明示し、OS により 32/64 が切り替わることを併記。
- ロギング:Machine、CorFlags、生の値も記録しておくと、後日の解析が速い。
まとめ
PE の Machine だけでは .NET アセンブリのターゲット CPU を正しく判定できません。CLR ヘッダーの CorFlags を読み、Prefer 32 → 32bitRequired → Machine(AMD64) → ILONLY の優先順位で解釈すれば、Any CPU を含め誤判定なく分類できます。BinaryReader だけで完結し、外部ツールも不要。ARM/ARM64 にも拡張可能で、.NET Core の apphost/Single-file といった近年の配布形態にも対応策があります。この記事のコードと運用指針を組み込めば、ビルド成果物の自動振り分け・資産棚卸・配布検証など、現場の自動化品質が一段と上がります。
付録:最小 VB 実装(短縮版/学習向け)
Public Shared Function GetCpuTypeLite(path As String) As CpuType
Using fs As New FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read),
br As New BinaryReader(fs)
fs.Seek(&H3C, SeekOrigin.Begin)
Dim peOffset = br.ReadInt32()
fs.Seek(peOffset, SeekOrigin.Begin)
If br.ReadUInt32() <> &H4550UI Then Return CpuType.Unknown
Dim machine = br.ReadUInt16()
Dim numberOfSections = br.ReadUInt16()
fs.Seek(12, SeekOrigin.Current)
Dim sizeOfOptionalHeader = br.ReadUInt16()
fs.Seek(2, SeekOrigin.Current)
Dim optStart = fs.Position
Dim magic = br.ReadUInt16()
Dim dataDirStart = optStart + If(magic = &H20BUS, &H70, &H60)
fs.Seek(dataDirStart + 8L * 14, SeekOrigin.Begin)
Dim clrRva = br.ReadUInt32()
Dim clrSize = br.ReadUInt32()
If clrRva = 0UI OrElse clrSize = 0UI Then Return CpuType.Unknown
Dim sectionTable = optStart + sizeOfOptionalHeader
fs.Seek(sectionTable, SeekOrigin.Begin)
Dim clrFileOffset As Long = -1
For i = 0 To numberOfSections - 1
fs.Seek(8, SeekOrigin.Current)
Dim vsize = br.ReadUInt32()
Dim vaddr = br.ReadUInt32()
Dim raw = br.ReadUInt32()
Dim ptr = br.ReadUInt32()
fs.Seek(16, SeekOrigin.Current)
Dim span = If(vsize = 0UI, raw, Math.Max(vsize, raw))
If clrRva >= vaddr AndAlso clrRva < vaddr + span Then
clrFileOffset = ptr + (clrRva - vaddr)
Exit For
End If
Next
If clrFileOffset < 0 Then Return CpuType.Unknown
fs.Seek(clrFileOffset + 16, SeekOrigin.Begin)
Dim flags = br.ReadUInt32()
Dim f32req = (flags And &H2UI) <> 0UI
Dim f32pref = (flags And &H20000UI) <> 0UI
Dim ilonly = (flags And &H1UI) <> 0UI
If f32pref AndAlso Not f32req Then Return CpuType.AnyCPU_Prefer32
If f32req Then Return CpuType.x86
If machine = &H8664US Then Return CpuType.x64
If ilonly Then Return CpuType.AnyCPU
Return CpuType.Unknown
End Using
End Function
付録:結果の文字列化と表示例
public static string ToDisplay(CpuType t) => t switch
{
CpuType.x86 => "x86(32bit 固定)",
CpuType.x64 => "x64(64bit 固定)",
CpuType.AnyCPU_Prefer32 => "Any CPU(Prefer 32 bit)",
CpuType.AnyCPU => "Any CPU",
_ => "Unknown"
};
運用チェックリスト
- 判定は マネージ対象ファイル(.exe / .dll)に対して行う(apphost 単体に注意)。
- CorFlags 読み取りの 優先順位を実装で固定する。
- 未知ケースは Unknown とし、Machine / CorFlags の生値をログに残す。
- テスト行列(AnyCPU/Prefer32/x86/x64/ARM)を事前に作成しておく。

コメント