リンカーによる DLL の遅延読み込み
MSVC リンカーでは、DLL の遅延読み込みをサポートしています。 この機能により、DLL の遅延読み込みを実装するために、Windows SDK の関数 LoadLibrary
と GetProcAddress
を使用する必要がなくなります。
遅延読み込みを行わない場合、実行時に DLL を読み込む唯一の方法は、LoadLibrary
と GetProcAddress
を使用することです。オペレーティング システムでは、それが使用される実行可能ファイルまたは DLL が読み込まれるときに、DLL を読み込みます。
遅延読み込みを行う場合、DLL を暗黙的にリンクするときに、プログラムで DLL 内の関数が呼び出されるまでその DLL の読み込みを遅らせるオプションが、リンカーによって提供されます。
アプリケーションでは、/DELAYLOAD
(遅延読み込みのインポート) リンカー オプションをヘルパー関数と共に使用することで、DLL の遅延読み込みを行うことができます。 (既定のヘルパー関数の実装は、Microsoft によって提供されます)。ヘルパー関数は、 LoadLibrary
を呼び出して、実行時に DLL をオンデマンドで読み込み、 GetProcAddress
します。
次の場合に、DLL の遅延読み込みを行うことを検討してください。
プログラムで DLL 内の関数が呼び出されない可能性がある。
プログラムの実行の最後の方まで、DLL 内の関数が呼び出されない可能性がある。
DLL の遅延読み込みは、EXE プロジェクトまたは DLL プロジェクトのいずれかのビルド時に指定できます。 1 つ以上の DLL の読み込みを遅らせる DLL プロジェクト自体では、遅延読み込みが行われたエントリ ポイントを DllMain
に呼び出さないでください。
遅延読み込みする DLL の指定
/delayload:
dllname
リンカー オプションを使用して、遅延読み込みを行う DLL を指定できます。 独自のバージョンのヘルパー関数を使う計画がない場合は、プログラムを delayimp.lib
(デスクトップ アプリケーションの場合) または dloadhelper.lib
(UWP アプリの場合) とリンクする必要もあります。
DLL の遅延読み込みの簡単な例を次に示します。
// cl t.cpp user32.lib delayimp.lib /link /DELAYLOAD:user32.dll
#include <windows.h>
// uncomment these lines to remove .libs from command line
// #pragma comment(lib, "delayimp")
// #pragma comment(lib, "user32")
int main() {
// user32.dll will load at this point
MessageBox(NULL, "Hello", "Hello", MB_OK);
}
DEBUG バージョンのプロジェクトをビルドします。 デバッガーを使用してコードを段階的に実行すると、MessageBox
に対する呼び出しを行う場合にのみ user32.dll
が読み込まれることがわかります。
遅延読み込みした DLL の明示的なアンロード
/delay:unload
リンカー オプションを使用すると、遅延読み込みが行われた DLL をコードで明示的にアンロードできます。 既定では、遅延読み込みが行われたインポートは、インポート アドレス テーブル (IAT) に残ります。 ただし、リンカー コマンド ラインで /delay:unload
を使用する場合、ヘルパー関数では、__FUnloadDelayLoadedDLL2
の呼び出しによる DLL の明示的なアンロードをサポートし、IAT を元の形式にリセットします。 現在無効なポインターが上書きされます。 IAT は、元の IAT のコピー (存在する場合) のアドレスが含まれる ImgDelayDescr
構造体のフィールドです。
遅延読み込みが行われた DLL のアンロードの例
次の例は、関数 fnMyDll
が含まれる DLL MyDll.dll
を明示的にアンロードする方法を示します。
// link with /link /DELAYLOAD:MyDLL.dll /DELAY:UNLOAD
#include <windows.h>
#include <delayimp.h>
#include "MyDll.h"
#include <stdio.h>
#pragma comment(lib, "delayimp")
#pragma comment(lib, "MyDll")
int main()
{
BOOL TestReturn;
// MyDLL.DLL will load at this point
fnMyDll();
//MyDLL.dll will unload at this point
TestReturn = __FUnloadDelayLoadedDLL2("MyDll.dll");
if (TestReturn)
printf_s("\nDLL was unloaded");
else
printf_s("\nDLL was not unloaded");
}
遅延読み込みが行われた DLL のアンロードに関する重要な注意事項:
__FUnloadDelayLoadedDLL2
関数の実装は、MSVC のinclude
ディレクトリにある、ファイルdelayhlp.cpp
内に見つかります。 詳細については、「遅延読み込みヘルパー関数について」を参照してください。__FUnloadDelayLoadedDLL2
関数のname
パラメーターは、インポート ライブラリに含まれるものと完全に一致している必要があります (大文字と小文字の区別を含む)。 (その文字列は、イメージ内のインポート テーブルにも含まれています)。DUMPBIN /DEPENDENTS
を使用して、インポート ライブラリの内容を表示できます。 大文字と小文字を区別せずに文字列を一致させる場合は、大文字と小文字を区別しない CRT 文字列関数のいずれか、または Windows API 呼び出しを使用するように、__FUnloadDelayLoadedDLL2
を更新できます。
遅延読み込みが行われたインポートのバインド
リンカーの既定の動作では、遅延読み込みが行われた DLL に対して、バインド可能なインポート アドレス テーブル (IAT) を作成します。 DLL がバインドされている場合、ヘルパー関数では、参照される各インポートで GetProcAddress
を呼び出す代わりに、バインドされた情報の使用を試みます。 タイムスタンプまたは優先アドレスが読み込まれた DLL のものと一致しない場合、ヘルパー関数ではバインドされたインポート アドレス テーブルが古いと見なされます。 IAT が存在しないかのように続行します。
DLL の遅延読み込みが行われたインポートをバインドしない場合は、リンカーのコマンド ラインで /delay:nobind
を指定します。 リンカーでは、バインドされたインポート アドレス テーブルを生成しません。これにより、イメージ ファイルの領域が節約されます。
遅延読み込みした DLL に対するすべてのインポートの読み込み
delayhlp.cpp
で定義された __HrLoadAllImportsForDll
関数では、/delayload
リンカー オプションを使用して指定された DLL からすべてのインポートを読み込むようにリンカーに指示します。
すべてのインポートを一度に読み込むと、エラー処理を 1 か所にまとめることができます。 インポートに対する実際のすべての呼び出しについて、構造化例外処理を回避できます。 また、プロセスの途中でアプリケーションが失敗する状況を回避します。たとえば、ヘルパー コードでその他のインポートの読み込みが成功した後に、あるインポートの読み込みに失敗した場合などです。
__HrLoadAllImportsForDll
を呼び出しても、フックやエラー処理の動作は変わりません。 詳細については、「エラー処理と通知」を参照してください。
__HrLoadAllImportsForDll
により、DLL 自体の内部に格納されている名前に対して、大文字と小文字を区別した比較が行われます。
TryDelayLoadAllImports
という関数で __HrLoadAllImportsForDll
を使用して、名前付き DLL の読み込みを試行する例を次に示します。 関数 CheckDelayException
を使用して、例外の動作を決定します。
int CheckDelayException(int exception_value)
{
if (exception_value == VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND) ||
exception_value == VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND))
{
// This example just executes the handler.
return EXCEPTION_EXECUTE_HANDLER;
}
// Don't attempt to handle other errors
return EXCEPTION_CONTINUE_SEARCH;
}
bool TryDelayLoadAllImports(LPCSTR szDll)
{
__try
{
HRESULT hr = __HrLoadAllImportsForDll(szDll);
if (FAILED(hr))
{
// printf_s("Failed to delay load functions from %s\n", szDll);
return false;
}
}
__except (CheckDelayException(GetExceptionCode()))
{
// printf_s("Delay load exception for %s\n", szDll);
return false;
}
// printf_s("Delay load completed for %s\n", szDll);
return true;
}
TryDelayLoadAllImports
の結果を使用して、インポート関数を呼び出すかどうかを制御できます。
エラー処理と通知
プログラムで遅延読み込みが行われた DLL を使用する場合は、エラーを確実に処理する必要があります。 プログラムの実行中にエラーが発生すると、ハンドルされない例外が発生します。 DLL 遅延読み込みのエラー処理と通知の詳細については、「エラー処理と通知」を参照してください。
遅延読み込みしたインポートのダンプ
遅延読み込みが行われたインポートは、DUMPBIN /IMPORTS
を使用してダンプできます。 それらのインポートは、標準のインポートとは少し異なる情報と共に表示されます。 それらは、/imports
リストの独自のセクションに分離され、遅延読み込みのインポートとして明示的にラベル付けされます。 イメージにアンロード情報が含まれている場合は、それが示されます。 バインド情報が存在する場合は、インポートのバインドされたアドレスと共に、ターゲット DLL の時刻と日付のスタンプが示されます。
DLL の遅延読み込みの制約
DLL インポートの遅延読み込みには、いくつかの制約があります。
データのインポートはサポートされません。 その回避策として、
LoadLibrary
(または遅延読み込みヘルパーで DLL が読み込まれたことが確認された後はGetModuleHandle
) およびGetProcAddress
を使用してデータ インポートを自分で明示的に処理します。Kernel32.dll
の遅延読み込みはサポートされていません。 遅延読み込みヘルパー ルーチンを機能させるには、この DLL が読み込まれる必要があります。転送されたエントリ ポイントのバインドはサポートされていません。
DLL が起動時に読み込まれるのではなく、遅延読み込みが行われる場合は、プロセスの動作が異なることがあります。 これは、遅延読み込みが行われた DLL のエントリ ポイントで発生するプロセスごとの初期化がある場合に見られることがあります。 ほかにも、DLL が
LoadLibrary
を通して読み込まれた場合、__declspec(thread)
を使用して宣言する静的 TLS (スレッド ローカル ストレージ) が処理されません。TlsAlloc
、TlsFree
、TlsGetValue
、およびTlsSetValue
を使用した動的 TLS は、静的または遅延読み込み DLL で引き続き利用可能です。各関数の最初の呼び出しの後に、インポートされた関数への静的グローバル関数ポインターを再初期化します。 これが必要なのは、関数ポインターの最初の使用は、読み込まれた関数ではなくサンクを指しているためです。
現時点では、通常のインポート メカニズムを使用しながら、DLL の特定の手順の読み込みだけを遅延させる方法はありません。
カスタム呼び出し規約 (x86 アーキテクチャでの条件コードの使用など) はサポートされません。 また、浮動小数点レジスタはどのプラットフォームにも保存されません。 カスタム ヘルパー ルーチンまたはフック ルーチンで浮動小数点型を使用する場合は注意してください。そのルーチンで、レジスタ呼び出し規則を浮動小数点パラメーターと共に使用するマシンの完全な浮動小数点状態を保存して復元する必要があります。 ヘルプ関数内の数値データ プロセッサ (NDP) スタックで、浮動小数点パラメーターを取得する CRT 関数を呼び出す場合、CRT DLL の遅延呼び出しに特に注意してください。
遅延読み込みヘルパー関数について
実行時に DLL を実際に読み込むのは、リンカーでサポートされる遅延読み込みのヘルパー関数です。 ヘルパー関数を変更して、その動作をカスタマイズできます。 delayimp.lib
で指定されたヘルパー関数を使用する代わりに、独自の関数を記述し、プログラムにリンクします。 1 つのヘルパー関数が、遅延読み込みが行われたすべての DLL に対して機能します。 詳細については、「遅延読み込みヘルパー関数について」と独自のヘルパー関数の開発に関する記事を参照してください。