2000.11.13 更新

■世界最小のSusie Plug-inを作る

●はじめに

 C++Builderで作るとサイズがデカイ!わけで、VCLを使わないようにしてももうひとまわりどうにかなりません?という具合。
 Visual C++ならもう少し小さくなって良い感じなのだが、世の中に出回っているものの中にはこれより小さいサイズのものがある・・・。ど〜なってるの?
 で、色々調べた結果をここにまとめておく。


●調査開始

 まず比較の対象に、読み込まれたらメッセージを表示するだけのDLLのサイズを見てみる。
 VisualC++は、VisualC++6.0SP4を使用。
 新規作成から「Win32 Dynamic-Link Library」の「空のプロジェクト」で生成されたプロジェクトにソースを追加して、デフォルトのRelease設定でビルドする。
 追加するソースファイルの名前は「nop_vc.c」として、中身は下の通り。

#include <windows.h>

BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
    MessageBox(NULL, "Hello World!", "nopdll", MB_OK);
    return TRUE;
}

 C++Builderのほうは、C++Builder5 Update Pack 1を使用。
 新規作成「DLLウィザード」でソースの種類を「C」、マルチスレッドにチェックして生成されたものにコードを追加して、デフォルトのリリース設定でメイクする。
 ソースコードは下の通り。関数名がVisualC++と異なるのは単にエントリポイント関数のデフォルト名の違いに過ぎない。このソースは後で使うので別のところにコピー、「nopbcb.c」にリネームして取っておく。

#include <windows.h>

int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
{
    MessageBox(NULL, "Hello World!", "nopdll", MB_OK);
    return 1;
}

 結果は次の通り。

コンパイラDLLサイズ
Visual C++6.0 SP440,960 Byte
C++Builder548,128 Byte

 うーん、Visual C++6.0はもっと小さかったハズなのだが・・・。SPのおかげか?
 ともかく、なんにもしない割には結構サイズが大きくない?
 これには何が詰まっているのでしょうねー。

 まあ実のところ、簡単にこれより小さくする方法はある。ただし実行には他にランタイムを必要とする、っていうヤツですが。
 この何にもしないDLLはランタイム無しに実行可能なわけだから、このDLLはランタイムと同様なものの塊に違いないと考えられます。(まあちょっと?違うけど)
 実行にはランタイム必要としてサイズを小さくしても、ランタイムのバージョン違いで不具合が出たりするかも知れず。ランタイムは最新のバージョンを使うようにする、というのではサイズを小さくした意味が無いような気が・・・。ランタイムってかなりサイズが大きいから。

 このランタイムというものはいったい何物なのか? これを取り除いてかつ実行可能にしない限りはサイズは小さくならないようだ。


●ランタイムの正体

 おおげさに書いたけど、知ってるよそんなこと。一応ヘルプで確認したけど...
 Cランタイムライブラリ、だね。これのおかげでCのライブラリ関数(mallocとかfopenとか)が使えるわけですよ。
 ということはすなわち、サイズを小さくする→Cランタイムライブラリをとっぱらう→ランタイムが無いのでCのライブラリ関数が使えない、んですが・・・。Win32 APIで代用できなくもないのでなんとかなります(というかなんとかしないといけません)。

 Win32 APIだけで書けば良いわけなんですが、CopyMemoryは実は「memcpyを呼んでるマクロ」だったりと色々裏切られます・・・。


●解決編?

 Visual C++6.0では楽々可能です。
 C++Builder5でも一応出来るようです。
※他のコンパイラは知りませんが、同様なことが出来るでしょう
 具体的には、Cランタイムのスタートアップルーチンを省くことでサイズを小さくします。
 代償としては、Cランタイムライブラリが使えない等の制限があります。



・Visual C++6.0の場合
 前に作成した DllMain 関数はエントリポイント(DLLがロードされたときなどに呼ばれる関数。ここで初期化などを行う)なのですが、実際のところはCランタイムライブラリを使用していると _DllMainCRTStartup というCランタイムの初期化ルーチンがエントリポイントになっていて、ランタイムライブラリを初期化した後に DllMain を呼ぶようになっています。
