😮

Any CPU -> x64移行でハマった:InstallUtil.exeは同じアーキテクチャで実行する必要がある

に公開

C# と .NET Framework で作成していた Windows サービスのターゲットアーキテクチャを Any CPU から x64 に移行したときにハマったエピソードです。

なぜ x64 に移行したのか

今回 x64 に移行した理由は主に以下の通りです。

  • Windows 11 では x86 版 OS が提供されていない
  • パフォーマンス向上を期待した
    • 物理 CPU のアーキテクチャと一致する
    • x86 のメモリ制限を回避できる
  • x86 固有のライブラリに依存していなかった

発生した問題

x64 ビルドしたアプリケーションを InstallUtil.exe で登録しようとしたところ、サービス登録に失敗しました。

実際に出力されたエラーメッセージは以下の通りです。

PS C:\Users\xxx> C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /ServiceName="MyService" /DispName="MyService" "C:\path\to\BackgroundService\bin\x64\Release\BackgroundService.exe"

Microsoft(R) .NET Framework Installation utility Version 4.8.9221.0
Copyright (C) Microsoft Corporation. All rights reserved.

インストールを初期化中に例外が発生しました:
System.BadImageFormatException: ファイルまたはアセンブリ 'file:///C:\path\to\BackgroundService\bin\x64\Release\BackgroundService.exe'、またはその依存関係の 1 つが読み込めませんでした。間違ったフォーマットのプログラムを読み込もうとしました。

エラーメッセージの読み方

このエラーメッセージのポイントはここです。

そして、例外がSystem.BadImageFormatExceptionであることです。

一見、「ファイルが見つからないエラー」に見えますが、実際は「アーキテクチャ(x86 / x64)の不一致」が原因です。

原因:アーキテクチャ不一致

System.BadImageFormatException は、.NET(.NET Framework / .NET いずれでも)において、アセンブリのアーキテクチャ(x86 / x64)が一致していない場合に発生する代表的な例外です。

  • x86プロセスで x64アセンブリを読み込もうとした
  • x64プロセスで x86専用アセンブリを読み込もうとした
  • ネイティブDLLのビット数が一致しない

今回のケースは1番目です。

  • InstallUtil.exe (x86)
  • 対象アプリケーション(x64)

だったため、アセンブリのロードに失敗し、System.BadImageFormatExceptionが発生していました。

InstallUtil.exe の動作と制約

InstallUtil.exe は内部でアセンブリをロードして処理します。
今回の例では、アセンブリは BackgroundService.exe です。

Microsoft の公式ドキュメントでは次のように説明されています。

Installutil.exe uses reflection to inspect the specified assemblies and to find all Installer types.
https://learn.microsoft.com/en-us/dotnet/framework/tools/installutil-exe-installer-tool

つまり、リフレクションを使って Installer クラスを検出するために、アセンブリをロードします。今回の例では、ここで失敗しました。

解決策

今回の問題の解決策は単純で、InstallUtil.exe を対象アプリケーションと同じアーキテクチャのものに切り替えることです。

公式ドキュメントでも以下のようにも明記されています。

Use the 32-bit Installer tool to install 32-bit assemblies, and the 64-bit Installer tool to install 64-bit assemblies.
https://learn.microsoft.com/en-us/dotnet/framework/tools/installutil-exe-installer-tool

今回、対象アプリケーションを x64 でビルドしたため、x86 版ではなく x64 版の InstallUtil.exe を使う必要がありました。

- C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /ServiceName="MyService" /DispName="MyService" "C:\path\to\BackgroundService\bin\x64\Release\BackgroundService.exe"
+ C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe /ServiceName="MyService" /DispName="MyService" "C:\path\to\BackgroundService\bin\x64\Release\BackgroundService.exe"

代替手段:sc.exe を使ったサービス登録

今回の問題は InstallUtil.exe を使っていたために発生しました。
サービス登録は別の方法でも行えます。

例えば、sc.exe を使います。
sc.exe は Service Control Manager(SCM)を直接操作するコマンドです。

sc create MyService binPath= "C:\path\to\BackgroundService.exe" DisplayName= "MyService"

sc.exe は Windows のサービス管理機能を直接操作するコマンドであり、InstallUtil.exe のようにアセンブリをロードして処理するわけではありません。そのため、「アーキテクチャ(x86 / x64)の影響を受けない」メリットがあります。一方、InstallUtil.exe が提供する「インストール/アンインストール時のカスタムフック処理」は使えません。

そのため、

  • 単純なサービス登録 → sc.exe
  • インストール処理をコードで制御 → InstallUtil.exe

で、使い分けるとよさそうです。

ラグザイア

Discussion