How Windows DLL Name Resolution Works - Search Order and SxS

· Updated: · · Windows, DLL, Loader, Security, Windows Development

Revision history (1 updates, last updated Sep 1, 2026)

A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.

Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614574)
First published
Cite this article(DOI: 10.5281/zenodo.21614573)

This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.

Go Komura (2026). How Windows DLL Name Resolution Works - Search Order and SxS. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614573 https://comcomponent.com/en/blog/2026/03/24/002-windows-dll-name-resolution/

DOI (latest version)
10.5281/zenodo.21614573
DOI (this version)
10.5281/zenodo.22218892

Whenever native DLLs on Windows come up, confusion like the following arises with remarkable frequency.

  • When you write LoadLibrary("foo.dll"), where does it actually look?
  • You put the DLL in the same folder as the executable, so why is a different DLL being loaded?
  • Does System32 take precedence, or does the application folder?
  • At which stage do manifests, API sets, and Known DLLs take effect?
  • What changes when you use SetDllDirectory or AddDllDirectory?
  • What makes DLL planting attacks and DLL hijacking likely to happen?

Memorizing the search order as a one-liner is not enough for real work. In reality, the Windows loader evaluates several special rules before it ever starts walking the file system in order.

Why memorizing the search order is not enoughA diagram showing that memorizing the DLL search order as a single line does not help in real work, because the Windows loader evaluates several special rules before it walks the file system in order.Memorize the search order as one lineDoes not help in real workWhat the Windows loader really doesEvaluate the special rules firstThen walk the file system in order

Figure 1: Name resolution begins with pre-stage special rules, before any folder search.

In this article, we organize DLL name resolution on Windows for practical use, covering the differences between unpackaged and packaged apps, Known DLLs, the loaded-module list, API sets, side-by-side manifests, and the effects of the LoadLibraryEx family of APIs. The content is based on public information on Microsoft Learn as of March 2026.123456789

Terms used in this article

Here is a one-line summary of each term that appears below without further introduction. The details are covered in the body of the article.

Term In one line
packaged app / unpackaged app Whether the app is distributed and installed as a package such as MSIX, or ships the traditional way with an executable placed in a folder. The search order is defined separately for each (sections 3 and 4)
safe DLL search mode A setting that is enabled by default and moves the current folder toward the back of the search order. Setting the registry value SafeDllSearchMode to 0 disables it1
DLL redirection Placing a marker named AppName.exe.local next to the executable makes the loader look in the executable’s folder first. Also called .local or DotLocal (section 7)7
SxS (side-by-side) A mechanism for letting multiple versions of the same DLL coexist, with a manifest declaring which version to bind to. SxS is the abbreviation of side-by-side (section 7)9
loaded-module list The mechanism by which the system checks whether a DLL with the same module name is already loaded into that process’s memory. If it is, that module is used regardless of which folder it came from (5.1)1
Known DLLs The list of DLLs Windows treats as well known for that version. For a DLL on the list, the system-side copy is used (5.2)1
API set A contract name such as api-ms-win-.... It is a virtual alias that hides the physical DLL providing the implementation (section 6)3
package dependency graph The app’s own package plus the dependency packages declared as PackageDependency entries in the Dependencies section of the package manifest. They are searched in the order written in the manifest1

1. The Conclusion First

Here are the practical conclusions up front.

  • Windows DLL name resolution is not “file system search first.” Elements like DLL redirection, API sets, SxS manifests, the loaded-module list, and Known DLLs come before the search order proper.1
  • In the standard configuration for an unpackaged app with safe DLL search mode enabled, the application folder ranks high, but the special rules above are evaluated before it.1
  • Even if you load a DLL by full path, its dependent DLLs are not automatically pinned to the same full path. Dependent DLLs are searched by module name only, so they can be resolved from somewhere else.1
  • Known DLLs is a mechanism by which the OS binds certain well-known DLLs to the system-side copies; it is not something a normal app-side deployment overrides.1
  • An API set is not “the actual DLL name itself” but a virtual alias that hides the implementation DLL. If you see a name like api-ms-win-... and reason about it the way you would about a normal DLL search, you will likely misunderstand.3
  • SetDllDirectory does more than change the search order - it has the behavior of effectively disabling safe DLL search mode, so using it casually can backfire from a security standpoint.1
  • In practice, the safe approach is to combine full-path loading, SetDefaultDllDirectories, AddDllDirectory, and the LOAD_LIBRARY_SEARCH_* flags of LoadLibraryEx to narrow the search scope explicitly.4562

In short, the practical view is that Windows DLL name resolution is determined not just by “which folder comes in what order,” but by “which pre-stage rules resolve the name first” and “how the APIs have altered the search space.”

The three things that decide DLL name resolutionA diagram showing that Windows DLL name resolution is decided by the overlap of three things, not folder order alone: the pre-stage rules that decide what the name resolves to, the folder search order, and the search space as altered by API calls.Pre-stage rules for the nameThe DLL actually loadedFolder search orderSearch space altered by APIsredirection and Known DLLsLoadLibraryEx flags

Figure 2: The result comes from the overlap of pre-stage rules, folder order, and the APIs.

