概要
かずぼん氏の「実行ファイルのサイズを小さくする」という記事(以下 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()
という関数を定義して、マクロで memset
を r_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 用のルーチンを使って動作はしますが、以下のいずれかの対策を行った方がよいでしょう。
-
コンパイルオプションに、
/arch:IA32
または/arch:SSE
を指定し、SSE2 を使わないようにする。(32bit の場合のみ。) -
以下のように、自前で
__isa_available
変数を定義する。(必要ならば、自前のスタートアップルーチン内で CPU の判定を行い値をセットする。)int __isa_available = 0; // SSE2 を使用するなら 2
当然、固定値で 2 を指定した場合は、SSE2 が使えない環境で実行すると正しく動作しません。
Universal CRT 対応
VC14(2015) では、Universal CRT の採用により、デフォルトライブラリの構成が変更されました。そのため、従来の libcmt.lib
に加え、libvcruntime.lib
や libucrt.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 年以上経過しており、実行ファイルのサイズを小さくする意義はますます薄れてきていますが、それでもファイルサイズにこだわりたい人はいろいろ試してみてください。