Windows DLL名前解決の仕組み - 検索順序とSxS

· 更新日: · · Windows, DLL, ローダー, セキュリティ, Windows開発

更新履歴(5件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
C#のプラグイン読み込みの例で、専用フォルダに置いたDLLを`ApplicationDirectory`と`System32`の検索フラグで拾おうとしていたのを直しました。前者はexeのあるフォルダ、後者はOSのフォルダを指すので、プラグインフォルダは見ません。フルパスで指定する形にし、`DllImportSearchPath`には任意のフォルダを指す値が無いこと、依存DLLまでは固定されないことを補足しました。
最小のコード例をC/C++とC#で追加しました(`SetDefaultDllDirectories`から`RemoveDllDirectory`までを通し、C#は属性の宣言だけでなく実際の呼び出しまで示しています)。どのDLLがどこから読まれたかを確認する章を新設し、冒頭に用語表、検索順序を12行の表として整理しました。
日本語記事なのに英語のままだった「関連記事」「参考リンク」の見出しを日本語に直し、関連記事リンクの文言をリンク先の現在のタイトルに揃えました。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589732)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Windows DLL名前解決の仕組み - 検索順序とSxS」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589732 https://staging.comcomponent.com/blog/2026/03/24/002-windows-dll-name-resolution/

DOI(最新版)
10.5281/zenodo.21589732
DOI(この版)
10.5281/zenodo.21732747

Windows でネイティブ DLL を扱う話になると、かなりの頻度で次のような混乱が起きます。

  • LoadLibrary("foo.dll") と書いたら、実際にはどこを見に行くのか
  • 実行ファイルと同じフォルダに置いたのに、なぜ別の DLL が読み込まれるのか
  • System32 が優先されるのか、アプリフォルダが優先されるのか
  • manifest や API set や Known DLLs は、どの段階で効くのか
  • SetDllDirectoryAddDllDirectory を使うと、何が変わるのか
  • DLL 植え込み攻撃や DLL hijacking は、何をすると起きやすいのか

この話は、単に「検索順序を 1 行で覚える」だけでは実務で役に立ちません。 実際には、Windows のローダーは、ファイルシステムを順番に舐める前に、いくつかの特別ルールを先に評価します。

この記事では、Windows での DLL の名前解決を、unpackaged app と packaged app の違い、Known DLLs、loaded-module list、API set、side-by-side manifest、LoadLibraryEx 系 API の影響 まで含めて、実務向けに整理します。 内容は 2026 年 3 月時点 で Microsoft Learn の公開情報を前提にしています。123456789

この記事で使う用語

以降で説明なしに出てくる言葉を、先に一行ずつ整理しておきます。詳しい話は本文の各章で扱います。

用語 ひとことで言うと
packaged app / unpackaged app MSIX などのパッケージとして配布・インストールされるアプリか、従来どおりフォルダに実行ファイルを置く形のアプリか。検索順序の定義そのものが別です (3 章・4 章)
safe DLL search mode 既定で有効な設定で、current folder を検索順序の後ろへ移します。レジストリ値 SafeDllSearchMode を 0 にすると無効になります1
DLL redirection 実行ファイルと同じ場所に アプリ名.exe.local という目印を置くと、ローダーが実行ファイルのフォルダを先に見るようになる仕組み。.local、DotLocal とも呼ばれます (7 章)7
SxS (side-by-side) 同じ DLL の複数バージョンを共存させ、どのバージョンへ結び付けるかを manifest に書いて指定する仕組み。side-by-side の略で、日本語では「並置」と説明されることもあります (7 章)9
loaded-module list 同じモジュール名の DLL がすでにそのプロセスのメモリに読み込まれていないか、をシステムが確認する仕組み。どのフォルダから読まれたかに関係なく、読み込み済みならそれが使われます (5.1)1
Known DLLs Windows がそのバージョンで既知とみなす DLL の一覧。該当する DLL は、システム側のコピーが使われます (5.2)1
API set api-ms-win-... のような契約名。実装している物理 DLL を隠す仮想エイリアスです (6 章)3
package dependency graph アプリのパッケージ本体と、パッケージ マニフェストの Dependencies セクションに PackageDependency として書かれた依存パッケージの集合。マニフェストに書かれた順に検索されます1

1. まず結論