※この初期化ルーチンには、非ローカルな静的変数に対して C++ コンストラクタを起動する、という役割もあるのでこれも制限になりますね。

 ということがヘルプに書いてあるのですが、DLLを逆アセンブルしたりすると分かり易い(?)かもしれません。
 DllMain以外に色々詰まっているのはMAPファイルを生成させてみると一目瞭然。うんざりするほどあります。普段はランタイムのおかげで楽させてもらっているのですが、今回ばかりはジャマです。


 ランタイムライブラリをリンクせず、DllMainをエントリポイントに設定します。
 せっかくだからMAKファイルを作成してコマンドラインからビルドすることにします。
 MAKファイルの名前は「minidll.mak」として、中身はこんな感じです。

# makefile

cc = cl
link = link
# cflags:コンパイラオプション
cflags = /nologo /O1
# lflags:リンカオプション
lflags = /nologo /DLL /NODEFAULTLIB /ENTRY:"DllMain" /opt:nowin98 /merge:.data=.text /merge:.rdata=.text /section:.text,erw

# dll:DLLのファイル名
dll = nop_vc.dll
# objs:ソースの分だけ追加
objs = nop_vc.obj
# libs:必要なライブラリ
libs = user32.lib

# ---------------------------------------------------
# ソースファイルの拡張子が".c"の場合
.c.obj:
    $(cc) $(cflags) -c $<
# 使いまわせるように".cpp"も入れとく
.cpp.obj:
    $(cc) $(cflags) -c $<

$(dll) : $(objs)
    $(link) $** $(libs) $(lflags) /out:$@

 /opt:nowin98 というオプションはヘルプから見つけることができなかったのですが・・・、Visual C++6.0をインストールしたフォルダにある Readmevc.htm にちょっとだけ記述があります。デフォルトではWindows98 メモリマネージャの都合が良いようにファイルアライメントが4096 バイトで行われる(結果サイズが大きくなる)が、/opt:nowin98 を指定すると512バイトになる。これは/ALIGN:512で指定しても良いような気もしますがこっちはWarning出ますね。(追記)(/ALIGNを使用すると、NT では問題無いそうですが 9x では動作しません。)
 他には/mergeしたりとVisual C++では色々小細工できます。
(追記)
 /NODEFAULTLIB を指定しておくと、うっかりランタイムライブラリ関数を使用してしまってもリンクエラーにしてくれるので指定しましょう...

nmake /F minidll.mak

 でメイクしたところ、気になるサイズは「1,536 Bytes」ですよ!



・C++Builder5の場合

 こちらも同じ理屈ですが、困ったことにC++Builderのリンカにはエントリポイントを指定するオプションがありません。
 「Inside Windows」1999年7月号の「tlink32に/Entryオプションを追加する」によると、
 Visual C++のリンカには/EntryというexeやDLLのエントリポイントを指定するオプションがあり、ランタイムライブラリのスタートアップルーチンをいれないようにすることができる。
 Borlandのリンカにはこのようなオプションが無いのだが、同じことを実現する方法が2つある。エントリポイントをアセンブラで書く。または、tlink32はランタイムライブラリ抜きでリンクできるので、エントリポイントのアドレスをあとで設定しなおす。
 ということらしい。(ただし、これはBorland C++5.2向けの記事なのでC++Builderのリンカilink32とは違うかもしれない)
 C++Builderには(プロフェッショナル版以上なら?)tasm32も付いているのでアセンブラで書いてもいいし、この記事にはエントリポイントのアドレスを設定するツールのソースが付いているし・・・、さて?

 まあともかくコンパイルしてから考えましょう。
 こちらは手抜きでMAKファイルは作りません。
 C++Builder5でコマンドラインからコンパイル、リンク。

bcc32 -O1 -c nopbcb.c
ilink32.exe /Tpd nopbcb.obj , nopbcb.dll, , import32.lib

 ?、エントリポイントを設定しなおす必要は無い?・・・。
 と思ってたのですが、逆アセンブルしてみると全然ダメですよ!?(エントリポイントが変)

 あーでもないこーでもない、と試行錯誤した結果を総合するに、
 「関数をひとつ以上エクスポートしないとエントリポイントが設定されない」ようだ。
 ソースを次のように修正して再コンパイルしたところバッチリです。

#include <windows.h>

void __declspec(dllexport) __stdcall nop(void);

