System32 に x86 と x64 が混ざっていた PE ヘッダー調査
System32 の DLL 4324本を読んだら、ARM64 が4155本、x86 が122本、x64 が46本、ARM32 が1本だった(Machine だけを見る割り切りを忘れそうになる)。
ARM64 版 Windows の C:\Windows\System32 なので、ほとんど ARM64 だろうとは思っていた。けれど、System32 という名前から受ける印象よりも中身は雑多に見える。EXE でも ARM64 696本に対して x64 が4本、x86 が2本あり、DLL では x86 の数が122本まで増える(名前のきれいさほど、中身はきれいに分かれていない)。
実際に見て一番引っかかったのは、ARM32 が1本だけ残っていたことだ。該当ファイルは C:\Windows\System32\DriverStore\FileRepository\prnms006.inf_arm64_8bcb2c96990be129\SendToOneNoteFilter.dll だった。ARM64 でも x86 でもなく ARM32 という結果が、プリンタードライバー配下に1本だけ出ている。
計測日は2026-06-23。2026年6月23日の夜に、手元の Surface Pro 11th Edition、Windows 11 Pro 10.0.26200 ARM64 で測った。数字はファイル名ではなく PE ヘッダーの Machine フィールドだけで分類している。短い調査のつもりだったが、フォルダー名と実体のずれを見始めると、思ったより引っかかる点が多かった。
ヘッダーだけを読むことにした
やったことは単純で、C:\Windows\System32 と C:\Windows\SysWOW64 の EXE / DLL を開き、PE ヘッダーの Machine を読むだけにした。先頭2バイトが MZ、0x3C に PE ヘッダーへのオフセットが入っている。そこから PE\0\0 を確認し、4バイト進めた位置の2バイトが Machine になる。
def read_machine(path):
with open(path, "rb") as f:
mz = f.read(2)
if mz != b"MZ":
return None
f.seek(0x3C)
pe_offset = int.from_bytes(f.read(4), "little")
f.seek(pe_offset)
if f.read(4) != b"PE\0\0":
return None
machine = int.from_bytes(f.read(2), "little")
return {
0xAA64: "ARM64",
0x8664: "x64",
0x014C: "x86",
0x01C4: "ARM32",
0xA641: "ARM64EC",
0xA64E: "ARM64X",
}.get(machine, hex(machine))
この方法だと、ファイル名やフォルダー名に引きずられない。System32 にあるから ARM64、SysWOW64 にあるから x86、とは決め打ちしない。実際にそうしなかったから、System32 配下の x86 と x64 が見えてきた(フォルダー名を信じていたら、この部分は丸ごと見落としていたはず)。
一方で、後で書くように ARM64X の判定には足りていなかった可能性がある。COFF ヘッダーの Machine だけを見る、という割り切りが今回の測定条件だ。
フォルダー別に数えた結果
走査した対象は System32 の EXE 702本、System32 の DLL 4324本、SysWOW64 の EXE 330本(ARM64X を拾うには読み取りが浅かった)。SysArm32 の EXE はフォルダー自体が存在しなかったので、件数は取れていない。
容量比率は bytes_by_machine から計算した。System32 の DLL は本数だと ARM64 が96.1%、容量だと93.2%になる(なぜ一部だけ逆向きに混ざるのか、後で引っかかった)。x86 は122本あるが、容量では5.7%だったので、数だけで見るよりは少し軽い存在感になる。
| 対象 | count | ARM64 | x64 | x86 | ARM32 | 容量メモ |
|---|---|---|---|---|---|---|
System32 *.exe |
702本 | 696本 / 99.1% / 636494216 bytes | 4本 / 0.6% / 764264 bytes | 2本 / 0.3% / 250704 bytes | 0本 | ARM64 が99.8%相当 |
System32 *.dll |
4324本 | 4155本 / 96.1% / 4685434672 bytes | 46本 / 1.1% / 52961288 bytes | 122本 / 2.8% / 286007728 bytes | 1本 / 0.0% / 77312 bytes | x86 が5.7%相当 |
SysWOW64 *.exe |
330本 | 2本 / 0.6% / 1119856 bytes | 0本 | 328本 / 99.4% / 51695656 bytes | 0本 | x86 が97.9%相当 |
SysArm32 *.exe |
なし | 0本 | 0本 | 0本 | 0本 | フォルダーなし |
System32 の EXE で x64 だったのは、C:\Windows\System32\dns-sd.exe、C:\Windows\System32\dpnsvr.exe、C:\Windows\System32\fixmapix64.exe、C:\Windows\System32\wbem\WmiPrvSEx64.exe の4本。x86 は C:\Windows\System32\DriverStore\FileRepository\devicestelemetryservicedriver.inf_arm64_1ed8bdd395254b72\AppLauncher.exe と C:\Windows\System32\WindowsPowerShell\v1.0\powershell_ise.exe の2本だった。
DLL 側の x64 サンプルには C:\Windows\System32\ccmcore.dll、C:\Windows\System32\ccmperf.dll、C:\Windows\System32\CcmUsrCse.dll、C:\Windows\System32\dfshim.dll、C:\Windows\System32\directml_x64.dll がある。x86 サンプルは C:\Windows\System32\AuthFWSnapin.dll、C:\Windows\System32\AuthFWWizFwk.dll、C:\Windows\System32\enterpriseetw.dll、C:\Windows\System32\ETWCoreUIComponentsResources.dll、C:\Windows\System32\Microsoft-Windows-AppModelExecEvents.dll だった。
SysWOW64 にも逆向きの混在があった
SysWOW64 は名前どおり x86 の置き場として見れば、328本が x86 という結果は自然だった。目を引いたのは、ARM64 が2本だけ混ざっていた点。該当したのは C:\Windows\SysWOW64\edit.exe と C:\Windows\SysWOW64\sudo.exe。SysWOW64 の EXE 330本中2本なので、本数では0.6%にすぎないが、混在している事実は残る(小さい例外ほど、決め打ちの足をすくう)。
容量で見ると、SysWOW64 の x86 は51695656 bytes、ARM64 は1119856 bytes(名前と実体のずれが思ったより多かった)。比率にすると x86 が97.9%、ARM64 が2.1%になる。2本だけでも sudo.exe や edit.exe のような目立つ名前が入っているので、フォルダー名だけでアーキテクチャを決めると間違える。
System32 でも同じだった。powershell_ise.exe は System32 の下にいるが x86 として出ている。WmiPrvSEx64.exe は名前にも x64 が入っていて、Machine も x64 だった。名前とヘッダーが一致する場合もあるが、常にそうだとは扱えない、というだけの話。
ARM64X を拾えなかった失敗
この点は測定の失敗として残しておく。判定表には ARM64EC の 0xA641 と ARM64X の 0xA64E を入れている。ところが結果はどちらも0本になり、あまりにきれいすぎて後から不安になった。
最初は、手元の Windows 11 Pro 10.0.26200 には ARM64X がほとんど無いのかもしれない、という見立て(次はヘッダーの奥まで見直したい)。けれど、ARM64X は System32 の一部 DLL で使われていると聞いていたので、0本はきれいすぎる(プリンタードライバー配下の一本が妙に残った)。気づくまで少し時間がかかったが、COFF ヘッダーの Machine だけ見ても ARM64X は ARM64 として見える可能性があり、このあたりで「読んでいる場所が浅いのではないか」と疑い始めた。つまり、今回の関数は「PE ファイルの表面上の Machine を読む」だけで、ARM64X の追加構造までは見ていない(ここを後から掘ってみる余地は残った)。
ここを詰めるなら、別のヘッダーやロード構成、あるいは ARM64X 特有のメタデータまで追う必要がある。今回はそこまで掘っていない。だから verdict は「動いた」ではなく「条件つき」にした。x86、x64、ARM32 の混在を見る用途では動いたが、ARM64X の検出器としては足りていない(例外の数より場所のほうが気になった)。
どう使い分けるか
この結果を見てから、Windows on ARM の調査ではフォルダー名を信用しすぎないことにした。System32 は ARM64 だけの箱ではない(フォルダー名を信用しすぎる癖が出ていた)。SysWOW64 も x86 だけの箱ではない(フォルダー名を信用しすぎる癖が出ていた)。だいたいは名前どおりでも、例外が数本から数百本ある。
個人的には、トラブルシュートで「これはネイティブかエミュレーションか」を見るとき、where.exe の場所だけでは終わらせないほうがいいと感じた。PE ヘッダーを2バイト読むだけなら軽く、今回のように全件を舐めてみるだけでも、フォルダー名からは見えない混在が浮いてくる(Machine だけを見る割り切りを忘れそうになる)。
一番おもしろかったのは、System32 の ARM32 DLL 1本だった。互換性のために残っているのか、ドライバー配下の都合なのか、この時点では分からない。次に測るなら ARM64X を正しく拾う方法を足して、同じ702本と4324本をもう一度数え直したい(ARM64X を拾うには読み取りが浅かった)。