先に実務向けの結論だけ並べておきます。

  • Windows の DLL 名前解決は、「まずファイルシステム検索」ではありません。DLL redirection、API set、SxS manifest、loaded-module list、Known DLLs といった要素が、検索順序の前段に入ります。1
  • unpackaged app で safe DLL search mode が有効な標準形では、アプリケーションフォルダは上位 にありますが、その前に上記の特別ルールが評価されます。1
  • DLL をフルパス指定でロードしても、その DLL の依存 DLL までは自動で同じフルパス固定になるわけではありません。依存 DLL はモジュール名だけで検索される扱いになるため、別の場所から解決されることがあります。1
  • Known DLLs は、OS が既知の特定 DLL をシステム側のコピーへ結び付ける仕組みで、通常のアプリ側配置で上書きする話ではありません。1
  • API set は「実体 DLL 名そのもの」ではなく、実装 DLL を隠蔽する仮想エイリアス です。api-ms-win-... のような名前を見て、通常の DLL 検索と同じ感覚で考えると誤解しやすいです。3
  • SetDllDirectory は検索順序を変えるだけでなく、safe DLL search mode を実質的に無効化する 振る舞いを持つため、安易に使うとセキュリティ面で逆効果になることがあります。1
  • 実務では、フルパス指定、SetDefaultDllDirectoriesAddDllDirectoryLoadLibraryExLOAD_LIBRARY_SEARCH_* フラグ を組み合わせて、検索範囲を明示的に狭めるのが安全です。4562

要するに、Windows の DLL 名前解決は「どのフォルダが何番目か」だけではなく、「名前を何に解決する前段ルールがあるか」と「API で検索空間をどう変えたか」で決まる と考えるのが実務的です。

この記事の知識マップ

この記事は、WindowsのDLL名前解決が単なるフォルダ探索順ではなく、DLL redirection・API set・SxSマニフェスト・loaded-module list・Known DLLsという前段ルールがファイルシステム探索より先に評価される仕組みであることを整理する。packaged appとunpackaged appでは検索順序の定義自体が異なり、current folderの位置はsafe DLL search modeの有効・無効で変わる。SetDllDirectoryはsafe DLL search modeを実質的に無効化するため、SetDefaultDllDirectoriesとLoadLibraryExの検索フラグ、AddDllDirectoryを組み合わせて検索空間を絞ることがDLLハイジャックの軽減につながる。フルパス指定でも依存DLLまでは自動固定されず、実際の読み込み元はProcess MonitorやListDLLsで確認する。

Windows DLL名前解決の知識マップDLL redirection・API set・SxSマニフェスト・loaded-module list・Known DLLs・package dependency graphがファイルシステム探索より前段で評価されること、packaged appとunpackaged appで検索順序の定義自体が異なること、SetDllDirectoryがsafe DLL search modeを弱める一方でSetDefaultDllDirectoriesとLoadLibraryExの検索フラグが検索空間を絞りDLLハイジャックを軽減すること、実際の読み込み元をProcess Monitor等で確認する手段の関係を示す図利用する利用する利用する利用する利用する利用するで構成できる両立しない利用する両立しない軽減する前提とする利用する用いるのは非推奨推奨される対応軽減する原因になり得る軽減する用いるのは非推奨利用するで確認できるで確認できるで確認できるDLLの検索順序DLLハイジャッキング(DLL preloading攻撃)DLLリダイレクション(.local)API setSxSマニフェストリダイレクションloaded-module listKnown DLLspackage dependency graphcurrent folderの検索safe DLL search modepackaged appunpackaged appSetDllDirectorySetDefaultDllDirectoriesAddDllDirectoryLoadLibraryExDLL検索空間の絞り込み裸の名前によるLoadLibraryフルパス指定でのDLLロード依存DLLの解決DllImportSearchPath(.NET)Process Monitor(procmon.exe)ListDLLstasklist /m

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全23件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. DLL の名前解決は「フォルダ探索」の前に前段ルールがある

Microsoft Learn の DLL search order の説明では、DLL のロード時にはまず次のような要素が検索順序の一部として扱われます。1

  1. DLL redirection
  2. API sets
  3. SxS manifest redirection
  4. loaded-module list
  5. Known DLLs

その後で、app folder、System32、Windows フォルダ、PATH などのファイルシステム上の探索に入ります。1

ここを見落とすと、たとえば「アプリフォルダより先に何かが決まるのはおかしい」と感じますが、Windows ローダーの説明としてはむしろそちらが本筋です。

DLL 名を解決したいDLL redirectionAPI setSxS manifest redirectionloaded-module listKnown DLLsファイルシステム上の検索順序実際にロードされる DLL が決まる

3. unpackaged app の標準検索順序

いわゆる普通のデスクトップアプリで、DLL をフルパス指定せずにロードする場合、Microsoft Learn では unpackaged app の標準検索順序が説明されています。safe DLL search mode が有効な既定状態では、次の順です。1