Knowledge map for this article

This article explains that DLL name resolution on Windows is not merely an order in which folders are searched, but a mechanism in which the earlier rules of DLL redirection, API sets, the SxS manifest, the loaded-module list, and Known DLLs are evaluated before the file system search. The search order itself is defined differently for a packaged app and an unpackaged app, and the position of the current folder changes depending on whether safe DLL search mode is enabled or disabled. Because SetDllDirectory effectively disables safe DLL search mode, combining SetDefaultDllDirectories, the LoadLibraryEx search flags, and AddDllDirectory to narrow the search space is what mitigates DLL hijacking. Specifying a full path does not automatically pin the dependent DLLs as well, so check where a module was actually loaded from with Process Monitor or ListDLLs.

Windows DLL name resolutionDiagram showing that DLL redirection, API sets, the SxS manifest, the loaded-module list, Known DLLs, and the package dependency graph are evaluated ahead of the file system search, that the search order itself is defined differently for a packaged app and an unpackaged app, that SetDllDirectory weakens safe DLL search mode while SetDefaultDllDirectories and the LoadLibraryEx search flags narrow the search space and mitigate DLL hijacking, and how all of this relates to the means of confirming the actual load source with tools such as Process Monitorusesusesusesusesusesusesconfigured byincompatible withusesincompatible withmitigatesrequiresusesnot recommended forrecommended formitigatesmay causemitigatesnot recommended forusesverified byverified byverified byDLL Search OrderDLL Hijacking (Binary Planting)DLL Redirection (.local)Windows API SetSxS Manifest RedirectionLoaded-Module ListKnown DLLsPackage Dependency GraphCurrent Directory Search StepSafe DLL Search ModePackaged AppUnpackaged AppSetDllDirectorySetDefaultDllDirectoriesAddDllDirectoryLoadLibraryExNarrowing the DLL Search PathLoadLibrary Without a Full PathFull-Path DLL LoadingDependent DLL ResolutionDllImportSearchPath (.NET)Process Monitor (procmon.exe)ListDLLs (Sysinternals)tasklist /m

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (23 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

According to the DLL search order documentation on Microsoft Learn, when a DLL is loaded, the following elements are treated as part of the search order first.1

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

Only after that does the loader proceed to the file system search across the app folder, System32, the Windows folder, PATH, and so on.1

If you miss this, it can feel “wrong” that something gets decided before the application folder - but as a description of the Windows loader, that is in fact the main path.

A DLL name needs resolvingDLL redirectionAPI setSxS manifest redirectionloaded-module listKnown DLLsSearch order on the file systemThe DLL actually loaded is determined

Figure 3: The pre-stage rules are evaluated in turn, and only when none of them match does the file system search begin.

3. The Standard Search Order for Unpackaged Apps

For an ordinary desktop app loading a DLL without a full path, Microsoft Learn describes the standard search order for unpackaged apps. In the default state with safe DLL search mode enabled, the order is as follows.1

# Where it looks Kind Notes
1 DLL redirection Pre-stage rule Whether an AppName.exe.local marker exists (section 7)
2 API sets Pre-stage rule From the contract name to the implementation DLL (section 6)
3 SxS manifest redirection Pre-stage rule Binding declared by a manifest (section 7)
4 loaded-module list Pre-stage rule Whether a module of the same name is already loaded (5.1)
5 Known DLLs Pre-stage rule For a well-known DLL, the system-side copy (5.2)
6 package dependency graph Pre-stage rule Windows 11 version 21H2 and later. Searched in the order written in the manifest
7 The folder the application was loaded from File system This is where the actual folder search starts
8 The system folder File system Normally %SystemRoot%\System32. The location returned by GetSystemDirectory
9 The 16-bit system folder File system The System folder from the 16-bit era. No function returns its path, but it is still searched. Modern apps essentially never have to think about it; knowing that it survives as an entry in the order is enough
10 The Windows folder File system The location returned by GetWindowsDirectory
11 The current folder File system With safe DLL search mode disabled, this moves up to position 8
12 The directories listed in PATH File system Per-application paths from the App Paths registry key are not included

This table is not something to memorize. It is a table for working out which stage decided your particular case. If the outcome is decided at stages 1 through 6, no amount of rearranging folders at stage 7 and beyond will change anything.

How to use the search order table correctlyA diagram showing that the search order table is not for memorizing but for working out which stage decided your case, and that trying to fix a problem decided by a pre-stage rule by rearranging folders changes nothing.Pre-stage rules (1-6)File system (7-12)Symptom: an unintended DLL is loadedWhich stage decided itMoving folders around changes nothingPlacement and paths are worth fixing

Figure 4: Before fixing anything, work out which stage decided the outcome.

Three points matter most in practice.

  • The current folder is quite far down by default. Safe DLL search mode makes it hard to move the current folder forward.1
  • However, being far down does not mean safe. As long as a directory the attacker can control remains in the search scope, room for DLL preloading remains.2
  • On Windows 11 21H2 and later, the package dependency graph appears in the unpackaged app search description as well. This is an easy delta to miss if you only remember the older description.1
Where the current folder sits and what that means for safetyA diagram showing that with safe DLL search mode enabled by default the current folder sits far down the search order, but that being far down does not make it safe, because room for DLL preloading remains as long as a directory the attacker can control is still in the search scope.safe DLL search mode enabled (default)The current folder sits far downFar down does not mean safeRoom remains if an attacker-controlled location is still searched

Figure 5: The current folder has been pushed down, but that alone is not a defense.

4. Packaged Apps and Unpackaged Apps Are Not the Same

Microsoft Learn defines a separate search order for packaged apps. In packaged apps, the package dependency graph takes effect at an earlier stage, and the search model itself is somewhat different.1

Miss this difference, and after moving to MSIX or adopting the Windows App SDK you get confusion like this:

  • A DLL found when running unpackaged during development is not found in the production package
  • Dependencies via the package manifest mix with old-style PATH dependence, and the reproduction conditions change
  • Explaining “the Windows DLL search order is such-and-such” with a single table, dropping the behavioral differences of packaged apps

In articles and design reviews, it is safest to first separate “is this about a packaged app or an unpackaged app?”1

Separate packaged from unpackaged firstA diagram showing that packaged apps have their own search order in which the package dependency graph takes effect earlier, that this causes confusion such as a DLL found during unpackaged development not being found in the production package, and that a design review should separate the two cases first.unpackaged apppackaged appWhich kind of app is this aboutThe standard search order in section 3A separate search order (dependency graph comes earlier)A source of confusion once MSIX changes the behavior seen in development

Figure 6: The search order is not one table; it forks first on packaged versus unpackaged.

5. What Known DLLs and the Loaded-Module List Are Doing

The parts of DLL resolution most likely to defy intuition are the loaded-module list and Known DLLs.

5.1 The loaded-module list

Microsoft Learn explains that the system can check whether a DLL with the same module name is already loaded in memory.1

In other words, before the file system search, there is a check of

  • whether that DLL name is already loaded, and
  • consequently, whether there is any need to go searching at all.

So if, during an investigation, you drop the fact that “in this process, a same-named DLL from another folder was already loaded first,” you will misread the reproduction conditions.

What the loaded-module list decidesA diagram showing that before the file system search the system checks whether a DLL with the same module name is already loaded into that process's memory, and that if it is, that module is used regardless of which folder it came from, so there is no need to go searching at all.Already loadedNot loadedA DLL name needs resolvingIs a module of the same name already loadedUse that module (the folder is irrelevant)Continue with the rest of the searchEasy to get lost when a same-named DLL from another folder was loaded first

Figure 7: If a module of the same name is already loaded, nothing new is searched for.

5.2 Known DLLs

Known DLLs is the list of DLLs Windows considers well known for that version, and it can be inspected at HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs. For a DLL on the list, the system uses its copy of that known DLL.1

What matters here is that Known DLLs is not the kind of thing a regular app can “win” by placing a same-named DLL in its application folder. Understanding it as a simple first-come race against System32 misreads the behavior.

What Known DLLs doesA diagram showing that Known DLLs is the list of DLLs Windows treats as well known for that version, that the system-side copy is used for a DLL on the list, and that this is therefore not something a regular app can win by placing a same-named DLL in its application folder, so reading it as a first-come race against System32 misses the behavior.On the listNot on the listThe DLL name to resolveIs it on the Known DLLs listThe system-side copy is usedContinue with the rest of the searchNot something an app folder can override

Figure 8: A known DLL binds to the system-side copy, so there is no race to win.

6. An API Set Is a Contract Name, Not an “Actual DLL Name”

When you see a name like api-ms-win-core-..., the reflex is to ask “where do I find that DLL file?” But Microsoft Learn explains that an API set is a virtual alias for a physical DLL - a mechanism that separates the contract from the implementation.3

In other words, reasoning like

  • API set name = the physical DLL file name as is
  • API set resolution = the same file search as a normal DLL

is inaccurate.

With the API set model in mind, it becomes easier to explain that

  • things stay consistent even when the implementing DLL name differs across Windows versions and device types, and
  • the caller does not need to know, in fixed terms, which host DLL provides the implementation.3
An API set is a contract nameA diagram showing that a name such as api-ms-win- is not a physical DLL file name but a virtual alias that hides the implementation DLL, so that separating contract from implementation keeps things consistent even when the implementing DLL name differs across Windows versions and device types, and the caller never has to know which host DLL provides the implementation.A contract name such as api-ms-win-…Resolved as a virtual aliasThe implementing physical DLL stays hiddenConsistent even when the implementation differs by version or deviceTreating it like a normal file search is inaccurate

Figure 9: An API set is not a file name to look for; it is a contract name that hides the implementation.

7. Manifests and Side-by-Side (SxS) Are an Alternative Answer to the DLL Versioning Problem

DLL redirection and SxS manifests are described not as minor search-order tricks but as mechanisms for avoiding DLL versioning conflicts.789

Microsoft Learn’s framing is:

  • A manifest is XML describing a side-by-side assembly or an isolated application
  • A side-by-side assembly is the unit of naming, binding, versioning, and deployment
  • The loader decides which version to bind to based on the dependencies recorded in the manifest89
How manifests and side-by-side relateA diagram showing that a manifest is XML describing side-by-side assemblies and isolated applications, that a side-by-side assembly is the unit of naming, binding, versioning, and deployment, and that the loader decides which version to bind to from the dependencies recorded in the manifest, which is how DLL versioning conflicts are avoided.manifest (XML)Declares the side-by-side assemblies and versions it depends onThe loader decides which version to bind toMultiple versions of the same DLL can coexist

Figure 10: SxS is not a search-order trick; it is a separate answer to versioning conflicts.

So in practice you need to think about these three separately:

  • Simply placing a private DLL in the app folder
  • Using DLL redirection such as .local
  • Using side-by-side binding via manifests

All of them are similar in that they affect DLL resolution, but their design intent is not the same. Microsoft Learn’s guidance is to pick between them this way: DLL redirection if you want to fix an existing application without touching it, side-by-side components if you are writing a new one.7

Choosing between the three mechanismsA diagram showing that placing a private DLL in the app folder, using DLL redirection such as .local, and using side-by-side binding via a manifest all affect DLL resolution but differ in design intent, and that the guidance is DLL redirection when fixing an existing application without touching it and side-by-side for a new application.Which mechanism is this aboutPut a private DLL in the app folderDLL redirection (.local)SxS manifest bindingWhen you need a fix without touching the existing appDependency management for a new app

Figure 11: The three mechanisms look alike; pick between them on design intent.

7.1 What .local Actually Does

This tends to get one line and no more, so here is the behavior for unpackaged apps, broken out.7

  • What you put there: the redirection file is named ExecutableName.local. For Editor.exe that means placing Editor.exe.local in the same folder as the executable. The DLL you want loaded goes in that folder too
  • What goes in it: the contents of the file are ignored. Its mere existence is the signal that makes the loader look in the executable’s folder first when loading a DLL
  • What it covers: it applies to full-path loads as well as module-name loads. Regardless of the path passed to LoadLibrary or LoadLibraryEx, if a DLL of the same name sits in the executable’s folder, that one is loaded. The behavior exists to rescue situations like COM, where there is only one registration slot
  • When nothing is found: if the executable’s folder does not have it, the search falls back to the normal search order
  • Folder form: it also works if you create a folder named Editor.exe.local and put the DLLs inside it
  • Enabling it machine-wide: create a DWORD value named DevOverrideEnable under HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options, set it to 1, and reboot. With this set, .local redirection takes effect even for apps that carry an application manifest
  • For packaged apps: the location changes - the loader looks in <package install location>\microsoft.system.package.metadata\application.local\

There is one side effect here that will send an investigation off the rails if you miss it. When DLL redirection is in use and the app cannot access every drive and directory in the search order, LoadLibrary stops searching at the point where access is denied. Without DLL redirection, inaccessible directories are skipped and the search continues.7 If a DLL that should be further down the order is reported missing only on machines where .local is present, suspect this difference.

How .local behavesA diagram showing that a marker named ExecutableName.local placed next to the executable acts as a signal regardless of its contents, making the executable's folder be looked at first even for full-path loads, that the search falls back to the normal order when nothing is found there, and that it carries the side effect of stopping the search on an access denial.YesNoPlace ExecutableName.localLook in the executable's folder firstIs a DLL of the same name thereThat one is loaded even for a full pathFall back to the normal search orderSide effect: a denial ends the search

Figure 12: With .local, the marker’s existence is the signal, and it reaches even full-path loads.

8. What Changes with LoadLibraryEx, SetDllDirectory, and AddDllDirectory

8.1 SetDllDirectory

SetDllDirectory changes the search order, but Microsoft Learn explicitly states that it effectively disables safe DLL search mode.1

That is, even if you only intended to “add one app-specific folder,” the search space changes as a result, including how the current folder is treated.

Furthermore, calling SetDllDirectory in a parent process can affect the standard search order in child processes as well.1

For this reason, rather than using SetDllDirectory carelessly as a habit, it is safer in practice to lean on:

  • SetDefaultDllDirectories
  • AddDllDirectory
  • the LOAD_LIBRARY_SEARCH_* flags of LoadLibraryEx456
The trap in SetDllDirectoryA diagram showing that SetDllDirectory, even when all you meant was to add one app-specific folder, effectively disables safe DLL search mode and changes the whole search space, that calling it in a parent process can affect the child process search order, and that leaning on SetDefaultDllDirectories, AddDllDirectory, and the LoadLibraryEx search flags is safer.Add one folder with SetDllDirectorysafe DLL search mode is effectively disabledThe whole search space changes, current folder includedChild process search order can be affected tooLean on the SetDefaultDllDirectories family instead

Figure 13: What was meant as adding one folder weakens the entire search space.

8.2 AddDllDirectory

Paths added with AddDllDirectory are used in combination with LOAD_LIBRARY_SEARCH_USER_DIRS. On Microsoft Learn, the search order among multiple added directories is unspecified.15

So a design that

  • adds multiple directories, and
  • strictly expects a particular search order among them

is best avoided.

Using AddDllDirectory and what to watch forA diagram showing that paths added with AddDllDirectory are used together with LOAD_LIBRARY_SEARCH_USER_DIRS, that the search order among multiple added directories is unspecified, and that a design which adds several directories and strictly expects a particular order among them should be avoided.Add a path with AddDllDirectoryTakes effect together with LOAD_LIBRARY_SEARCH_USER_DIRSOrder among multiple additions is unspecifiedAvoid designs that depend on that order

Figure 14: The order among added user directories cannot be relied on by specification.

8.3 SetDefaultDllDirectories

SetDefaultDllDirectories is described as an API for removing the more vulnerability-prone directories from the standard DLL search path and restricting the search scope.4

Three properties are particularly worth knowing.

  • It takes effect per process
  • Once called, it persists for the lifetime of the process
  • A standard search path once set cannot simply be restored to the original default

From a security standpoint, this API lends itself to a design of shifting to a safer search space right after startup.4

Properties of SetDefaultDllDirectoriesA diagram showing that SetDefaultDllDirectories removes the more vulnerability-prone directories from the standard DLL search path to restrict the search scope, that it takes effect per process and persists for the lifetime of the process, and that the search path once set cannot be restored to the original default, which makes shifting to a safer search space right after startup an easy design.Call SetDefaultDllDirectories right after startupDrop the vulnerability-prone locations from the search scopeTakes effect per process for its whole lifetimeCannot be restored to the original default

Figure 15: An API to call once at startup to move the whole process to the safe side.

8.4 LoadLibraryEx

LoadLibraryEx lets you change the search behavior with LOAD_WITH_ALTERED_SEARCH_PATH and the LOAD_LIBRARY_SEARCH_* flags.61

Practically, it is an API well suited to requirements like

  • including the folder of the DLL being loaded in the search scope, dependencies included, or
  • restricting the search to the application folder, System32, and explicitly added user directories only.

There are four constraints worth knowing before you write any of it.6

Constraint Detail
Second argument hFile is reserved for future use; always pass NULL
Cannot be combined LOAD_WITH_ALTERED_SEARCH_PATH cannot be combined with any of the LOAD_LIBRARY_SEARCH_* flags
Full path required If you use LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, the first argument must be a full path
Order when several are set The search runs LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, then LOAD_LIBRARY_SEARCH_APPLICATION_DIR, then LOAD_LIBRARY_SEARCH_USER_DIRS, then LOAD_LIBRARY_SEARCH_SYSTEM32. The order within USER_DIRS is unspecified

Specify even one LOAD_LIBRARY_SEARCH_* flag and the standard search path is not used at all. So if you pass only LOAD_LIBRARY_SEARCH_SYSTEM32, the application folder is not searched. That is what narrowing means.

Specifying a search flag means the standard path is not usedA diagram showing that specifying even one LOAD_LIBRARY_SEARCH_ flag means the standard search path is not used at all, so that passing only LOAD_LIBRARY_SEARCH_SYSTEM32 leaves the application folder unsearched, which is what narrowing the search means.None specifiedAt least one specifiedWas any LOAD_LIBRARY_SEARCH_ flag specifiedThe standard search path is usedThe standard search path is not used at allOnly the scope of the specified flags is searchedSYSTEM32 alone leaves the app folder unsearched

Figure 16: The flags do not add to the search; they replace it with exactly that scope.

8.5 A Minimal Code Example (C/C++)

Putting the APIs so far into one place gives the following shape. SetDefaultDllDirectories and the LOAD_LIBRARY_SEARCH_* flags are Windows 8 and later APIs, so the target version has to be stated explicitly for the headers.46

/* cl /W4 loader.c  (Visual Studio 2022 + Windows SDK 10)
 * SetDefaultDllDirectories / AddDllDirectory / LOAD_LIBRARY_SEARCH_* are
 * Windows 8 and later. To also target Windows 7, KB2533623 is a prerequisite
 * and you need to resolve them at run time from Kernel32.dll with
 * GetProcAddress. */
#define _WIN32_WINNT 0x0602   /* Windows 8 */
#include <windows.h>
#include <stdio.h>

int wmain(void)
{
    /* 1. Move the process-default search space to the safe side.
     *    The goal is to drop the current folder and PATH from the search scope.
     *    LOAD_LIBRARY_SEARCH_DEFAULT_DIRS is the combination of
     *    APPLICATION_DIR + SYSTEM32 + USER_DIRS.
     *    Without USER_DIRS in here, the AddDllDirectory call in step 2
     *    will not be reflected in the process default. */
    if (!SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)) {
        wprintf(L"SetDefaultDllDirectories failed: %lu\n", GetLastError());
        return 1;
    }

    /* 2. Explicitly add just our own plugin folder.
     *    What you pass to AddDllDirectory must be an absolute path. */
    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. Load it.
     *    The second argument hFile is reserved, so always NULL.
     *    Passing flags here means this combination of flags is used
     *    instead of the process default set up in step 1.
     *    APPLICATION_DIR is left out, so the app folder is not searched. */
    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. Always confirm where it was loaded from.
     *    In an investigation, where it loaded from matters more than
     *    whether it loaded. */
    {
        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;
}

There are four things this code gets right.

  1. Call SetDefaultDllDirectories first. Once called, it holds for the lifetime of the process, and you cannot revert to the standard search path4
  2. AddDllDirectory only means something paired with LOAD_LIBRARY_SEARCH_USER_DIRS. If SetDefaultDllDirectories was not given USER_DIRS, the added directory is used only by LoadLibraryEx calls that specify LOAD_LIBRARY_SEARCH_USER_DIRS5
  3. Clean up. The cookie returned by AddDllDirectory can be passed to RemoveDllDirectory to remove the directory again5
  4. Print where it loaded from. Whether that one GetModuleFileNameW line exists decides how long the investigation takes when something breaks

Note that the DLLs foo.dll depends on are also searched within this LOAD_LIBRARY_SEARCH_* scope. If you want foo.dll’s own folder to be searched for its dependencies, make the first argument a full path and add LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR.

The flow of the minimal code exampleA diagram showing the flow of moving the process-default search space to the safe side with SetDefaultDllDirectories, explicitly adding the plugin folder with AddDllDirectory, loading with LoadLibraryEx and search flags, confirming where the module came from with GetModuleFileNameW, and cleaning up with RemoveDllDirectory.1. SetDefaultDllDirectories to make the default safe2. AddDllDirectory to add the approved folder3. LoadLibraryEx with search flags4. GetModuleFileNameW to confirm the source5. RemoveDllDirectory to clean up

Figure 17: The skeleton of the example is five steps: narrow, add, load, confirm, clean up.

8.6 Using It from C#

The same thinking applies in .NET. The P/Invoke search path is controlled with the DefaultDllImportSearchPaths attribute, and explicit loading with NativeLibrary.Load.

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

// Restrict the default search location to System32 for every P/Invoke in this assembly.
// Limits: the attribute does not apply to a P/Invoke that specifies an absolute path,
//         and it has no effect on platforms other than Windows.
[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]

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

    private static void Main()
    {
        // Declaring the attribute alone does nothing. It only takes effect on a call.
        Console.WriteLine($"GetTickCount = {GetTickCount()}");

        // The plugin lives in its own folder. Do not write
        //   NativeLibrary.Load("foo.dll", asm, ApplicationDirectory | System32)
        // here. ApplicationDirectory means the folder holding the exe and System32
        // means the OS folder; neither one looks at the plugin folder.
        // The load either fails or grabs some other foo.dll that happened to sit
        // next to the exe. When you know where a file is, a full path is the sure way.
        string pluginDir = Path.Combine(AppContext.BaseDirectory, "plugins");
        string pluginPath = Path.GetFullPath(Path.Combine(pluginDir, "foo.dll"));
        if (!File.Exists(pluginPath))
        {
            throw new FileNotFoundException("Plugin not found.", pluginPath);
        }

        // The overload that takes a path reads that file directly (it does not search).
        IntPtr handle = NativeLibrary.Load(pluginPath);

        try
        {
            // Confirm where it was loaded from.
            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);
        }
    }
}

