モジュールのロードを高速化する方法

戻る

EXEファイルの種類をEXEヘッダーから判断して,どのような環境で実行すべきかを ローダーが判断します.たとえば,WindowsNTで16ビットアプリケーションを実行し ようとすると,DOSサブシステムとWOW(Windows On Win32)が動作を開始して,16ビット アプリケーションを実行できる環境を作り出します.
ローダーが行う処理は意外と複雑で,メモリ内にモジュールをマッピングし, EXEファイルが利用するDLLのエントリポイントを設定したり多くの処理を必要と します.ある程度の規模を持つアプリケーションでは,EXEファイルやDLLをロードして マッピングするための処理時間は無視できないものとなってきています.
そこで,少しでもローダーの処理を軽減して,アプリケーションの起動時間を速くする 方法について考えます.

9-1 最も簡単な高速化

Microsoft Office をインストールすると,「Fast スタート」という小さな アプリケーションが「スタートアップグループ」に登録されていることに気づきます. このアプリケーションは,Office アプリケーションが利用するOLE関連のモジュールを 前もってメモリ中にロードさせておくことで,アプリケーションの起動時間を短縮 させるために利用されます.これと同様に,通常のアプリケーションでも,巨大なDLLを ロードし,しかも起動,終了を頻繁に繰り返す可能性のあるアプリケーションでは, 「スタートアップ」グループに起動速度のボトルネックになるDLLをロードさせるために そのDLLのインポートライブラリをリンクした,何もしない(終了もしない) アプリケーションを実行させておくことで,起動時間を短縮することができるように なります.しかし,不用意にこのような方法を利用すると,メモリの少ないマシン では,かえって速度低下を招く恐れがありますので注意は必要です.
この方法をさらに発展させた方法としては,「秀丸エディタ」 (* 秀まるお氏著作の高機能テキストエディタ,ちなみに本稿は秀丸エディタで執筆しているが,同氏と著者はまったく面識はない.)
などのように自分自身の一部を常時タスクトレイアイコンに登録してロードさせて おくことで高速化を図るといったことも考えられます. この方法の方がアプリケーションの利用者に対して,無意味にモジュールを ロードしているようなイメージを与えないため,精神衛生上よいのではないかと 思われます.ただし,アプリケーションの性質にもより,必ずしもこの方法が最適とは 限りませんので,作成するアプリケーションにふさわしい方法を検討する必要が あります.

9-2 ローダーの作業を前もって行っておく

通常,EXEファイルは 40000000H に,ユーザが作成したDLLは 10000000H にロード されます.プロセス内にはEXEファイルは1つしかありませんが,DLLはシステムDLLを 含めて複数存在します.
ユーザDLLが複数存在する場合,最初のDLLがロードされると,次のDLLからは リロケーションが必ず発生します.複数のDLLをロードするアプリケーションを Visual C++ 4.2 の Deveroper Studio でデバッグを行うとリロケーションを行ったこと を知らせるログを出力します.

例9-1:ベースアドレスが設定されていない3つのDLLを同一プロセス空間にロード (WindowsNT4.0 / VisualC++ver4.2 Deveroper Studioで実行)

LDR: Automatic DLL Relocation in test.exe
LDR: Dll AAA.dll base 10000000 relocated due to collision with C:\Work\BBB.dll
LDR: Automatic DLL Relocation in test.exe
LDR: Dll AAA.dll base 10000000 relocated due to collision with C:\Work\CCC.dll

1つのEXEファイルがベースアドレスを設定していない複数のDLLを利用する場合, プロセス(EXEファイル)が実行を開始する度にリロケーションが実行されます.
このため,DLLの数が多ければ多いほど,起動時間が遅くなることになります.
このオーバヘッドを回避するには,各DLLのベースアドレスをあらかじめ衝突しない アドレスに設定しておく必要があります.
システムDLLは,出荷時にバインドが実行されており,どの組み合わせでロードされても 衝突が起こらないようになっています.通常,0x70000000 から 0x78000000 (MIPSベースのOS 0x68000000 から 0x78000000) にマッピングされています.
このため,ユーザが作成したDLLは,0x60000000 から 0x68000000 の間に ベースアドレスを設定する必要があります.
マッピングを行うための規則は自由ですが,アプリケーションの拡張によってDLLの 数が増えても影響を受けないような方法を検討する必要があります. また,1つのEXEファイルでしか利用されないDLLしか存在しないときには, REBASE.EXE と モジュールを列挙したテキストファイルを用意すれば,EXEファイルを 先頭にして利用するDLLのベースアドレスを一斉に変更することもできます.
(* REBASE.EXE のオプションの説明は /? で表示させることができますが, コンソールの80×25 に収まりません.WindowsNTではバッファサイズを指定すれば スクロールさせて全体を見ることができますが,Windows95では見ることができません. また,標準エラー出力に出力しているため more を利用することができません. 最近,コンソールを使う機会がめっきり減ってきていますが,復習の意味を込めて 標準エラー出力ストリームの操作をまとめてみた.)