# 探す場所 種類 補足
1 DLL redirection 前段ルール アプリ名.exe.local があるかどうか (7 章)
2 API sets 前段ルール 契約名から実装 DLL へ (6 章)
3 SxS manifest redirection 前段ルール manifest による binding (7 章)
4 loaded-module list 前段ルール 同名モジュールがロード済みかどうか (5.1)
5 Known DLLs 前段ルール 既知 DLL ならシステム側のコピー (5.2)
6 package dependency graph 前段ルール Windows 11 バージョン 21H2 以降。マニフェストに書かれた順に検索されます
7 アプリケーションがロードされたフォルダ ファイルシステム ここからが実際のフォルダ探索です
8 システムフォルダ ファイルシステム 通常は %SystemRoot%\System32GetSystemDirectory で取得できる場所です
9 16-bit system folder ファイルシステム 16 ビット時代の System フォルダ。パスを取得する関数は存在しませんが、検索はされます。現代のアプリで意識する場面はまずなく、「順序の項目としては残っている」とだけ知っていれば十分です
10 Windows フォルダ ファイルシステム GetWindowsDirectory で取得できる場所です
11 current folder ファイルシステム safe DLL search mode が無効だと、ここが 8 の位置まで繰り上がります
12 PATH に並んだディレクトリ ファイルシステム App Paths レジストリキーのアプリ別パスは含まれません

この表は暗記するためのものではありません。 自分のケースがどの段で決まっているのかを当てるための表です。1〜6 で決まっている問題を、7 以降のフォルダ配置で直そうとすると何をやっても変わりません。

実務で特に効いてくるのはこの 3 点です。

  • current folder は既定ではかなり後ろ です。safe DLL search mode により、current folder を前に出しにくくしています。1
  • ただし 後ろにあるから安全という意味ではありません。攻撃者が支配できるディレクトリが検索対象に残っている時点で、DLL preloading の余地は残ります。2
  • Windows 11 21H2 以降では、unpackaged app の検索説明にも package dependency graph が入っています。古い説明だけを覚えていると見落としやすい差分です。1

4. packaged app と unpackaged app は同じではない

Microsoft Learn では、packaged app については別の検索順序が定義されています。packaged app では package dependency graph がより前段で効き、検索の考え方自体が少し違います。1

この差を見落とすと、MSIX 化や Windows App SDK 導入後にこんな混乱が起きます。

  • 開発中の unpackaged 実行では見つかる DLL が、本番 package では見つからない
  • package manifest による依存関係と、昔ながらの PATH 依存が混ざって再現条件が変わる
  • 「Windows の DLL 検索順序はこう」と単一の表だけで説明してしまい、packaged app の挙動差を落とす

記事や設計レビューでは、「packaged app の話か、unpackaged app の話か」 を最初に分けるのが安全です。1

5. Known DLLs と loaded-module list は何をしているのか

DLL の解決で直感に反しやすいのが、loaded-module listKnown DLLs です。

5.1 loaded-module list

Microsoft Learn では、同じモジュール名の DLL がすでにメモリにロードされているか をシステムが確認できると説明されています。1

つまり、ファイルシステム検索の前に、

  • その DLL 名は、すでにロード済みではないか
  • その結果として、今から探しに行く必要自体があるのか

という判定が入ります。

そのため、調査中に「このプロセスでは別フォルダの同名 DLL が先にロード済みだった」という事実を落とすと、再現条件を読み違えます。

5.2 Known DLLs

Known DLLs は、Windows がそのバージョンで既知とみなす DLL の一覧で、HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs で確認できます。該当する DLL なら、システムはその既知 DLL のコピーを使います。1

ここで大事なのは、Known DLLs は「一般アプリが同名 DLL をアプリフォルダへ置けば勝てる」種類の話ではない という点です。 System32 との単純な先着争いとして理解すると、挙動を誤解します。

6. API set は「実体 DLL 名」ではなく契約名

api-ms-win-core-... のような名前を見ると、つい「その DLL ファイルをどこから探すのか」と考えがちです。ですが Microsoft Learn では、API set は物理 DLL への仮想エイリアスであり、実装と契約を分離する仕組みだと説明されています。3

つまり、

  • API set 名 = そのまま物理 DLL ファイル名
  • API set の解決 = 通常 DLL と同じファイル探索

と考えるのは不正確です。

API set の考え方を入れておくと、

  • Windows のバージョンやデバイス種別で実装 DLL 名が違っても整合する
  • 呼び出し側は「どのホスト DLL が実装しているか」を固定で知らなくてよい

という説明がしやすくなります。3

7. manifest と side-by-side (SxS) は DLL versioning 問題への別解

DLL redirection や SxS manifest は、単なる検索順序の小技ではなく、DLL versioning の衝突を避けるための仕組み として説明されています。789