The values of DllImportSearchPath correspond to the LOAD_LIBRARY_SEARCH_* flags. So when you write “System32 only” on the C# side, the constraints from 8.4 apply unchanged. The application directory is not searched.

The thing to watch here is that DllImportSearchPath has no value meaning “any folder I choose.” ApplicationDirectory means the folder holding the exe and System32 means the OS folder; there is no value that represents a folder you picked yourself for plugins. That is why search flags can never reach something placed in a dedicated folder, and why you end up specifying a full path as above.

Also note that even a full-path load does not pin the dependencies: as section 9 explains, the DLLs foo.dll depends on are still not fixed. If the plugin brings its own dependent DLLs, you need the same discipline as on the C side - add that folder with AddDllDirectory and enable LOAD_LIBRARY_SEARCH_USER_DIRS, or keep the dependencies together in one folder and use LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR.

How to load a DLL from a dedicated folder in C#A diagram showing that DllImportSearchPath has no value for an arbitrary folder because ApplicationDirectory means the folder holding the exe and System32 means the OS folder, so a plugin placed in a dedicated folder cannot be reached by search flags and must be loaded directly by full path, with dependent DLLs still needing the same discipline as on the C side.No value points at an arbitrary folderThe sure wayPut the plugin in a dedicated folderCan a search flag reach itFlags cannot reach itBuild the full path and load it directly with NativeLibrary.LoadDependent DLLs still need the C-side discipline

