先讲一个让我印象深刻的翻车现场。去年给一个桌面客户端接第三方视频识别SDK,对方只给了一个 native_vision.dll 和一堆小DLL。本地开发机调试一切正常,发布到客户的 Windows Server 之后,程序一跑到算法初始化就直接报 System.DllNotFoundException: 无法加载 DLL“native_vision.dll”。我用 Procmon 抓了一遍,发现程序压根没去我以为的那个目录找文件,而是先跑了一堆莫名其妙的位置,最后空手而归。那次之后我把微软文档里关于 DLL 搜索顺序的内容翻了个底朝天,又在 .NET Framework 和 .NET Core 两种项目里分别做了验证,踩了不少坑。这篇文章就把 P/Invoke 执行时的搜索顺序完整讲清楚,适合所有写过 [DllImport] 的 C# 开发者,尤其是那些被“本地好好的,部署就崩”折磨过的人。
很多人以为 P/Invoke 就是“声明一个外部函数,然后调用”,但其实 DLL 搜索顺序才是最容易出幺蛾子的地方。我在这篇文章里会先讲 Windows 加载器的默认规则,再讲 .NET 各版本在规则之上做了哪些手脚,最后给出可实操的验证方法、部署建议和排查表。内容偏底层,但我会尽量用大白话和真实案例讲,保证你不只看懂,还能直接照着排查。
1. 先别急着写代码:P/Invoke 加载背后的“搜索顺序”要害
1.1 一次部署事故引发的排查
回到开头那个事故。客户现场的系统是 Windows Server 2019,我们的程序装在 D:\App\ 目录下,第三方 SDK 放在 D:\App\Native\ 子目录里,代码里写的是:
csharp复制[DllImport("native_vision.dll")]
private static extern int VisionInit();
本地开发机上,我把 native_vision.dll 放在了编译输出目录(也就是 exe 旁边),所以怎么跑都没事。发给客户之后,安装包把 exe 放到了 D:\App\,把 native 库放到了 D:\App\Native\,我满以为放在 exe 同目录的子目录下也能被找到,结果 P/Invoke 直接罢工。
当时的日志只显示“找不到 DLL”,没有半点线索。后来用 Procmon 一条条看,才发现 .NET 在尝试加载这个 DLL 时,实际走的路径顺序和我预期完全不一致:它根本没有去 D:\App\Native\ 找过。原因很直接:我把搜索顺序想简单了,以为只要是程序目录下的文件都能被找到,实际上子目录并不会自动进入默认搜索范围。
这件事给我的教训是:在写任何 P/Invoke 代码之前,先搞清楚目标平台的 DLL 搜索顺序,比写好几个 [DllImport] 都重要。
1.2 DllImport 只负责告诉你“找谁”,真正跑腿的是系统装载器
P/Invoke 的完整流程可以这样理解:C# 这边的 [DllImport] 只定义了一个“契约”,告诉 CLR:我要调用 native_vision.dll 里的 VisionInit 函数。CLR 在第一次调用这个方法时,会把任务交给 Windows 的模块装载器,由装载器去磁盘上定位并加载这个 DLL。
这里有个容易忽略的细节:C# 侧并不自己实现“搜索算法”,它只是调用系统提供的加载 API,最终会走到 LoadLibrary 或 LoadLibraryEx。也就是说,搜索顺序的底层规则是 Windows 定的,不是 .NET 定的。你要回答“P/Invoke 执行时到底先搜哪里、再搜哪里”,本质上是在回答“Windows 加载 DLL 时按什么顺序搜索”。
不过 .NET 也不是完全撒手不管。在部分版本里,运行时会在调用系统 API 之前先做一些自己的探测,比如先看一眼调用方程序集所在目录,甚至解析依赖清单里的路径。这就是为什么同样的代码,在 .NET Framework 和 .NET Core 下表现可能不一样。这也是很多人最容易搞混的地方:同一个问题,去网上搜,答案五花八门,因为大家用的运行时不一样。
1.3 为什么“本机没毛病,客户机器必翻车”
这个问题几乎成了 P/Invoke 项目的魔咒。原因说到底就是一句话:不同机器上的“环境”差异太大。
本机能跑,可能是你开发机的 PATH 环境变量里恰好有一个目录包含这个 DLL,可能是 Visual Studio 调试时把工作目录设成了项目输出目录,也可能是你把 DLL 复制到了 C:\Windows\System32(后来被清理了才想起来)。而客户机器是一台干净的服务器,没有开发环境,没有多余的 PATH 路径,甚至系统目录里存在一个同名但不同版本的旧 DLL,加载器优先命中了它,行为立刻变得诡异。
我见过更夸张的情况:某个服务程序通过任务计划程序启动,工作目录被设置成了 C:\Windows\System32,代码里用相对路径去拼 native 库的位置,结果程序找不到文件,但不报文件不存在,反而报 DllNotFoundException,排查半天才发现是路径拼接错了。所以,理解搜索顺序不是纯理论,它能帮你快速定位这类部署事故。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Windows 的默认 DLL 搜索顺序到底是怎么排的
2.1 开启 SafeDllSearchMode 时的标准顺序
从 Windows XP SP2 开始,系统默认启用了 SafeDllSearchMode。在这个模式下,当一个进程调用 LoadLibrary("xxx.dll") 且不附加任何特殊标志时,加载器会按下面这个顺序搜索:
- 应用程序所在目录,也就是启动 exe 所在的目录。
- 系统目录,通常是从
GetSystemDirectory()拿到的路径,一般是C:\Windows\System32。 - 16 位系统目录,通常不存在,可忽略。
- Windows 目录,一般是
C:\Windows。 - 当前工作目录(Current Working Directory)。
- PATH 环境变量中列出的各个目录,按从左到右的顺序。
第一眼看上去挺简单,但这里藏着两个很反直觉的点。第一个:当前工作目录的优先级比系统目录还低。很多人以为“程序在哪个目录启动,就会优先去哪个目录找 DLL”,这个理解是错的。优先级最高的是 exe 所在目录,不是当前目录。如果你程序的工作目录被切到别处,加载器会先找 exe 目录,再去 System32,最后才轮到当前目录。
第二个反直觉的点:PATH 目录排在最后,而且是在当前目录之后。如果你的 DLL 放在 PATH 目录里,但系统目录或 exe 目录里恰好有个同名文件,加载器根本不会走到 PATH 那一步。
如果 SafeDllSearchMode 被关闭(可以通过注册表关闭,也可以被某些进程创建方式影响),顺序会变成:exe 目录、当前目录、系统目录、16 位目录、Windows 目录、PATH。当前目录的优先级被提前到了系统目录之前,这在安全上是比较危险的,所以绝大多数系统默认都开着安全模式。
2.2 Changed Search Path:用 LoadLibraryEx 做局部修改
上面说的标准顺序是“普通加载”的情况。实际编码时,很多第三方库会调用 LoadLibraryEx,并传入 LOAD_WITH_ALTERED_SEARCH_PATH 标志。这个标志的作用是:如果传入的 DLL 路径是完整路径,那么加载器会优先在“这个 DLL 所在目录”里搜索它自身以及它的依赖项,然后再按标准顺序找。
举个例子,如果代码执行 LoadLibraryEx("D:\\App\\Native\\native_vision.dll", 0, LOAD_WITH_ALTERED_SEARCH_PATH),加载器会先尝试 D:\App\Native\ 目录,而不是先找 exe 目录。这对把 DLL 放在子目录的部署方式非常重要,因为它使得“DLL 周边目录优先”成为可能。
不过要注意,这个标志只影响“被加载的 DLL 自身所在目录”的优先级,并不会把依赖 DLL 的搜索范围限制在该目录内。如果 native_vision.dll 又依赖 common_helper.dll,加载器在找 common_helper.dll 时,标准搜索顺序依然会起作用,只是会额外把 native_vision.dll 所在目录排在比较靠前的位置。
2.3 KnownDLLs:系统目录里那些“优先户口”
Windows 维护着一张内部列表,叫 KnownDLLs。位于此列表中的系统 DLL(例如 kernel32.dll、user32.dll、advapi32.dll 等)在加载时会被特殊对待:加载器直接从系统目录映射,根本不会去搜索 exe 目录、当前目录或 PATH。
这意味着一个常见的误解要纠正:不要以为在 exe 目录放一个 user32.dll 就能让程序优先加载你自己的版本。对于 KnownDLLs 里的名字,系统目录之外的副本基本不会被普通加载流程命中。这也是为什么很多人尝试用 DLL 替换的方式做测试时,发现毫无效果,因为系统根本不给你机会。
这个问题和 P/Invoke 的关联在于:如果你 P/Invoke 声明的 DLL 名字恰好和某个系统已知 DLL 重名,加载器很可能直接命中 KnownDLLs,而不是去你的应用目录找。遇到这种诡异情况,第一反应应该是检查 DLL 名字是不是和系统模块重名,而不是继续调搜索路径配置。
2.4 依赖 DLL 的搜索是另一场递归
很多时候,我们只关注 P/Invoke 指定的那个 DLL,却忽略了它自己还有一堆依赖。主 DLL 加载成功不等于万事大吉,它的导入表里引用的每个依赖 DLL,加载器都需要单独走一遍搜索流程。
比如 native_vision.dll 编译时静态链接了某个第三方库,但那个库的部分功能是动态加载的,它运行时调用 LoadLibrary("tensor.dll"),这时候 tensor.dll 的搜索顺序又是一个独立的加载过程。问题是,主 DLL 内部用的加载方式你控制不了,它可能使用标准搜索,也可能使用变了形的搜索路径。
Procmon 抓到的日志里,你经常会看到主 DLL 加载成功后,紧跟着一堆对依赖 DLL 的 CreateFile 请求。排查的时候只看主 DLL 是不够的,必须把所有依赖 DLL 的路径都检查一遍,否则报错信息会非常误导。常见情况是 DllNotFoundException 报的是主 DLL 名字,但实际上缺失的是某个依赖 DLL,因为主 DLL 加载成功后,系统找不到它的依赖,也会把错误以主 DLL 的名字抛出来。
3. .NET 各时代对搜索顺序的“插一脚”
3.1 .NET Framework 下常见行为
在 .NET Framework 时代,这里的水很深。不同版本、不同补丁级别下,P/Invoke 的搜索行为有细微差别。我自己实测下来,最常见的情况是:CLR 会先把调用方程序集所在目录加入搜索范围,然后再走 Windows 标准顺序。
也就是说,假设你的 exe 在 C:\MyApp\,程序集在 C:\MyApp\Bin\,通过 [DllImport] 加载一个 DLL,CLR 通常会先探测 C:\MyApp\Bin\ 下有没有这个文件。这其实是运行时为了支持 XCOPY 部署做的一个便利性处理。很多老项目依赖这个行为:把 native DLL 和托管 DLL 放在一起,什么都不配就能跑。
但这里有个坑:它不一定总是这么做。某些加载路径下(比如从网络位置加载程序集,或者程序集是从字节数组加载的),这个探测就不成立。另外,.NET Framework 的 ASP.NET 应用里,工作目录可能被 IIS 的 w3wp.exe 工作进程固定住,和你的站点目录完全不同。如果你把 native DLL 放在站点的 bin 下,靠“程序集目录优先”可能没问题,但如果你放在站点根目录的某个子目录里,就会出现找不到的情况。
3.2 .NET Core 的 Probing 与 NativeLibrary
到了 .NET Core 3.0 之后,运行时对 native 库的解析方式有了大变化,公开了一整套 NativeLibrary API。这个时候,P/Invoke 的实际加载路径变得更有迹可循:它会先走运行时自己的探测逻辑,探测不到才真正调用 Windows 加载器。
一个常见的探测来源是依赖清单文件 deps.json。如果你的项目通过 NuGet 引用了某个包含 native 库的包,这个包的 native 文件通常会被放在 runtimes/win-x64/native/ 这样的目录里。发布时,这些文件可能被复制到输出目录。运行时在解析库名时,会结合 RID(Runtime Identifier)去匹配这些路径。
举个例子:你 P/Invoke 声明加载 sqlite3,NuGet 包里带了一个 runtimes/win-x64/native/sqlite3.dll,在 .NET Core 3.0+ 项目里,运行时可能自动找到这个文件,不需要你手动复制到 exe 目录。但在 .NET Framework 项目里,这种行为是不存在的,你必须手动把 DLL 放到能被搜索到的位置。
这也是为什么同一个解决方案,从 .NET Framework 迁移到 .NET 6 之后,有些 DLL 不复制反而能跑,有些复制了反而加载了错误版本,因为运行时的搜索逻辑已经变了。
3.3 LibraryImport 之后的实际变化
.NET 7 开始,微软大力推荐使用 [LibraryImport] 取代 [DllImport]。这个特性会把 P/Invoke 的封送代码改为编译期生成,减少运行时反射开销。但有一点要明确:它改变的是“如何调用函数”,并没有改变“如何搜索 DLL”。
[LibraryImport("native_vision.dll")] 和 [DllImport("native_vision.dll")] 在搜索顺序上并没有本质区别,底层仍然依赖 native 库解析机制。我看到不少开发者以为换了 LibraryImport 就能解决 DLL 找不到的问题,结果换了之后问题原封不动。
不过 LibraryImport 确实有一个间接好处:它让整个调用链更透明。因为源码生成器会生成一段显式的解析代码,你可以清楚地看到它什么时候调用 NativeLibrary.Load,什么时候触发默认加载流程。这在调试时比黑盒的 [DllImport] 容易理解得多。
3.4 我建议把“搜索顺序”当作可观察行为而非固定规则
说了这么多版本差异,我最想强调的一点是:不要试图背下所有版本的搜索顺序,然后靠记忆写代码。不同补丁、不同宿主(控制台、Windows 服务、ASP.NET Core、WPF)都可能带来不同的默认值。
更好的做法是把它当作一个可观察行为:写代码时明确指定“我希望它从哪里加载”,而不是“希望系统帮我找到它”。能用完整路径就用完整路径,能用自定义解析器就用自定义解析器,能不依赖全局 PATH 就不依赖。把不确定性降到最低,部署问题会少一大半。
4. 实测:用 Procmon 和一点点 C++ 还原搜索全过程
4.1 用 Procmon 抓到真相
Sysinternals 套件里的 Procmon(Process Monitor)是排查 DLL 搜索顺序的第一神器。我以前花了很多时间靠文档猜,后来发现直接抓日志最快。
用 Procmon 观察的过程很简单:
- 打开 Procmon,先清空当前事件。
- 设置过滤条件:
Process Name是你的程序进程名,比如MyApp.exe。 - 再增加一个过滤:
Path以.dll结尾。 - 运行触发 P/Invoke 的代码。
- 观察日志中
CreateFile操作和Load Image操作。
你会看到加载器依次访问一堆路径,大部分结果是 NAME NOT FOUND,直到某一行的结果是 SUCCESS。那个成功的路径就是最终加载的位置。用这种方式,你都不用看任何文档,就能知道这台机器上实际按什么顺序搜索。
有一点要注意:Procmon 里的 Load Image 表示真正把 DLL 映射进进程的那一次操作,CreateFile 的 SUCCESS 只是文件打开成功,不一定是最终加载位置。以最后的 Load Image 路径为准。
4.2 做一个会报版本号的小 DLL
为了验证搜索顺序,我习惯写一个极小的 C++ DLL,导出一个函数,返回一个固定的版本号。然后我在不同目录放不同版本号的同名 DLL,再通过 C# 调用这个函数,看返回值就知道加载的是哪一个。
C++ 侧代码很简单:
cpp复制#include <windows.h>
extern "C" __declspec(dllexport) int GetDllVersion()
{
return 42;
}
编译成 native_sample.dll,复制到多个目录,分别把版本号改成 1、2、3、4。C# 侧代码:
csharp复制using System.Runtime.InteropServices;
class Program
{
[DllImport("native_sample.dll")]
private static extern int GetDllVersion();
static void Main()
{
int version = GetDllVersion();
Console.WriteLine($"Loaded version: {version}");
}
}
然后我在 exe 所在目录放一个返回 1 的版本,在 System32 放一个返回 2 的版本,在当前工作目录放一个返回 3 的版本,在一个 PATH 目录放一个返回 4 的版本。每次只改环境,运行程序看输出,就能验证搜索顺序。
4.3 放不同目录跑一轮后的结论
按标准搜索顺序,在我的 Windows 10 测试机上关闭所有应用层自定义配置后,结果很干净:
- 当 exe 目录有同名 DLL 时,加载的是 exe 目录里的版本,也就是版本 1。
- 把 exe 目录里的文件删掉,加载的是 System32 里的版本 2,而不是当前目录的版本 3。
- 再把 System32 里的文件也删掉,加载的是当前工作目录的版本 3。
- 如果当前目录也不存在,才轮到 PATH 目录里的版本 4。
这个结果和文档描述一致。但要注意,我的测试程序是控制台应用,如果换成 .NET Framework 项目,第一步有可能变成“调用方程序集所在目录优先”,具体要看程序集和 exe 是否在同一个目录。建议在你自己项目里做同样实验,因为我实测发现,发布模式下的 .NET Core 单文件程序和普通框架依赖程序,行为还会有一点差别。
测试完之后,记得把 System32 里那个测试 DLL 删掉,千万别留在系统目录里。这种操作在本机验证可以,在客户机器上绝对不能干,会影响其他程序。
5. 工程上最稳妥的几种加载策略
5.1 全部放 exe 目录:交付省心的“XCOPY”方案
如果你的应用不需要安装到 Program Files,也没有管理员权限限制,最简单的策略就是把所有 native DLL 和 exe 放在同一目录,做成 XCOPY 部署。
这样做的好处是直接命中搜索顺序的第一位:exe 所在目录。不用配置 PATH,不用调用额外的 API,也不用处理当前工作目录变化的问题。P/Invoke 执行时,加载器能最快命中目标。
缺点是目录会比较乱,尤其当 native 库很多、第三方 SDK 还自带一堆子目录时,全堆在一起很难管理。而且如果你的程序以 Windows 服务形式运行,exe 目录的可写性通常很差,但读取没有问题。只要 DLL 和 exe 在同一目录,服务方式运行也能正常加载。
5.2 用 SetDllDirectory 和 AddDllDirectory 注入目录
如果你的 native DLL 必须放在子目录,或者放在一个独立的数据目录里,可以考虑在进程启动早期给搜索路径注入自定义目录。
传统的做法是调用 SetDllDirectory,把某个目录插入到“exe 目录之后、系统目录之前”。这个 API 对当前进程全局生效,之后所有 DLL 搜索都会把该目录排到比较靠前的位置。
csharp复制[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool SetDllDirectory(string lpPathName);
调用时机很关键:必须在第一次 P/Invoke 之前调用,否则之前已经解析过的 DLL 不受影响。另外,它影响的是整个进程,后续如果加载第三方插件,插件里的 DLL 也可能从这个目录加载,有引入冲突的风险。
更新的做法是 AddDllDirectory,它把目录加入进程的“用户自定义搜索列表”,但需要配合 LOAD_LIBRARY_SEARCH_USER_DIRS 标志使用,调用方式更细。用 AddDllDirectory 的好处是不会覆盖之前设置的目录,而是增量添加;坏处是它只影响使用了相应标志的加载调用,并不改变标准的 LoadLibrary 搜索行为。如果你的 P/Invoke 走的不是带标志的加载路径,加了目录可能还是没用。
5.3 关键场景用 DllImportResolver 接管加载
到了 .NET Core 3.0+,最优雅的方案是注册自定义 DllImportResolver。它给你一个机会,在系统默认搜索之前介入,精确决定某个库从哪里加载。
示例代码:
csharp复制using System.Runtime.InteropServices;
internal static class NativeLibraryConfig
{
private static bool _initialized;
internal static void Init()
{
if (_initialized)
return;
NativeLibrary.SetDllImportResolver(
typeof(NativeLibraryConfig).Assembly,
ResolveNativeLibrary);
_initialized = true;
}
private static IntPtr ResolveNativeLibrary(
string libraryName,
Assembly assembly,
DllImportSearchPath? searchPath)
{
if (libraryName.Contains("native_vision"))
{
string candidate = Path.Combine(
AppContext.BaseDirectory,
"Native",
"native_vision.dll");
if (NativeLibrary.TryLoad(candidate, out IntPtr handle))
{
return handle;
}
}
return IntPtr.Zero;
}
}
这个方案的好处是逻辑完全在托管侧掌控:库名匹配规则、候选路径、加载失败后的行为都由你说了算。返回 IntPtr.Zero 时,运行时会继续走默认搜索流程,所以你不会把原有行为封死。
需要注意的是,同一个 DllImportResolver 对同一个程序集的所有 DllImport 都生效,所以在 resolver 内部最好做好库名区分,别把所有加载请求都导向同一个目录。另外,这个 API 在 .NET Framework 里不可用,只能在 .NET Core / .NET 5+ 使用。
5.4 别信当前工作目录,别信 PATH
工程上还有一条老生常谈但必须反复强调的建议:不要依赖当前工作目录,也不要把 native DLL 目录手动加进 PATH。
当前工作目录是一个极不稳定的值。在 Visual Studio 里运行时,调试器会把工作目录设为项目输出目录。用任务计划程序启动时,工作目录可能被设成 C:\Windows\System32。从资源管理器双击运行时,工作目录通常是 exe 所在目录。同一个程序,不同的启动方式,工作目录完全不一样。你的代码一旦用相对路径拼接 native 库位置,结果就是薛定谔的部署:有时能跑,有时不能跑。
PATH 也一样。把自定义目录加进系统 PATH,确实能让搜索顺序走到那一步,但副作用是新开的所有程序都会受这个 PATH 影响,可能干扰其他应用的 DLL 解析,而且你在客户机器上不一定有权限修改系统 PATH。
5.5 对 DLL 替换与劫持多做一点防御
排查 DLL 搜索顺序问题时,我们也会顺带提到 DLL 劫持这种安全风险。从开发者的防御视角看,有几个习惯值得养成。
第一,尽量不要用“把某个目录切到当前工作目录再加载 DLL”这种临时手段。这个操作会把一个可写目录插入搜索链,如果这个目录恰好允许低权限用户写文件,风险就很大。第二,加载第三方 native 库时优先使用绝对路径或自定义 resolver,明确指定从程序自己的受控目录加载。第三,定期用 Procmon 检查程序的加载日志,确认每个 native 库都来自预期路径,没有从当前目录或 PATH 中的非预期位置加载同名文件。
安全话题我不展开太多,但“搜索顺序”本身就是一个两面性问题。正常开发时,它是部署排查的钥匙;被恶意利用时,它是 DLL 劫持的入口。作为开发者,把自己代码里的加载路径写死、写明确,既是稳定性需求,也是基本的安全卫生习惯。
6. 高频问题排查对照与收尾提醒
6.1 遇到这些问题先查什么
排查 P/Invoke 加载问题时,我建议按下面这个表快速定位:
| 症状 | 可能原因 | 优先排查方向 |
|---|---|---|
| 本地能跑,部署后 DllNotFoundException | 依赖了本地 PATH 或工作目录 | 抓 Procmon 看实际搜索路径 |
| 报错说找不到主 DLL,但文件明明在 | 依赖 DLL 缺失或主 DLL 在非预期目录被加载 | 抓 Procmon 看所有 .dll 访问记录 |
| 程序加载了旧版本行为异常 | exe 目录和 System32 存在同名旧文件 | 用 Process Explorer 查看已加载模块列表 |
| 放在子目录找不到 | 子目录不在默认搜索范围 | 改用 resolver 或 SetDllDirectory 注入该目录 |
| 32/64 位不匹配 | System32 和 SysWOW64 重定向导致路径不对 | 确认进程位数和 DLL 位数一致 |
| 在 IDE 里能跑,发布后不能跑 | 调试器改变了工作目录或本机存在开发期产物 | 检查相对路径拼接位置和输出目录内容 |
每次排查,我的第一步永远是抓加载日志,而不是直接改代码。没有日志的情况下,改来改去很容易把问题搞得更隐蔽。
6.2 一个容易忽略的边界:DLL 名称带完整路径时搜索顺序已经失效
最后补充一个很容易忽略但非常有用的技巧:[DllImport] 里的字符串如果包含完整路径,那么搜索顺序实际已经不生效了,加载器会直接去指定路径加载。
csharp复制[DllImport(@"D:\App\Native\native_vision.dll")]
private static extern int VisionInit();
这种写法能精确加载指定文件,但我不建议在生产代码里直接用,原因有两个:一是路径硬编码后,换机器、换安装目录就要改代码;二是如果路径不存在,报错依然很隐晦,而且因为跳过了默认搜索,你连“放在 exe 目录备用”的机会都没有了。
真正合理的做法是把完整路径计算出来,再用 NativeLibrary.Load 或自定义 resolver 加载。这样既保留了精确性,又不把路径写死在声明里。
在解决完那次客户现场的故障后,我给自己定了一条规矩:所有用到 P/Invoke 的项目,必须在启动阶段显式初始化 native 库的搜索策略。要么把 DLL 统一放到 exe 目录,要么在第一时间注册 resolver,绝不在业务代码里依赖环境变量和当前目录。发布之前,我一定会用 Procmon 把加载日志过一遍,确认每条 Load Image 都指向预期路径。这个习惯帮我在后续好几个项目里躲过了规模不小的部署事故。搜索顺序这个问题,只要认真把它当成系统行为去对待,其实并不复杂,怕的是想当然。