Microsoft Learn では、

  • manifest は side-by-side assembly や isolated application を記述する XML
  • side-by-side assembly は命名、binding、versioning、deployment の単位
  • manifest に記された依存関係で、どのバージョンへ bind するかをローダーが判断する

という整理です。89

そのため、実務ではこの 3 つを分けて考える必要があります。

  • 単に private DLL を app folder に置く話
  • .local などの DLL redirection を使う話
  • manifest による side-by-side binding を使う話

どれも「DLL の解決に影響する」点では近いですが、設計意図は同じではありません。 Microsoft Learn は、既存のアプリに手を入れずに解決したいなら DLL redirection、新しく作るアプリなら side-by-side コンポーネント、という使い分けを案内しています。7

7.1 .local は具体的に何をしているのか

1 行で流されがちなので、unpackaged app での挙動を分けて書いておきます。7

  • 置くもの: リダイレクトファイルの名前は 実行ファイル名.local です。Editor.exe なら Editor.exe.local を、実行ファイルと同じフォルダに置きます。読み込ませたい DLL も同じフォルダに置きます
  • 中身: ファイルの中身は無視されます。 存在すること自体が、DLL をロードするときに実行ファイルのフォルダを先に見させる合図になります
  • 効く範囲: フルパス指定のロードにも、モジュール名だけのロードにも効きます。LoadLibraryLoadLibraryEx に渡したパスに関係なく、実行ファイルのフォルダに同名 DLL があればそちらが読まれます。COM のように登録先が 1 つしかない場面を救うための仕様です
  • 見つからなかったとき: 実行ファイルのフォルダに無ければ、通常の検索順序に戻ります
  • フォルダ形式: Editor.exe.local というフォルダを作り、その中に DLL を置く形でも動きます
  • マシン全体で有効にする: HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution OptionsDevOverrideEnable という DWORD 値を作って 1 にし、再起動します。これを設定すると、アプリが application manifest を持っていても .local によるリダイレクトが効くようになります
  • packaged app の場合: 置き場所が変わり、<パッケージのインストール先>\microsoft.system.package.metadata\application.local\ を見にいきます

ここで 1 つ、見落とすと調査が迷子になる副作用があります。 DLL redirection を使っていて、かつアプリが検索順序上のドライブやディレクトリすべてにアクセスできない場合、LoadLibrary はアクセスを拒否された時点で検索を打ち切ります。 DLL redirection を使っていなければ、アクセスできないディレクトリは飛ばして検索が続きます。7 .local を置いた環境だけ「その先にあるはずの DLL が見つからない」と言われたら、この違いを疑ってください。

8. LoadLibraryExSetDllDirectoryAddDllDirectory で何が変わるか

8.1 SetDllDirectory

SetDllDirectory は検索順序を変えますが、Microsoft Learn では safe DLL search mode を実質的に無効化する と明記されています。1

つまり、「アプリ専用フォルダを 1 つ足したいだけ」のつもりで使っても、結果として current folder の扱いなどを含めた検索空間が変わります。

さらに、親プロセスで SetDllDirectory を呼ぶと、その影響が子プロセス側の標準検索順序にも及ぶことがあります。1

このため、実務では SetDllDirectory を雑に常用するより、

  • SetDefaultDllDirectories
  • AddDllDirectory
  • LoadLibraryExLOAD_LIBRARY_SEARCH_*

に寄せる方が安全です。456

8.2 AddDllDirectory

AddDllDirectory で追加したパスは、LOAD_LIBRARY_SEARCH_USER_DIRS と組み合わせて使います。 Microsoft Learn では、複数追加した場合の検索順序は 未規定 です。15

なので、

  • 複数ディレクトリを追加した
  • その探索順まで厳密に期待した

という設計は避けた方がよいです。

8.3 SetDefaultDllDirectories

SetDefaultDllDirectories は、標準の DLL search path から脆弱になりやすいディレクトリを外し、検索対象を限定するための API として説明されています。4

特に押さえておきたい性質はこの 3 つです。

  • プロセス単位で効く
  • 呼び出し後はプロセスの寿命中継続する
  • 一度設定した標準検索パスを、そのまま元の標準形へ戻すことはできない

セキュリティ面を考えるなら、「起動直後に安全寄りの検索空間へ寄せる」設計が取りやすい API です。4

8.4 LoadLibraryEx

LoadLibraryEx は、LOAD_WITH_ALTERED_SEARCH_PATHLOAD_LIBRARY_SEARCH_* フラグで検索挙動を変えられます。61

実務的には、

  • 依存 DLL を含めて、ロード元 DLL のフォルダも探索対象にしたい
  • アプリフォルダ、System32、明示追加したユーザーディレクトリだけに絞りたい

といった要求に対応しやすい API です。

書く前に押さえておきたい制約が 4 つあります。6