Figure 18: In .NET too, a folder you chose yourself is best loaded by full path.

9. A Full Path Does Not Pin the Dependent DLLs

This is the point on this topic that matters a great deal in practice yet is easily overlooked. Microsoft Learn explains that even when the first DLL is loaded by full path, its dependent DLLs are searched by module name only.14

That is, just because

  • you explicitly loaded C:\\MyApp\\plugins\\foo.dll

it does not follow that

  • bar.dll, which foo.dll depends on, will always be taken from the same folder.

With this misconception in place, you get the rather nasty failure mode of

  • it works in the development environment,
  • a different bar.dll gets resolved at the deployment site, and
  • dependent-DLL conflicts become environment-dependent to reproduce.
A full path does not pin the dependent DLLsA diagram showing that even when the first DLL is loaded by full path its dependent DLLs are treated as searched by module name only, so they can be resolved from somewhere else, producing an environment-dependent failure where the code works in development but a different dependent DLL is resolved at the deployment site.Load foo.dll by full pathfoo.dll itself loads exactly as specifiedThe dependent bar.dll is searched by module name onlyIt can be resolved from a different place per environmentWorks in development, breaks at the deployment site

Figure 19: A full path pins only the first DLL; the dependencies are handled separately.

10. Avoiding DLL Preloading / Hijacking