int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
{
    MessageBox(NULL, "Hello World!", "nopdll", MB_OK);
    return 1;
}

void __stdcall nop(void)
{
    return;
}

 ということで、どうやらうれしいことにC++Builder5ではエントリポイントをアセンブラで書いたり後で設定し直さなくても良いようです。DllEntryPoint(またはDllMainでも可)がエントリポイントに設定されます。
 サイズは「4,608 Bytes」でした。MessageBox入れたらさらに大きくなった(^^;

(追記)
 こっちもMAKファイル作りました。

# makefile

cc  = bcc32
link = ilink32
# cflags:コンパイラオプション
cflags = -O1 -w-par

# dll:プラグインのファイル名
dll = nopbcb.dll
# objs:ソースの分だけ追加
objs = nopbcb.obj

# ---------------------------------------------------
# ソースファイルの拡張子が".c"の場合
.c.obj:
    $(cc) $(cflags) -c $<
# 使いまわせるように".cpp"も入れとく
.cpp.obj:
    $(cc) $(cflags) -c $<

$(dll): $(objs)
    $(link) /Tpd /Gn /x $** , $@, , import32.lib

 このMAKファイルの名前を minidll.mak とすると、

make -f minidll.mak

 でメイクできます。



MessageBoxを表示するだけのDLLのサイズ
コンパイラ使用前使用後
Visual C++6.0 SP440,960 Byte1,536 Byte
C++Builder548,128 Byte4,608 Byte



(追記)
 エントリポイントをアセンブラで書いてみました(tasm32用)。
 と言っても単に 1 を返す簡単なやつですが。

.386p

_TEXT SEGMENT PUBLIC DWORD USE32 'CODE'

PUBLIC EntryPoint

EntryPoint proc near
  mov eax, 1
  ret 12
EntryPoint endp

_TEXT ENDS

END EntryPoint

 アセンブルしてリンク。

tasm32 dllmain.asm
ilink32 /Tpd dllmain.obj , nopbcb.dll, , import32.lib

 サイズは 3,072 Bytes。


(追記)
●Visual C++6.0 で標準より小さいスタブプログラムを組み込む

 標準より小さいスタブプログラムを組み込むと、DLL(EXE)のファイルサイズが小さくなる場合があるようです。

 Windows プログラムを MS-DOS で実行すると「このプログラムはDOSじゃ動かないよ」というたぐいの文句が表示されますね。このメッセージは Windows プログラムに組み込まれているMS-DOSプログラムが表示しています。この MS-DOS で実行したときに起動するプログラムがスタブプログラムです。

 /STUB リンカオプションでスタブプログラムを指定することができます。
 「有効な MS-DOS のアプリケーションであれば、どのアプリケーションでもスタブ プログラムとして使用できます。」とヘルプにありますが、ヘッダが有効であれば中身は無効でも使用できるようです(^^;
0000 : 4D 5A 60 00 01 00 00 00 04 00 00 00 FF FF 00 00 MZ`.............
0010 : B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0020 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040 : 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 0D 0A ........!..L.!..
0050 : 68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 2E 0D 0A 24 hello, world...$
 これは「hello, world.」と表示するMS-DOSプログラムのdumpです。
 バイナリエディタでハンドアセンブルしたのでヘッダが合ってるのか不安ですが、とりあえず動きます(^^;
 DLLはDOSで実行されることは無いと思われるのでこれを使っても良いですよね?
 このプログラムファイル名を hello.exe として、リンカオプションに /STUB:"hello.exe" を加えます。これでmakeするとサイズが小さくなる「かも」しれません。


●実践編?

 以上のことを元にBMPのSusieプラグインを作ってみた。
 小さいSusieプラグインサンプルソース(5,492 Byte) 2000.11/11

世界最小の?Susieプラグイン(2000.11/13現在)
コンパイラプラグインサイズ
Visual C++6.0 SP33,072 Byte
Visual C++6.0 SP43,584 Byte
Visual C++6.0 SP4
(小さいスタブプログラムを使用)
3,072 Byte
C++Builder55,632 Byte

Visual C++6.0 SP1で作った方が小さいかもしれない。
さらに小さくするにはフルアセンブラ?
さらにはファイル末尾の0で埋められているところを削除しても動作可能だ。
クラスタサイズより小さけりゃ十分だけど...


戻る