制約 内容
第 2 引数 hFile は将来のための予約で、必ず NULL を渡します
併用不可 LOAD_WITH_ALTERED_SEARCH_PATH は、どの LOAD_LIBRARY_SEARCH_* フラグとも組み合わせられません
フルパス必須 LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR を使うなら、第 1 引数はフルパスでなければなりません
複数指定時の順序 LOAD_LIBRARY_SEARCH_DLL_LOAD_DIRLOAD_LIBRARY_SEARCH_APPLICATION_DIRLOAD_LIBRARY_SEARCH_USER_DIRSLOAD_LIBRARY_SEARCH_SYSTEM32 の順に探されます。ただし USER_DIRS の中での順序は未規定です

LOAD_LIBRARY_SEARCH_* フラグを 1 つでも指定すると、標準検索パスは一切使われません。 つまり LOAD_LIBRARY_SEARCH_SYSTEM32 だけを渡した場合、アプリフォルダは探されません。「絞る」とはそういう意味です。

8.5 最小のコード例 (C/C++)

ここまでの API を 1 つにまとめると、次の形になります。 SetDefaultDllDirectoriesLOAD_LIBRARY_SEARCH_*Windows 8 以降の API なので、ヘッダーの対象バージョンを明示する必要があります。46

/* cl /W4 loader.c  (Visual Studio 2022 + Windows SDK 10)
 * SetDefaultDllDirectories / AddDllDirectory / LOAD_LIBRARY_SEARCH_* は
 * Windows 8 以降。Windows 7 も対象にするなら KB2533623 が前提になり、
 * GetProcAddress で Kernel32.dll から実行時に取得する形が必要です。 */
#define _WIN32_WINNT 0x0602   /* Windows 8 */
#include <windows.h>
#include <stdio.h>

int wmain(void)
{
    /* 1. プロセス既定の検索空間を安全側へ寄せる。
     *    current folder と PATH を検索対象から外すのが目的です。
     *    LOAD_LIBRARY_SEARCH_DEFAULT_DIRS は
     *    APPLICATION_DIR + SYSTEM32 + USER_DIRS の組み合わせです。
     *    USER_DIRS を含めておかないと、手順 2 の AddDllDirectory は
     *    プロセス既定には反映されません。 */
    if (!SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)) {
        wprintf(L"SetDefaultDllDirectories failed: %lu\n", GetLastError());
        return 1;
    }

    /* 2. 自前のプラグインフォルダだけを明示的に足す。
     *    AddDllDirectory に渡すのは絶対パスでなければなりません。 */
    const wchar_t *pluginDir = L"C:\\MyApp\\plugins";
    DLL_DIRECTORY_COOKIE cookie = AddDllDirectory(pluginDir);
    if (cookie == NULL) {
        wprintf(L"AddDllDirectory failed: %lu\n", GetLastError());
        return 1;
    }

    /* 3. ロードする。
     *    第 2 引数 hFile は予約済みなので必ず NULL。
     *    ここでフラグを渡すと、手順 1 のプロセス既定ではなく
     *    このフラグの組み合わせだけが使われます。
     *    APPLICATION_DIR を外しているので、アプリフォルダは探されません。 */
    HMODULE h = LoadLibraryExW(
        L"foo.dll",
        NULL,
        LOAD_LIBRARY_SEARCH_USER_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32);
    if (h == NULL) {
        wprintf(L"LoadLibraryExW failed: %lu\n", GetLastError());
        RemoveDllDirectory(cookie);
        return 1;
    }

    /* 4. どこから読まれたかを必ず確認する。
     *    調査では「読めたかどうか」より「どこから読めたか」が重要です。 */
    {
        wchar_t loadedPath[MAX_PATH];
        DWORD cap = (DWORD)(sizeof(loadedPath) / sizeof(loadedPath[0]));
        DWORD len = GetModuleFileNameW(h, loadedPath, cap);
        if (len > 0 && len < cap) {
            wprintf(L"loaded from: %s\n", loadedPath);
        } else {
            wprintf(L"GetModuleFileNameW failed: %lu\n", GetLastError());
        }
    }

    FreeLibrary(h);
    RemoveDllDirectory(cookie);
    return 0;
}

このコードで押さえている点は 4 つです。

  1. SetDefaultDllDirectories を先に呼ぶ。 一度呼ぶとプロセスの寿命中ずっと効き、標準の検索パスへ戻すことはできません4
  2. AddDllDirectoryLOAD_LIBRARY_SEARCH_USER_DIRS とセットで初めて意味を持つ。 SetDefaultDllDirectoriesUSER_DIRS を含めていない場合、追加したディレクトリは LOAD_LIBRARY_SEARCH_USER_DIRS を指定した LoadLibraryEx の呼び出しでしか使われません5
  3. 後始末をする。 AddDllDirectory が返した cookie は RemoveDllDirectory に渡して外せます5
  4. 読めた場所を出力する。 GetModuleFileNameW の 1 行があるかどうかで、障害時の調査時間が変わります