Microsoft Learn’s DLL security documentation explains that the combination of dynamic loading without a full path and attacker-controllable directories in the search scope leads to DLL preloading attacks and binary planting attacks.2

In practice, it helps to converge on this baseline.

  • Reduce bare-name loads like LoadLibrary("foo.dll")
  • Use full-path loading where needed
  • Narrow the process-default search path with SetDefaultDllDirectories
  • Add only explicitly approved directories with AddDllDirectory
  • Make the search scope explicit with flags like LOAD_LIBRARY_SEARCH_SYSTEM32, LOAD_LIBRARY_SEARCH_APPLICATION_DIR, and LOAD_LIBRARY_SEARCH_USER_DIRS
  • Avoid the current folder and careless reliance on PATH

A situation where a process running with administrator privileges has an ambiguous search path is especially dangerous. Microsoft Learn also notes that when a malicious DLL gets loaded, it executes with the privileges of that process.2

The combination that makes a DLL preloading attack workA diagram showing that dynamic loading without a full path combined with attacker-controllable directories in the search scope leads to a DLL preloading attack, that a malicious DLL then executes with the privileges of that process, and that the countermeasure is to reduce bare-name loads and narrow the search space.Dynamic loading without a full pathThe conditions for DLL preloading are metAttacker-controllable directories in the search scopeA malicious DLL runs with the process's privilegesFix: fewer bare names, narrower search space

