LoginSignup
10
10

More than 5 years have passed since last update.

実行ファイルのサイズを小さくする

Last updated at Posted at 2016-08-24

概要

かずぼん氏の「実行ファイルのサイズを小さくする」という記事(以下 kazu-1)を元に、最近の Visual C++ を使った場合の情報をまとめました。

なお、対象とするコンパイラは以下の通りとします。
VC10(2010), VC11(2012), VC12(2013), VC14(2015)

デフォルトライブラリ

kazu-1 では、自前のスタートアップルーチンを使用し、デフォルトライブラリ(ランタイムライブラリ、CRT)をリンクしないことでファイルサイズを削減していましたが、実際には、一部のデフォルトライブラリの機能を使いながらもファイルサイズを削減することができます。

デフォルトライブラリの中で、使用可能な機能とそうでないものは以下のように分類できます。

使用可能なもの:

  • 初期化・終了処理が不要なもの
    • 基本的な浮動小数点演算
    • 64ビット整数演算
    • など

使用できないもの(あるいは注意が必要なもの):

  • 初期化・終了処理が必要なもの
    • C++ でグローバル変数等でコンストラクタ・デコンストラクタがあるもの
    • 標準入出力等
    • /GS オプションによるセキュリティチェック
    • memset()memcpy()
    • sin() などの数学関数
    • などなど

なお、初期化・終了処理が必要な機能を使用すると、リンク時に以下のような警告が表示されます。

LIBCMT.lib(cpu_disp.obj) : warning LNK4210: .CRT セクションが存在します。静的初期化子、または終末記号がハンドルされていない可能性があります。

機能によっては、この警告を無視しても動く場合がありますが、基本的には使用できないと考えたほうが良いでしょう。

スタートアップルーチン

kazu-1 では、スタートアップルーチンを以下のように定義していますが、実は呼び出し規約が合っていないため、デフォルトライブラリとリンクさせようとするとリンクエラーが発生してしまいます。

void WINAPI WinMainCRTStartup(void)
{
        :
        :
    ExitProcess(ret);
}

正しくは以下のように、WINAPI を外す必要があります。(つまり、__cdecl を使用する。)

void WinMainCRTStartup(void)
{
        :
        :
    ExitProcess(ret);
}

実は、WinMainCRTStartup() の戻り値は ExitProcess() に渡されるため、以下のようにすることもできます。

int WinMainCRTStartup(void)
{
        :
        :
    return ret;
}

ExitProcess() を使わない分、こちらの方がファイルサイズが小さくなる可能性があります。

なお、コンソールアプリの場合は、スタートアップルーチンとして mainCRTStartup() という名前を使用します。

セキュリティチェック

VC8(2005) から、VC ではセキュリティ関連の機能が強化され、デフォルトで /GS オプションによるバッファーオーバーランの検出が有効になっています。

自前のスタートアップルーチンを使う場合、必要な初期化が行われないため、/GS オプションによるセキュリティチェックが使えません。そのため /GS- を指定して無効化する必要があります。また、セキュリティチェックを無効にした方が(セキュリティ上のリスクは上がりますが)ファイルサイズは小さくなります。

逆に、セキュリティチェックを有効にする場合は、WinMainCRTStartup() の先頭に、以下の行を追加する必要があります。

    __security_init_cookie();

memset(), memcpy()

比較的最近の VC では、memset() が SSE2 に対応しており、その影響で memset() が組み込み関数ではなくなりました。さらに、スタートアップルーチン内で、CPU の機能をチェックし、SSE2 が使用できるかどうかの判定を行うようになりました。このため、デフォルトライブラリの memset() は使用できません。
また、明示的に memset() を使用しなくとも、以下のような構造体のクリアなどで自動的に memset() が使われる場合があります。

struct foo_t foo = {0};

このため kazu-1 のように、r_memset() という関数を定義して、マクロで memsetr_memset に置き換える方法は使えません。しかし、単純に memset() という関数を定義しようとしても以下のようにエラーになってしまいます。

..\common\nodeflib.c(29) : error C2169: 'memset' : 組み込み関数は定義できません。

この場合、以下のように #pragma function を使い、組み込み関数として扱わないように指定する必要があります。