例9-2:標準エラー出力ストリーム

appication.exe <    filename   ... 標準入力(stdin)
appication.exe >    filename   ... 標準出力(stdout)
appication.exe >>   filename   ... 標準出力(stdout)追加オープン
appication.exe 2>   filename   ... 標準エラー出力(stderr)
appication.exe 2>>  filename   ... 標準エラー出力(stderr)追加オープン
appication.exe 2>&1            ... 標準エラー出力を標準出力に変更
appication.exe 1>&2            ... 標準出力を標準エラー出力に変更

例9-2:c:\work\base.txt を作成してベースアドレスが衝突しないようにベースアドレスを再設定する

C:\Work>REBASE -b 400000 -R c:\work -G base.txt


base.txtの内容
+----------+
|TEST.EXE  |
|AAA.DLL   |
|BBB.DLL   |
|CCC.DLL   |
+----------+

9-3 「セットアップはバインドを実行しています」

Visual Studio 97 のインストールを実行すると,最後の段階で「セットアップは バインドを実行しています」といったような意味のダイアログボックスを表示して, なにやら処理を行っています.
そもそも「バインド」とは,ローダーがモジュールをロードしたときに利用するすべて のDLLへのエントリポイント(APIの呼び出しアドレス)をプロセス内に設定する処理の ことを指します.この作業は,何回実行しても同様の結果となるため実行する以前に ファイル内の情報を設定しておくことで,メモリにロードされる度に実行する必要が なくなります.バインドされていないモジュールをローダーがメモリ内にロードを 行う場合,インポートされているすべてのAPI(ユーザDLLの関数も含む)に対して, エントリポイントの設定を行います.このため,APIやDLLの関数を多く利用数に 比例してモジュールのロードのオーバーヘッドが増大することになります.

9-4 実行する前にバインドする

モジュールを実行する前にバインドしておくことにより,それらの処理を省略する ことができます.しかし,バインドで設定される値はOSやOSのバージョンによって 異なるため,バインドを実行するマシン以外でモジュールを実行するとその情報は 無効となりバインドされていないときと同様にローダーがバインドを実行することに なることがあります.このため,バインドはセットアッププログラム内で実行し, モジュールがロードされる環境とバインドを行う環境ができるだけ一致できるような タイミングで行うのがよいことになります.

9-5 バインドを行うには

では実際にバインドを行うにはどうすればよいのでしょうか?
まず,ビルド時に リンカーの/BASE オプションを利用するか,REBASE.EXE を利用して プロセスにロードされる各モジュールのベースアドレスが衝突しないようにしておく 必要があります.衝突が発生してリロケーションが発生すると,次に行うバインドが 無効になってしまいますので注意が必要です. そして,EDITBIN.EXE や Win32 SDK に含まれる BIND.EXE を利用してバインドを 実行します.
しかし,このツールは再配布できませんのでセットアッププログラムと共に配布する ことができません.となると,自前で組み込むことになります. バインドを行う処理は,以前 Win32 SDK に IMAGEHLP.DLL サンプルソースとして提供 されるていましたが,このDLLも再配布が禁止されているため,DLLのサンプルソース から必要な処理を摘出して自分のアプリケーションに埋め込む必要がありました. このサンプルソースは,最近のSDKではなくなっていましたが, なぜか Visual C++ ver5.0 に再びサンプルソースとして提供されるようになりました. このDLLは,Windows95/NT4.0 から,サンプルで提供されているものから拡張された バージョンが正式にサポートされるようになり,インストーラなどで簡単にバインドを 行うことができるようになりました.

例9-3:DUMPBIN.EXE でバインド前後のインポートされた関数の状態を出力

         Section contains the following Imports

            KERNEL32.dll
                 D5   GetCurrentThread
                14C   GetVersion
                179   InitializeCriticalSection
                 4C   DeleteCriticalSection
                 58   EnterCriticalSection
                18F   LeaveCriticalSection
                 6B   ExitProcess
                            :
                            :

               (a) バインド前のイメージ


         Section contains the following Imports

            KERNEL32.dll
               7000514E    D5   GetCurrentThread
               7001412C   14C   GetVersion
               7001C240   179   InitializeCriticalSection
               77F5CAE2    4C   DeleteCriticalSection
               77F57420    58   EnterCriticalSection
               77F574F0   18F   LeaveCriticalSection
               70019B3C    6B   ExitProcess
                            :
                            :

               (b) バインド後のイメージ
                   (ロード時に割り当てられるアドレスが
                           モジュール内に記録されている)

           バインド前後のモジュールに対して DUMPBIN.EXE /ALL を実行した結果の
           を WINDIFF.EXEで比較すると,その違いを見ることができる