なお foo.dll が依存する DLL も、この LOAD_LIBRARY_SEARCH_* の範囲で探されます。 foo.dll 自身のフォルダを依存 DLL の探索先にしたい場合は、第 1 引数をフルパスにしたうえで LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR を足します。

8.6 C# から使う場合

.NET でも同じ考え方が使えます。P/Invoke の検索パスは DefaultDllImportSearchPaths 属性で、明示ロードは NativeLibrary.Load で制御します。

// .NET 8 / C# 12
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;

// アセンブリ内のすべての P/Invoke について、既定の検索先を System32 に限定する。
// 制約: 絶対パスを指定した P/Invoke には、この属性は適用されません。
//       また Windows 以外のプラットフォームでは効果がありません。
[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]

internal static class Program
{
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern uint GetTickCount();

    private static void Main()
    {
        // 属性は宣言しただけでは何も起きません。呼び出して初めて効きます。
        Console.WriteLine($"GetTickCount = {GetTickCount()}");

        // プラグインは専用フォルダに置いてある。ここで
        //   NativeLibrary.Load("foo.dll", asm, ApplicationDirectory | System32)
        // と書いてはいけません。ApplicationDirectory が指すのは exe のあるフォルダ、
        // System32 が指すのは OS のフォルダで、どちらもプラグインフォルダを見ません。
        // 読み込みに失敗するか、たまたま exe の隣にあった別の foo.dll を掴みます。
        // 置き場所が分かっているものは、フルパスで指定するのが確実です。
        string pluginDir = Path.Combine(AppContext.BaseDirectory, "plugins");
        string pluginPath = Path.GetFullPath(Path.Combine(pluginDir, "foo.dll"));
        if (!File.Exists(pluginPath))
        {
            throw new FileNotFoundException("プラグインが見つかりません。", pluginPath);
        }

        // パスを渡すオーバーロードは、そのファイルを直接読みます(検索しません)。
        IntPtr handle = NativeLibrary.Load(pluginPath);

        try
        {
            // どこから読まれたかを確認する。
            foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
            {
                if (string.Equals(module.ModuleName, "foo.dll", StringComparison.OrdinalIgnoreCase))
                {
                    Console.WriteLine($"loaded from: {module.FileName}");
                }
            }
        }
        finally
        {
            NativeLibrary.Free(handle);
        }
    }
}

DllImportSearchPath の値は、LOAD_LIBRARY_SEARCH_* フラグと対応しています。 そのため、C# 側で「System32 だけ」と書いた場合も、8.4 の制約がそのまま当てはまります。アプリケーションディレクトリは探されません。

ここで注意したいのが、DllImportSearchPath には「任意のフォルダ」を指す値がないことです。ApplicationDirectory は exe のあるフォルダ、System32 は OS のフォルダを指すもので、プラグイン用に自分で決めたフォルダを表す値はありません。だから専用フォルダに置いたものを検索フラグで拾おうとしても届かず、上のようにフルパスで指定することになります。

なお、フルパスで読んでも 9 章のとおり foo.dll が依存する DLL までは固定されません。プラグインが独自の依存 DLL を連れてくるなら、AddDllDirectory でそのフォルダを足したうえで LOAD_LIBRARY_SEARCH_USER_DIRS を効かせるか、依存関係ごと 1 つのフォルダに収めて LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR を使う、という C 側の作法がそのまま必要になります。

9. フルパス指定しても依存 DLL までは固定されない

このテーマで実務上かなり重要なのに見落とされやすいのがここです。 Microsoft Learn では、最初の DLL をフルパス指定でロードしても、その DLL の依存 DLL はモジュール名だけで検索される と説明されています。14

つまり、

  • C:\\MyApp\\plugins\\foo.dll を明示ロードした
  • だから foo.dll が依存する bar.dll も同じフォルダから必ず取られる

とは限りません。

この誤解があると、

  • 開発環境では動く
  • 配布先では別の bar.dll が解決される
  • 依存 DLL の衝突が再現環境依存になる

という、少し厄介な障害になります。

10. DLL preloading / hijacking を避けるには

Microsoft Learn の DLL security では、フルパスなしの動的ロードと、攻撃者が支配できる検索対象ディレクトリの組み合わせが、DLL preloading attack や binary planting attack につながると説明されています。2