Figure 20: The attack is the product of an ambiguous name and a controllable location.

11. Confirming Which DLL Was Loaded from Where

Everything so far has been about the specification. In an actual investigation, confirming instead of guessing is faster. Here are four options, by what you want to know.

What you want to know What to use Where to look
Where it looked and where it found the file Process Monitor10 The record of file accesses
What is loaded right now Process Explorer, tasklist /m11 The module list of the process
Which processes are holding a given DLL ListDLLs12 A list across all processes
The path actually loaded while debugging The Modules window in Visual Studio Debug > Windows > Modules7

11.1 Following the Search Trail with Process Monitor

This is the option with the most information in it. The procedure runs as follows.10

  1. Start Process Monitor as administrator and begin capturing
  2. Open Filter > Filter…, enter Process Name is with the name of the target exe, and add it
  3. On the same screen, add Path ends with .dll
  4. Start the target app, and stop capturing once the problem occurs

What you look at here is the Result column. The loader tries to open locations from the top of the search order down, so the places that did not have the file line up as NAME NOT FOUND and the place that actually opened shows SUCCESS. In other words, the line right after the run of NAME NOT FOUND entries is the path that was actually used.

Line this up against the table in section 3 and you can tell which stage decided the case. When a pre-stage rule (rows 1 through 6 of the table) decided it, there is no record of the loader going to the file system at all. That absence is an important clue in itself.

