.NET Framework 4.8程序DLL注入成功后无法正常弹出卸载问题
核心原因
.NET Framework 程序集的加载生命周期完全独立于Win32原生DLL模块管理逻辑,原生注入器的卸载接口根本无法操作CLR层面的引用:
- 通用DLL注入器的Inject/Eject逻辑全部面向原生Win32 DLL设计:Eject方法的本质是在目标进程内调用
FreeLibrary,将对应PE模块的引用计数递减至0后,从进程虚拟地址空间移除模块、解除文件占用。 - 你注入的是基于.NET Framework 4.8开发的托管类库,这类DLL被加载到自带CLR的WPF进程后,会被直接映射到进程默认CLR AppDomain中,CLR会独立持有程序集的强引用。此时调用原生
FreeLibrary仅能递减PE模块的Win32层引用计数,完全不会触发CLR释放托管程序集引用,这就是为什么Eject方法返回true(代表原生API调用成功)但DLL文件依然被进程锁定的根本原因。 - 额外限制:目标WPF进程启动时已经初始化了默认CLR运行时,你注入的托管DLL直接复用该运行时,没有独立可销毁的CLR实例,无法通过卸载CLR的方式释放DLL。
可行实现方案
方案1:AppDomain隔离卸载(推荐,可实现内存、文件双无残留)
完全放弃注入器自带的原生Eject逻辑,在托管DLL内部自行实现CLR层面的生命周期管理:
- 不要将核心业务逻辑直接实现在初始注入的DLL中,初始DLL只保留极小的引导逻辑:当注入器调用DLL导出入口时,引导逻辑通过
AppDomain.CreateDomain创建一个配置了独立ApplicationBase路径的隔离AppDomain,所有业务代码(包括WPF相关的UI操作、事件订阅)全部加载到这个隔离AppDomain中执行,禁止任何业务对象跨域引用默认AppDomain的对象(尤其是WPF的DependencyObject、路由事件委托,跨域引用会导致后续AppDomain卸载失败)。 - 需要卸载DLL时,先在引导逻辑中调用
AppDomain.Unload释放你创建的隔离域,CLR会自动回收该域内加载的所有托管程序集,释放所有对应的文件锁。 - 等待CLR卸载操作完成后,再调用注入器的Eject方法,释放最开始注入的引导DLL模块,此时即可完成彻底卸载,无文件锁定、无内存残留。
注意:绝对不能尝试直接卸载进程默认AppDomain,该操作会直接触发进程崩溃。
方案2:内存加载托管DLL(无需卸载逻辑,无文件锁)
如果不想实现复杂的AppDomain隔离逻辑,可以通过内存加载的方式从根源上避免文件锁定:
- 初始注入阶段不要通过注入器直接
LoadLibrary磁盘上的托管业务DLL,而是先注入一个极小的原生引导壳。 - 引导壳在目标进程内拿到CLR入口后,直接读取磁盘上业务DLL的全量二进制字节,调用
Assembly.Load(byte[] rawAssembly)重载以内存模式加载托管程序集,再通过反射调用你的业务入口(比如示例中的Spam方法)。 - 这种加载模式下CLR不会锁定磁盘上的原始DLL文件,不需要执行任何Eject操作,随时可以修改、替换、删除磁盘上的DLL。唯一的残留是托管程序集会一直留在目标进程内存中直到进程退出,但不会产生任何文件占用问题。如果你的业务DLL有其他依赖类库,所有依赖项也需要通过字节数组方式加载,否则依然会锁定对应依赖的磁盘文件。
方案3:临时副本方案(仅调试场景使用,不推荐生产环境)
如果只是本地调试阶段需要快速迭代DLL、不想频繁重启目标进程,可以在注入前将待注入的DLL及其依赖复制到系统临时目录下的随机命名子文件夹中,用临时副本执行注入操作。这种方式下原始开发目录的DLL文件不会被锁定,可以随时编译替换,缺点是每次注入都会在进程内存中留下一份DLL残留,长期运行会造成内存泄漏,仅适合临时调试。
现有代码的问题
当前测试代码完全走原生DLL注入流程,没有任何CLR层面的生命周期处理,必然无法卸载托管DLL:
var process = Process.GetProcessesByName("ModsOptimizer").FirstOrDefault(); _injector = new Injector(process); _injectionDllPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Injection.dll"); var address = _injector.Inject(_injectionDllPath); // 仅通过Win32 LoadLibrary加载DLL,触发CLR将托管程序集加载到默认AppDomain _injector.CallFunction(_injectionDllPath, "Spam", default); // 业务逻辑直接运行在进程默认AppDomain,CLR持有强引用 _injector.Eject(_injectionDllPath); // 仅调用原生FreeLibrary递减Win32层模块引用计数,完全不处理CLR层的托管引用
之前尝试的FreeLibrary、FreeLibraryAndExitThread全部是Win32原生API,根本无法操作CLR内部的托管程序集引用计数,调用后自然无法释放文件锁。
内容的提问来源于stack exchange,提问作者Alex Wyler
相关产品推荐
相关产品推荐

