CorFlagsだけで判定!.NET実行ファイルのCPUターゲットをPEヘッダーから正確に見分ける完全ガイド(Any CPU/Prefer 32 bit/x86/x64/ARM)

「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) が 132 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 関連フラグが 064 bit 固定x64
それ以外判定材料不足/非マネージUnknown

なぜ Machine=I386 なのに Any CPU なのか

.NET の管理対象 PE は、歴史的事情から COFF ヘッダーの Machine を I386(0x14C)として埋めることがよくあります。しかし実際に 32/64 bit どちらとしてロードするかは、CLR ヘッダーの CorFlags と OS のビット数で決まります。つまり Machine だけでは不十分で、CorFlags が真の答えです。

アルゴリズム(概要)

  1. MZ 署名を確認し、e_lfanew (0x3C) から PE ヘッダーのオフセットを得る。
  2. 「PE\0\0」を読み飛ばし、Machine を控える。
  3. Optional Header の Data Directory[14](CLR Runtime Header)の RVA とサイズを読む。
  4. セクションテーブルを走査し、RVA → ファイルオフセットを変換する。
  5. 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 bitCorFlags判定結果
.exeAny CPUオフILONLY=1, 32BITREQUIRED=0, 32BITPREFERRED=0Any CPU
.exeAny CPUオンILONLY=1, 32BITREQUIRED=0, 32BITPREFERRED=1Any CPU(Prefer 32 bit)
.exe / .dllx86—32BITREQUIRED=1x86
.exe / .dllx64—32BITREQUIRED=0(Machine=0x8664)x64
.dllAny CPU—ILONLY=1, 32BITREQUIRED=0, 32BITPREFERRED=0Any CPU

ARM・Itanium などの拡張

Machine フィールドも補助的に使えば、ARM 系も検出可能です。必要に応じて列挙を増やせば OK です。

MachineCPU備考
0x14CI386Any CPU でもこの値になることがある(要 CorFlags)
0x8664AMD64x64 固定の指標
0x1C0ARMARM32
0xAA64ARM64Windows on ARM など
0x200IA64Itanium(歴史的)

堅牢化のポイント(失敗しない読み方)

  • 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) =&gt; t switch
{
    CpuType.x86 =&gt; "x86(32bit 固定)",
    CpuType.x64 =&gt; "x64(64bit 固定)",
    CpuType.AnyCPU_Prefer32 =&gt; "Any CPU(Prefer 32 bit)",
    CpuType.AnyCPU =&gt; "Any CPU",
    _ =&gt; "Unknown"
};

運用チェックリスト

  • 判定は マネージ対象ファイル(.exe / .dll)に対して行う(apphost 単体に注意)。
  • CorFlags 読み取りの 優先順位を実装で固定する。
  • 未知ケースは Unknown とし、Machine / CorFlags の生値をログに残す。
  • テスト行列(AnyCPU/Prefer32/x86/x64/ARM)を事前に作成しておく。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次