How to read a Process Monitor captureA diagram showing that because the loader tries to open locations from the top of the search order down, the places without the file line up as NAME NOT FOUND and the place that opened shows SUCCESS, so the line after the run of NAME NOT FOUND entries is the path actually used, and that no record at all means a pre-stage rule decided the outcome.Read the Result column from the topA run of NAME NOT FOUND = places searched without a hitThe SUCCESS right after it is the path actually usedNo record at all means a pre-stage rule decided it

Figure 21: The trail is in the Result column, and the absence of a record is itself a clue.

11.2 Listing the Loaded Modules

For a process that is already running, seeing what is loaded and from where takes no extra tooling.

tasklist /m foo.dll

This command lists the name and PID of every process that has the given module loaded.11 Once you have narrowed down which process you care about, switch the lower pane of Process Explorer to the DLL view and you can see the full path of every DLL that process has loaded.

Sysinternals ListDLLs does the same thing from the command line, and across all processes at once.12

This is the only way to tell whether the loaded-module list from 5.1 is in play. If a DLL of the same name was already loaded from another folder, that process will not go looking for a new one.

How to confirm the loaded modulesA diagram showing that tasklist /m narrows down the processes holding the given module, that Process Explorer's DLL view or ListDLLs then shows the full path of what is loaded, and that this is the only way to see whether the loaded-module list is in play.tasklist /m foo.dll to find the processes holding itProcess Explorer or ListDLLs for the full pathWhere it was loaded from becomes a settled factThe effect of the loaded-module list is visible only here

Figure 22: What is loaded right now should be settled by a module list, not by guesswork.

12. A Practical Decision Checklist