9-6 イメージヘルプAPI

IMAGEHLP.DLLは,バインドを行う以外にイメージ(* 画像ではなく,実行モジュール のことを指します.ハッカーなどのマニアックな人種がバイナリコードのことを イメージと呼ぶことに由来していると聞いたことがあるが定かではない.) を直接操作することをサポートするためのヘルパーAPI群です.
イメージヘルプには,イメージ修正関連,デバッグサポート関連,イメージアクセス 関連,イメージの完全性サポートに区分けされます.イメージのバインドなどイメージ の情報を操作するときには,イメージ修正関連のAPIを利用します.本章で利用している Win32 SDK のツール REBASE.EXE や BIND.EXEは,これらのAPIを利用しています. ベースアドレスの変更を行うには,Win32 API ReBaseImage を,バインドを行うには Win32 API BindImage または Win32 API BindImageEx を利用します.
なお,BindImageEx は,バインドを行ったときのステータスをコールバック関数で 受け取ることができます.

表9-1:イメージヘルプAPI(イメージ修正関連API)
BOOL BindImage(LPSTR lpszImageName,
 LPSTR lpszDllPath, LPSTR lpszSymbolPath)
DLLからインポート関数の仮想アドレスを算出する

引数
LPSTR lpszImageName ... バインドを行うモジュールのファイル名
LPSTR lpszDllPath ... DLLのサーチパス
LPSTR lpszSymbolPath ... シンボルファイルのサーチパス

戻り値
正常終了 TRUE
異常終了 FALSE
BOOL BindImageEx(DWORD dwFlags, LPSTR lpszImageName,
 LPSTR lpszDllPath, LPSTR lpszSymbolPath
 PIMAGEHLP_STATUS_ROUTINE StatusRoutine)
DLLからインポート関数の仮想アドレスを算出する

引数
DWORD dwFlags ... コントロールフラグ
 BIND_NO_BOUND_IMPORTS ... 新しいインポートアドレステーブルを生成しない
 BIND_NO_UPDATE ... ファイルのアップデートを行わない
 BIND_ALL_IMAGES ... このファイルのためのすべての呼び出しをバインドする
LPSTR lpszImageName ... バインドを行うモジュールのファイル名
LPSTR lpszDllPath ... DLLのサーチパス
LPSTR lpszSymbolPath ... シンボルファイルのサーチパス
PIMAGEHLP_STATUS_ROUTINE StatusRoutine ... IMAGEHLP_STATUS_ROUTINE構造体を指すポインタ

戻り値
正常終了 TRUE
異常終了 FALSE
BOOL ReBaseImage(LPSTR lpszCurrentImageName, LPSTR lpszSymbolPath,
 BOOL fReBase, BOOL fRebaseSysfileOk, BOOL fGoingDown,
 DWORD dwCheckImageSize,
 LPDWORD lpdwOldImageSize, LPDWORD lpdwOldImageBase,
 LPDWORD lpdwNewImageSize, LPDWORD lpdwNewImageBase,
 DWORD dwTimeStamp)
DLLのロードを行うアドレスを変更する

引数
LPSTR lpszCurrentImageName ... ベースアドレスの変更を行うファイル名
LPSTR lpszSymbolPath ... シンボルファイルへのサーチパス
BOOL fReBase ... 変更を行うときにはTRUEを設定する
BOOL fRebaseSysfileOk ... システムイメージの変更を行うときTRUEを設定する.
 システムイメージのベースアドレスは 0x80000000 より
 大きい値を設定する必要がある
BOOL fGoingDown ... もしイメージが与えられたベースの下で変更するときにはTRUEを設定する
DWORD dwCheckImageSize ... イメージの最大サイズを設定する
 指定しないときには0を設定する
LPDWORD lpdwOldImageSize ... 変更を行う前のイメージサイズ
LPDWORD lpdwOldImageBase ... 変更を行う前のイメージベース
LPDWORD lpdwNewImageSize ... 新しいイメージサイズ
LPDWORD lpdwNewImageBase ... 新しいイメージベース
DWORD dwTimeStamp ... 新しいタイムスタンプ(Cランタイムのtime関数で取得する)

