Tech Notes

MSVCで極小サイズのEXEファイルをビルド(2026年最新版)

韓国発で「1.44MB GAME_DEV CONTEST」なるゲーム制作コンテストが発表されたらしい。1.44MBに収まる実行ファイル一本でゲームを作るコンテストだ。

1.44MB GAME_DEV CONTEST(リンク)

容量が収まるならゲームエンジンを使用しても問題ないとあるが、Unityのような近年の大規模なゲームエンジンは空のプロジェクトをビルドするだけで数十MBに達するので、実質的には使用不可と言って良いだろう。いわゆるゲームエンジンでなくともSiv3Dですら怪しい。しかし一方で、そうしたモダンなエンジンやフレームワークを使わないのであれば、1.44MBは思いのほか容易に達成可能なサイズでもある。

実行ファイルの容量というものはどの程度まで小さくできるのか?試しに実験してみたところ、空のウィンドウを出すだけのEXEファイルを1.4KBに収めることに成功した。せっかくなのでこれを共有してみたいと思う。1.44MB GAME_DEV CONTESTの土台にでもなってくれれば幸いだ。

今回作成したプロジェクトファイル(ダウンロード, ZIP形式)

これはもう10年以上も前だが、Windowsの実行ファイルを小さくする記事を書いたことがある。これはその記事の令和最新版である。

フリーゲームの伝説「洞窟物語」が1.44MBに収まっていることは有名[誰によって?]だが、あれは圧縮した上での容量なので、このコンテストはさらに厳しい条件と言えるだろう。といいつつ、1.44MBは依然として実行ファイルとしては膨大な容量だと個人的には考えている。近年の巨大なエンジンやフレームワークがおかしいのである。

基本的な方針

Visual StudioでWindowsデスクトップアプリケーションのテンプレートから始める。空のウィンドウを出すコードで、メニューなどの機能がちょこちょこ付いている。プロジェクト作成時点での実行ファイルは103KBあった。

Visual Studio付属のツール「dumpbin」を使うと、WindowsのEXEファイル(PEファイル)を分析することができる。これを使って余計なものを探して取り除いていく。

チューニングは常に測定と調整の繰り返しによって行われる。結果的には今回以下のような作業を行った。

  • リソースの削除
  • マニフェスト生成オフ
  • デバッグ情報生成オフ
  • スタートアップ処理の排除
  • バッファセキュリティチェックの排除
  • ベースアドレスのランダム化オフ
  • アラインメント最小化
  • サイズ優先最適化

リソースの削除

Windowsの実行ファイルには「リソース」という機能がある。アイコン画像やダイアログのレイアウトといったデータを実行ファイルに埋め込むものだ。任意のデータを埋め込めるので、ゲームの素材を埋め込むのにも使われる。

とりあえず全部削除する。ウィンドウを出すだけならアイコンもダイアログも要らない。全削除。

どうやらアイコン画像が大きいらしく、ここで一気に103KB→10KBに減った。

マニフェスト生成オフ

Windowsの実行ファイルには「マニフェスト」というものがある。これは実行ファイルを動かす際にOSが参考にするためのメタ情報で、リソースの1つという形で埋め込まれる。

  • プロジェクトのプロパティ→「リンカー」→「マニフェスト ファイル」→「マニフェストの生成」で「いいえ (/MANIFEST:NO)」を指定。

これは大した容量ではないのでほとんど減らなかった。

デバッグ情報生成オフ

デバッグデータを削除する。Releaseビルドならデバッグ情報なんてないかと思いきや、どうやら「公開されても良い形のデバッグ情報」というものが入っているらしい。

  • プロジェクトのプロパティ→「リンカー」→「デバッグ」→「デバッグ情報の生成」で「いいえ」を指定。

0.5KBほど削れた。

スタートアップ処理の排除

一般的にC/C++のプログラム開始点はmain関数、Windowsアプリの場合はWinMain関数とされるが、実際には内部的にはそうではない。main関数などより前にランタイムによる初期化処理があり、これのおかげで普段我々は安心して開発することができている。具体的には色々な安全チェックを行ったり、main関数より外に出て行った例外の捕捉などが行われる。