実務では、この基本形に寄せると考えやすいです。

  • LoadLibrary("foo.dll") のような裸の名前ロードを減らす
  • 必要ならフルパス指定を使う
  • プロセス既定の検索パスを SetDefaultDllDirectories で絞る
  • 明示的に許可したディレクトリだけ AddDllDirectory で追加する
  • LOAD_LIBRARY_SEARCH_SYSTEM32LOAD_LIBRARY_SEARCH_APPLICATION_DIRLOAD_LIBRARY_SEARCH_USER_DIRS などを使って探索範囲を明示する
  • current folder や不用意な PATH 依存を避ける

特に、管理者権限で動くプロセスが曖昧な検索パスを持つ 状況は危険です。Microsoft Learn でも、悪意ある DLL がロードされると、その DLL はそのプロセスの権限で実行されると説明されています。2

11. どの DLL がどこから読まれたかを確認する

ここまでは仕様の話でした。実際の調査では、推測せずに確認するほうが早いです。 目的別に 4 つ挙げます。

知りたいこと 使うもの 見る場所
どこを探して、どこで見つけたか Process Monitor10 ファイルアクセスの記録
いま何がロードされているか Process Explorer、tasklist /m11 プロセスのモジュール一覧
ある DLL を、どのプロセスが掴んでいるか ListDLLs12 プロセス横断の一覧
デバッグ中に実際に読まれたパス Visual Studio のモジュールウィンドウ 「デバッグ」>「ウィンドウ」>「モジュール」7

11.1 Process Monitor で探索の足跡を追う

いちばん情報量が多いのがこれです。手順はこうなります。10

  1. Process Monitor を管理者として起動し、記録を開始する
  2. 「Filter」>「Filter…」を開き、Process Name is に対象の exe 名を入れて追加する
  3. 同じ画面で Path ends with .dll を追加する
  4. 対象のアプリを起動し、問題が起きたところで記録を停止する

このとき見るのは Result 列 です。 ローダーは検索順序に沿って上から順に開こうとするので、見つからなかった場所には NAME NOT FOUND、実際に開けた場所には SUCCESS が並びます。 つまり、NAME NOT FOUND が並んだ最後の次の行が、実際に採用されたパスになります。

3 章の表と突き合わせると、そのケースがどの段で決まったのかが分かります。 前段ルール (表の 1〜6) で決まっている場合は、そもそもファイルを探しに行った記録が出ません。これも重要な手がかりです。

11.2 ロード済みモジュールを一覧する

すでに起動しているプロセスについて、何がどこから読まれているかを見るだけなら、追加ツールなしでも確認できます。

tasklist /m foo.dll

このコマンドは、指定したモジュールを読み込んでいるプロセスの名前と PID を一覧します。11 どのプロセスが対象かを絞ってから、Process Explorer の下部ペインを DLL 表示に切り替えると、そのプロセスが読んでいる DLL のフルパスまで確認できます。

Sysinternals の ListDLLs を使うと、同じことをコマンドラインで、しかもプロセス横断で確認できます。12

5.1 の loaded-module list が効いているかどうかは、この方法でしか分かりません。 同名の DLL が別フォルダから先にロード済みなら、そのプロセスでは新しく探しに行かないためです。

12. 実務での判断チェックリスト

Windows で DLL ロード設計をレビューする時は、最低限、以下を確認すると事故が減ります。

  1. そのアプリは packaged app か unpackaged app か
  2. どの DLL が静的リンク由来で、どれが動的ロードか
  3. フルパス指定か、モジュール名だけか
  4. SetDllDirectory を使っていないか
  5. SetDefaultDllDirectoriesLOAD_LIBRARY_SEARCH_* を使える構成か
  6. AddDllDirectory を複数使っていて、順序依存を暗黙に期待していないか
  7. manifest / SxS / private DLL / redirection のどれで依存関係を管理しているか
  8. current folder や PATH にセキュリティ上の弱い前提がないか
  9. 依存 DLL が別環境で別の場所から解決されないか
  10. 症状が出ている環境で、実際にどこから読まれたかを確認したか (11 章)

この 10 点を分けて確認すると、「DLL が見つからない」「違う DLL が読まれた」「本番だけ起動しない」「脆弱性レビューで止まる」といった問題を、かなり前段で潰せます。

13. まとめ

Windows での DLL の名前解決は、単なる「フォルダの探索順」ではありません。 実際には、DLL redirection、API set、SxS manifest、loaded-module list、Known DLLs、そして API 呼び出しで変更される検索空間 が重なって決まります。134