#pragma function(memset)
void *memset(void *d, int c, size_t l)
{
        :

memcpy() についても同様です。

最適化による SSE2 利用

for などを使った普通のループ処理でも、最適化によって自動的に SSE2 が使われる場合があります。
以下は、実際のコンパイル例です。movdqa などの SSE2 命令が使われていることが分かります。

  000000B9: 83 3D 00 00 00 00  cmp         dword ptr [___isa_available],2
            02
  000000C0: 0F 8C 98 00 00 00  jl          0000015E
  000000C6: 66 0F 6E C7        movd        xmm0,edi
  000000CA: 83 E1 07           and         ecx,7
  000000CD: 8B D3              mov         edx,ebx
  000000CF: 66 0F 70 E0 00     pshufd      xmm4,xmm0,0
  000000D4: 2B D1              sub         edx,ecx
  000000D6: 66 0F 6F EC        movdqa      xmm5,xmm4
  000000DA: 8D 9B 00 00 00 00  lea         ebx,[ebx]
            :
        ; SSE2 用の処理
            :
  0000015E: 3B C3              cmp         eax,ebx
  00000160: 73 0E              jae         00000170
  00000162: 8B 4C 84 24        mov         ecx,dword ptr [esp+eax*4+24h]
            :
        ; SSE2 が使えない場合の処理
            :

__isa_available (アセンブリ上では ___isa_available) という変数が 2 未満ならば SSE2 が使えない場合の処理 (0000015E 以降) を実行し、そうでなければ SSE2 用の処理 (000000C6 以降) を実行するようになっています。
この __isa_available という変数は、スタートアップルーチンの中で CPU の機能をチェックし、その結果が格納されるようになっています。
スタートアップルーチンで __isa_available を初期化しない場合、0 が設定されているため、このままでも非 SSE2 用のルーチンを使って動作はしますが、以下のいずれかの対策を行った方がよいでしょう。

  1. コンパイルオプションに、/arch:IA32 または /arch:SSE を指定し、SSE2 を使わないようにする。(32bit の場合のみ。)
  2. 以下のように、自前で __isa_available 変数を定義する。(必要ならば、自前のスタートアップルーチン内で CPU の判定を行い値をセットする。)

    int __isa_available = 0;  // SSE2 を使用するなら 2
    

当然、固定値で 2 を指定した場合は、SSE2 が使えない環境で実行すると正しく動作しません。

Universal CRT 対応

VC14(2015) では、Universal CRT の採用により、デフォルトライブラリの構成が変更されました。そのため、従来の libcmt.lib に加え、libvcruntime.liblibucrt.lib をリンクしなければならない場合があります。
例えば、x64 で strcmp() を使用する場合は、libucrt.lib が必要です。

参照: CRT ライブラリの機能

セクションのマージ

実行ファイルはいくつかのセクションから成ります。
以下は代表的なセクションの例です。

名前 属性 説明
.text 実行可能、読み込み専用 プログラムコード
.rdata 読み込み専用 読み込み専用データ
.data 読み書き可能 読み書き可能データ
.rsrc 読み込み専用 リソースデータ

1つのセクションは、たいてい、ファイル上では 512 bytes 単位、メモリ上では 4096 bytes 単位になっています。そのため、セクションを結合すると無駄な空きが減り、サイズが小さくなる可能性があります。
上記のセクションのうち、.rdata.text はどちらも読み込み専用のため、.rdata.text にマージすることが可能です。リンカのオプションに以下を指定することでセクションをマージできます。

/merge:.rdata=.text

なお、.rsrc も読み込み専用ですが、マージしてしまうとアイコンが表示されなくなるなどの問題が発生するため、マージすることはできません。

.data.text にマージすることもできなくはないのですが、コードが書き換え可能となってしまい、セキュリティリスクが上がるため、非推奨です。

/OPT:NOWIN98

VC6 の頃は、ファイルサイズを小さくする方法として、リンカオプションに /OPT:NOWIN98 を指定するというものがありました。
VC6 では、Windows 98 での実行ファイルのロードを高速化するために、ファイル上のセクションのサイズを 4096 bytes 単位にするのがデフォルトとなっていました。/OPT:NOWIN98 を指定するとファイル上のセクションサイズが 512 bytes 単位に変更され、それによりファイルサイズが最大で 1/8 近くまで小さくなりました。
現在では、/OPT:NOWIN98 は削除され、それを指定せずともファイル上のセクションサイズは 512 bytes 単位がデフォルトとなっています。

/OPT:NOWIN98 で検索すると、VC6 当時のファイルサイズを小さくするための情報がいくつか見つかります。

あとがき

kazu-1 の記事が公開されてからすでに 10 年以上経過しており、実行ファイルのサイズを小さくする意義はますます薄れてきていますが、それでもファイルサイズにこだわりたい人はいろいろ試してみてください。

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10