MSVCでは、コンソールアプリの場合mainCRTStartup、デスクトップアプリの場合はWinMainCRTStartupが本物のエントリポイントとなっている。これを書き換えれば、安全性と引き換えに実行ファイルを縮めることができる。

WinMain関数の代わりに以下のような関数を用意してみる。

#ifdef __cplusplus
extern "C" 
#endif
DWORD CustomStartup() {
    // ...
}
  • プロジェクトのプロパティ→「リンカー」→「詳細設定」→「エントリポイント」で「CustomStartup」を指定。ここはCリンケージであれば単純に関数名指定でリンクできる。

これで9KB→5KBまで縮む。なお、Debug構成でこれをやると怒られるのでReleaseビルドのみに限定している。

バッファセキュリティチェックの排除

MSVCではセキュリティのため、バッファオーバーランを検出するための機構が自動で様々な関数に付与される。これを無効にすることができる。 参考はこちらの記事(リンク)。

  • プロジェクトのプロパティ→「C/C++」→「コマンドライン」で/GS-を指定。

ベースアドレスのランダム化オフ

MSVCにはベースアドレスのランダム化という機能が付いている。これは何かというと、DLLなどからインポートする関数について、そのアドレス指定をランダム化する機能だ。どうしてそんなことをするかというと、セキュリティ対策である。

バッファオーバーランを利用して飛び先アドレスを書き換える→任意コード実行というのが攻撃者の常套手段な訳だが、そこでアドレスがランダム化されているとこの攻撃がやりづらい。それでこういう機能が用意されている訳である。動けばいいだけならこの機能は切って良い。

  • プロジェクトのプロパティ→「リンカー」→「詳細設定」→「ランダム化されたベースアドレス」で「いいえ (/DYNAMICBASE:NO)」を指定。これで.relocセクションが消える。

アラインメント最小化

アラインメントとは、データを「キリの良い位置」に配置することである。これによって処理の都合が良くなってパフォーマンスが良くなったりするわけだが、動けば良いだけなら必須ではない場合も多い。

  • プロジェクトのプロパティ→「リンカー」→「詳細設定」→「セクション アラインメント」で「16」を指定。これより下だとエラーが出た。
  • プロジェクトのプロパティ→「リンカー」→「コマンドライン」で「/FILEALIGN:1」を指定。こちらはPEファイルのセクションのアラインメントらしい。こちらは1でも問題ないようだった。

なお、これも詰めすぎるとDebug構成ではエラーが出るのでRelease構成のみにしている。

サイズ優先最適化

最後はコンパイラの最適化頼みでコードサイズを縮める。

速度優先だと.textセクションにアラインメント用のパディングが入ったりするが、これだとギチギチに詰まる。

  • プロジェクトのプロパティ→「C/C++」→「最適化」→「最適化」で最大最適化(サイズを優先) (/O1)を指定。
  • プロジェクトのプロパティ→「C/C++」→「最適化」→「速度またはサイズを優先」で実行可能ファイルのサイズを優先 (/Os)を指定。

その他のサイズ低減の工夫

上に書いたものは設定面のものだけだが、プログラムの書き方もEXEファイルのサイズに関わる。

  • 使わなくて良いAPIは使わない。インポートテーブルのサイズが増えるため。
  • 大きなグローバル変数を使わない。.dataセクションが増えるため。

あとはUPXを使う手もある。UPXとは実行ファイルを実行できるまま圧縮できるフリーのソフトだ。ただしよくマルウェアに使われてしまっているためウイルス検知ソフトウェアに誤検知されるリスクもある。

UPX: the Ultimate Packer for eXecutables(リンク)

おわり

ウィンドウを出すだけなら1.4KBに収まることが分かった。1文字1byteとして400字詰め原稿用紙4枚に満たない。

1.44MBとは当然この1000倍の容量である。一体どれだけのことができるだろうか?

コメント