実務で一番大事なのは、この 6 つに尽きます。

  • 検索順序を 1 枚の表として暗記しない。表は、自分のケースがどの段で決まったかを当てるために使う
  • packaged / unpackaged を分ける
  • フルパス指定でも依存 DLL は別扱いになりうると理解する
  • SetDllDirectory を安易に使わない
  • 安全側へ寄せるなら SetDefaultDllDirectoriesLoadLibraryEx の検索フラグを使う
  • 推測で終わらせず、Process Monitor などで実際に読まれたパスを確認する

DLL 名の解決は、起動障害、環境差異、セキュリティ問題がまとめて表面化しやすい場所です。 だからこそ、「Windows がどの順番で探すか」だけでなく、「Windows がそもそも何を名前解決の前提として扱っているか」まで理解しておく価値があります。

関連記事

参考リンク

  1. Microsoft Learn: Dynamic-link library search order
  2. Microsoft Learn: Dynamic-Link Library Security
  3. Microsoft Learn: Windows API sets
  4. Microsoft Learn: SetDefaultDllDirectories function
  5. Microsoft Learn: AddDllDirectory function
  6. Microsoft Learn: LoadLibraryEx function
  7. Microsoft Learn: Dynamic-link library redirection
  8. Microsoft Learn: Manifests
  9. Microsoft Learn: About Side-by-Side Assemblies
  10. Microsoft Learn: Process Monitor
  11. Microsoft Learn: ListDLLs
  12. Microsoft Learn: tasklist
  1. Microsoft Learn, Dynamic-link library search order, accessed March 24, 2026  2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

  2. Microsoft Learn, Dynamic-Link Library Security, accessed March 24, 2026  2 3 4 5

  3. Microsoft Learn, Windows API sets, accessed March 24, 2026  2 3 4 5 6

  4. Microsoft Learn, SetDefaultDllDirectories function, accessed March 24, 2026  2 3 4 5 6 7 8 9

  5. Microsoft Learn, AddDllDirectory function, accessed March 24, 2026  2 3 4 5 6

  6. Microsoft Learn, LoadLibraryEx function, accessed March 24, 2026  2 3 4 5 6

  7. Microsoft Learn, Dynamic-link library redirection, accessed March 24, 2026  2 3 4 5 6 7

  8. Microsoft Learn, Manifests, accessed March 24, 2026  2 3

  9. Microsoft Learn, About Side-by-Side Assemblies, accessed March 24, 2026  2 3 4

  10. Microsoft Learn, Process Monitor  2

  11. Microsoft Learn, tasklist  2

  12. Microsoft Learn, ListDLLs  2

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

LoadLibraryでDLL名を指定すると、Windowsはどの順番で探しますか?
ファイルシステムを順番に探す前に、前段ルールが評価されます。具体的には、DLL redirection、API set、SxS manifest redirection、loaded-module list、Known DLLsが先に効き、そのあとでアプリケーションフォルダ、System32、Windowsフォルダ、current folder、PATHといったファイルシステム上の探索に入ります。safe DLL search modeが有効な既定状態では、current folderはかなり後ろに置かれます。また、packaged appとunpackaged appでは検索順序の考え方自体が違います。
DLLをフルパスでロードすれば依存DLLも同じフォルダから読まれますか?
読まれるとは限りません。最初のDLLをフルパス指定でロードしても、そのDLLが依存するDLLはモジュール名だけで検索される扱いになるため、別の場所から解決されることがあります。この誤解があると、開発環境では動くのに配布先では別の依存DLLが解決され、環境依存の厄介な障害になります。依存DLLも含めて制御したい場合は、LoadLibraryExのLOAD_LIBRARY_SEARCH_*フラグなどで検索空間を明示します。
SetDllDirectoryを使ってはいけないのはなぜですか?
検索順序を変えるだけでなく、safe DLL search modeを実質的に無効化する振る舞いを持つためです。アプリ専用フォルダを1つ足したいだけのつもりでも、current folderの扱いを含めた検索空間全体が変わり、セキュリティ面で逆効果になることがあります。さらに、親プロセスで呼ぶと子プロセス側の検索順序にも影響が及ぶことがあります。代わりにSetDefaultDllDirectories、AddDllDirectory、LoadLibraryExの検索フラグに寄せるほうが安全です。
DLLハイジャック(DLL preloading攻撃)を防ぐにはどうすればよいですか?
フルパスなしの動的ロードと、攻撃者が支配できる検索対象ディレクトリの組み合わせが攻撃につながるため、両方を絞ります。具体的には、裸の名前によるLoadLibraryを減らし、必要ならフルパス指定を使い、SetDefaultDllDirectoriesでプロセス既定の検索パスを絞り、許可したディレクトリだけAddDllDirectoryで追加し、current folderや不用意なPATH依存を避けます。特に管理者権限で動くプロセスが曖昧な検索パスを持つ状況は危険で、悪意あるDLLがそのプロセスの権限で実行されます。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る