Share via


リンカーによる DLL の遅延読み込み

MSVC リンカーでは、DLL の遅延読み込みをサポートしています。 この機能により、DLL の遅延読み込みを実装するために、Windows SDK の関数 LoadLibraryGetProcAddress を使用する必要がなくなります。

遅延読み込みを行わない場合、実行時に DLL を読み込む唯一の方法は、LoadLibraryGetProcAddress を使用することです。オペレーティング システムでは、それが使用される実行可能ファイルまたは DLL が読み込まれるときに、DLL を読み込みます。

遅延読み込みを行う場合、DLL を暗黙的にリンクするときに、プログラムで DLL 内の関数が呼び出されるまでその DLL の読み込みを遅らせるオプションが、リンカーによって提供されます。

アプリケーションでは、/DELAYLOAD (遅延読み込みのインポート) リンカー オプションをヘルパー関数と共に使用することで、DLL の遅延読み込みを行うことができます。 (既定のヘルパー関数の実装は、Microsoft によって提供されます)。ヘルパー関数は、実行時に呼び出すことによって、必要に応じて DLL を読み込LoadLibraryGetProcAddressみます。

次の場合に、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 (スレッド ローカル ストレージ) が処理されません。 TlsAllocTlsFreeTlsGetValue、および TlsSetValue を使用した動的 TLS は、静的または遅延読み込み DLL で引き続き利用可能です。

  • 各関数の最初の呼び出しの後に、インポートされた関数への静的グローバル関数ポインターを再初期化します。 これが必要なのは、関数ポインターの最初の使用は、読み込まれた関数ではなくサンクを指しているためです。

  • 現時点では、通常のインポート メカニズムを使用しながら、DLL の特定の手順の読み込みだけを遅延させる方法はありません。

  • カスタム呼び出し規約 (x86 アーキテクチャでの条件コードの使用など) はサポートされません。 また、浮動小数点レジスタはどのプラットフォームにも保存されません。 カスタム ヘルパー ルーチンまたはフック ルーチンで浮動小数点型を使用する場合は注意してください。そのルーチンで、レジスタ呼び出し規則を浮動小数点パラメーターと共に使用するマシンの完全な浮動小数点状態を保存して復元する必要があります。 ヘルプ関数内の数値データ プロセッサ (NDP) スタックで、浮動小数点パラメーターを取得する CRT 関数を呼び出す場合、CRT DLL の遅延呼び出しに特に注意してください。

遅延読み込みヘルパー関数について

実行時に DLL を実際に読み込むのは、リンカーでサポートされる遅延読み込みのヘルパー関数です。 ヘルパー関数を変更して、その動作をカスタマイズできます。 delayimp.lib で指定されたヘルパー関数を使用する代わりに、独自の関数を記述し、プログラムにリンクします。 1 つのヘルパー関数が、遅延読み込みが行われたすべての DLL に対して機能します。 詳細については、「遅延読み込みヘルパー関数について」と独自のヘルパー関数の開発に関する記事を参照してください。

関連項目

Visual Studio での C/C++ Dll の作成
MSVC リンカーのリファレンス