戻り値
正常終了 TRUE
異常終了 FALSE

CheckSumMappedFile イメージファイルのチェックサムを計算する
MapFileAndCheckSumA イメージファイルのチェックサムを計算する(ANSI版)
MapFileAndCheckSumW イメージファイルのチェックサムを計算する(UINCODE版)
RemovePrivateCvSymbolic CodeViewデバッグ情報からパブリック情報を取り除く
RemoveRelocations リロケーション情報をイメージファイルから取り除く
SplitSymbols シンボル情報をイメージから取り除く
UpdateDebugInfoFile 指定された情報でDBGファイルの情報を更新する
UpdateDebugInfoFileEx 指定された情報でDBGファイルの情報を更新する

表9-2:Win32 API BindImageEx で利用する関数と実行ステータス
BOOL (__stdcall *PIMAGEHLP_STATUS_ROUTINE)(
 IMAGEHLP_STATUS_REASON Reason,
 LPSTR lpszImageName, LPSTR lpszDllName,
 ULONG uVa, ULONG uParameter)
BindImageEx の実行ステータスを取得する

引数
IMAGEHLP_STATUS_REASON Reason
LPSTR lpszImageName ... イメージファイル名
LPSTR lpszDllName ... DLL名
ULONG uVa ... 仮想アドレス
ULONG uParameter ... パラメータ

戻り値
正常終了 TRUE
異常終了 FALSE

typedef enum _IMAGEHLP_STATUS_REASON {
    BindOutOfMemory,              メモリ不足
    BindRvaToVaFailed,            RVAからVAに変換できなかった
    BindNoRoomInImage,            イメージファイルが読み込めなかった
    BindImportModuleFailed,       インポートモジュールをバインドできなかった
    BindImportProcedureFailed,    インポートプロシジャをバインドできなかった
    BindImportModule,             インポートモジュールをバインドした
    BindImportProcedure,          インポートプロシジャをバインドした
    BindForwarder,                ???
    BindForwarderNOT,             ???
    BindImageModified,            イメージを修正した
    BindExpandFileHeaders,        拡張ファイルヘッダをバインドした
    BindImageComplete,            イメージのバインドが終了した
    BindMismatchedSymbols,        シンボルが一致しなかった
    BindSymbolsNotUpdated         シンボルはアップデートされなかった
} IMAGEHLP_STATUS_REASON;

9-7 サンプルアプリケーション

Win32 SDK の BIND.EXE の簡易版です.
BIND.EXE は,再配布できないアプリケーションであるため,セットアッププログラム で利用することができません.しかし,IMAGEHLP.DLL が標準でサポートされましたので アプリケーション内部にその処理を組み込むことができるようになりました.
実際に利用するときには,事前にREBASE.EXEでベースアドレスが衝突しないように設定 しておく必要があります.ただし,このサンプルアプリケーションはWindows95/NT4.0 でしか利用できません.WindowsNT3.51以前のOSで利用する必要があるときには, Win32 SDK や Visual C++ ver5.0 の IMAGEHLP.DLL サンプルを参考に組み込む必要が あります.Win32 API BindImageEx の実行テスト用に作成したため,いろいろ表示 しますが,単純にバインドしてその結果を書き込みを行なってるだけです.
実際にこのAPIを利用するのはインストーラが多いと思いますのが,利用する前にAPIの 実行で何が起こるのかをよく確認しておくとよいでしょう.

●使い方
>BINDER イメージファイル名 [-dllpath パス] [-symbolpath パス]
例: >binder test.exe
例: >binder test.exe -dllpath c:\dll -symbolpath c:\winnt\symbol
-dllpath を省略すると,システムディレクトリとなり,-symbolpath を省略すると カレントディレクトリとなります.-sympath のディレクトリにバインドを行う モジュールのDBGファイルが見つかると,DBGファイルのチェックサムなどを更新 します.
BindImageEx は,イメージ(実行ファイル)内で利用されているDLLを,カレント ディレクトリから探し,見つからなかったときに lpszDllPath に指定されたパスを 探します.正常に実行されたかは,このBINDER.EXEの実行結果でも分かりますが, DUMPBIN.EXE /ALL (/IMPORTS)でイメージファイルの出力結果でも確認することが できます.

●ソースファイル
BINDER.EXE
 BINDER.C
 MAKEFILE

例9-4:実行例

-----------------------------------------
イメージファイル名:   wboot32.exe
DLLの検索パス:        C:\WINDOWS\SYSTEM
シンボルファイルパス: D:\WORK\MSC\imagehlp\src
-----------------------------------------
BindImageEx: 処理は正常に終了しました.