When reviewing DLL loading design on Windows, checking at least the following reduces the number of things that go wrong.

  1. Is the app a packaged app or an unpackaged app?
  2. Which DLLs come from static linking, and which are dynamically loaded?
  3. Full-path loading, or module name only?
  4. Is SetDllDirectory in use anywhere?
  5. Is the configuration such that SetDefaultDllDirectories and LOAD_LIBRARY_SEARCH_* can be used?
  6. Are multiple AddDllDirectory calls in use with an implicit expectation of ordering?
  7. Are dependencies managed via manifests / SxS / private DLLs / redirection - and which one?
  8. Are there weak security assumptions around the current folder or PATH?
  9. Could dependent DLLs be resolved from a different location in a different environment?
  10. On a machine showing the symptom, has anyone confirmed where the DLL was actually loaded from (section 11)?

Checking these ten points separately lets you head off problems like “DLL not found,” “the wrong DLL was loaded,” “it only fails to start in production,” and “blocked at the vulnerability review” well before they surface.

The order to work through in a reviewA diagram showing that a DLL loading design review goes better when worked through in order: the shape of the app as packaged or unpackaged, how loading is done by full path or by name, how the APIs are used including SetDllDirectory and the search flags, how dependencies are managed, and finally confirming on a real machine where the DLL was loaded from.Check the shape of the app (packaged / unpackaged)Check how it loads (full path or name only)Check the API usage (SetDllDirectory, flags)Check dependency management (manifest / SxS / PATH)Confirm the load source on a real machine (section 11)

Figure 23: The checklist works through in order: shape, load, API, dependencies, real-machine check.

13. Summary

DLL name resolution on Windows is not merely a “folder search order.” In reality, it is determined by the overlap of DLL redirection, API sets, SxS manifests, the loaded-module list, Known DLLs, and the search space as modified by API calls.134

What matters most in practice comes down to these six things.

  • Do not memorize the search order as a single table. Use the table to work out which stage decided your case
  • Separate packaged from unpackaged
  • Understand that even with full-path loading, dependent DLLs can be treated differently
  • Do not use SetDllDirectory casually
  • To err on the safe side, use SetDefaultDllDirectories and the search flags of LoadLibraryEx
  • Do not stop at guesswork; confirm the path actually loaded with Process Monitor or something like it

DLL name resolution is a place where startup failures, environment differences, and security problems all tend to surface at once. That is exactly why it is worth understanding not just “in what order Windows searches,” but “what Windows treats as the premises of name resolution in the first place.”

The conclusion of this articleA diagram showing that DLL name resolution is a place where startup failures, environment differences, and security problems all surface at once, so it is worth understanding not just the order in which Windows searches but what Windows treats as the premises of name resolution in the first place.In what order it searches (folder order)Only both together explain the behaviorWhat it treats as premises (pre-stage rules and search space)Startup failures, environment differences, and security handled together

Figure 24: The practical knowledge is not the memorized order but the understanding of the premises.

References

  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

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Windows App Development

DLL placement and loading directly affect whether a Windows app starts, how it is distributed, and how reproducible failures are, so this is well worth addressing in the context of Windows application development.

Frequently Asked Questions

Common questions about the topic of this article.

When I pass a DLL name to LoadLibrary, in what order does Windows look for it?
A set of pre-stage rules is evaluated before the file system is walked at all. Specifically, DLL redirection, API sets, SxS manifest redirection, the loaded-module list, and Known DLLs take effect first, and only then does the search move on to the file system: the application folder, System32, the Windows folder, the current folder, and PATH. In the default state with safe DLL search mode enabled, the current folder sits quite far down. Note also that packaged apps and unpackaged apps use genuinely different search models.
If I load a DLL by full path, will its dependent DLLs be loaded from the same folder?
Not necessarily. Even when the first DLL is loaded by full path, its dependent DLLs are treated as searched by module name only, so they can be resolved from somewhere else. This misconception produces the awkward environment-dependent failure where everything works on the development machine but a different dependent DLL is resolved at the deployment site. To control the dependencies as well, make the search scope explicit with the LOAD_LIBRARY_SEARCH_* flags of LoadLibraryEx.
Why should I avoid SetDllDirectory?
Because it does more than change the search order: it behaves in a way that effectively disables safe DLL search mode. Even when all you meant to do was add one app-specific folder, the entire search space changes, including how the current folder is treated, and that can backfire from a security standpoint. On top of that, calling it in a parent process can affect the search order on the child process side. Leaning on SetDefaultDllDirectories, AddDllDirectory, and the LoadLibraryEx search flags instead is safer.
How do I prevent DLL hijacking (DLL preloading attacks)?
The attack comes from the combination of dynamic loading without a full path and attacker-controllable directories in the search scope, so narrow both. Concretely: reduce bare-name LoadLibrary calls, use full-path loading where needed, narrow the process-default search path with SetDefaultDllDirectories, add only approved directories with AddDllDirectory, and avoid the current folder and careless reliance on PATH. A process running with administrator privileges and an ambiguous search path is especially dangerous, because a malicious DLL then executes with that process's privileges.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog