如何获取加载在独立AppDomain中的C#程序集的内存基地址
首先,你遇到的问题根源在于:Process.GetCurrentProcess().Modules是进程级别的枚举,它只会列出被加载到默认AppDomain的托管程序集,或者作为原生模块加载的文件。而你在独立的Temporal AppDomain中通过Assembly.Load(buffer)加载的程序集,属于该AppDomain的私有加载上下文,不会被进程级的Modules集合包含。
下面是几种无需内存扫描就能获取目标程序集基地址的方法,都符合你的需求(允许不安全/非托管代码):
方法1:在目标AppDomain内直接获取并跨域传递
这是最直接且可靠的方式,因为你的Loader类已经是MarshalByRefObject,可以在目标AppDomain的上下文里执行代码并返回结果。
你只需要修改CallInternal方法,在加载程序集后直接读取它的模块基地址,然后把地址和方法执行结果一起返回:
首先定义一个可序列化的类来承载结果和基地址(跨AppDomain传递需要序列化支持):
[Serializable] public class ExecutionResult { public object MethodResult { get; set; } public IntPtr AssemblyBaseAddress { get; set; } }
然后修改Loader的相关方法:
public class Loader : MarshalByRefObject { ExecutionResult CallInternal(string dll, string method, string[] parameters) { byte[] buffer = File.ReadAllBytes(dll); Assembly a = Assembly.Load(buffer); IntPtr baseAddr = IntPtr.Zero; // 获取程序集的第一个模块基地址(托管程序集通常只有一个模块) if (a.Modules.Count > 0) { baseAddr = a.Modules[0].BaseAddress; } Type[] types = a.GetTypes(); MethodInfo m = null; Type myType = null; foreach (var type in types) { m = type.GetMethod(method); if (m != null) { myType = type; break; } } object methodResult = null; if (m != null && myType != null) { var myInstance = Activator.CreateInstance(myType); methodResult = m.Invoke(myInstance, new object[] { parameters }); } return new ExecutionResult { MethodResult = methodResult, AssemblyBaseAddress = baseAddr }; } public static ExecutionResult Call(string dll, string method, params string[] parameters) { AppDomain dom = AppDomain.CreateDomain("Temporal"); Loader ld = (Loader)dom.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(Loader).FullName); ExecutionResult result = ld.CallInternal(dll, method, parameters); AppDomain.Unload(dom); // 记得用完卸载AppDomain,避免内存泄漏 return result; } }
这样调用Loader.Call后,就能直接拿到目标程序集的基地址,完全不需要内存扫描。
方法2:使用CLR非托管API(进阶)
如果你需要在默认AppDomain中直接获取其他AppDomain的加载信息,可以使用CLR的非托管API(比如ICorRuntimeHost或ICorProfilerInfo)来枚举所有AppDomain及其加载的程序集。不过这种方法需要编写非托管互操作代码,复杂度较高,不如方法1简洁。
举个简单的例子,通过ICorRuntimeHost获取所有AppDomain:
using System.Runtime.InteropServices; using System.Collections; [ComImport, Guid("CB2F6723-AB3A-11D2-9C40-00C04FA30A3E")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface ICorRuntimeHost { void _VtblGap1_1(); void EnumDomains(out IntPtr enumHandle); void NextDomain(IntPtr enumHandle, out AppDomain appDomain); void CloseEnum(IntPtr enumHandle); } public static AppDomain[] GetAllAppDomains() { ArrayList domains = new ArrayList(); ICorRuntimeHost host = (ICorRuntimeHost)RuntimeEnvironment.GetRuntimeInterfaceAsObject(null, typeof(ICorRuntimeHost).GUID); IntPtr enumHandle; host.EnumDomains(out enumHandle); AppDomain domain; while (true) { host.NextDomain(enumHandle, out domain); if (domain == null) break; domains.Add(domain); } host.CloseEnum(enumHandle); return (AppDomain[])domains.ToArray(typeof(AppDomain)); }
拿到目标AppDomain后,你还需要进一步枚举它加载的程序集,再获取模块基地址。但这种方法同样需要在目标AppDomain的上下文里执行代码(因为Assembly对象是AppDomain特定的),本质上和方法1没有太大区别,只是多了一层非托管调用的复杂度。
注意事项
- 托管程序集的
Module.BaseAddress返回的是该模块在内存中的加载基址,对于动态加载的程序集(通过Assembly.Load(byte[])),这个地址是CLR分配的内存区域。 - 记得在使用完独立AppDomain后调用
AppDomain.Unload(dom),避免内存泄漏。 - 你的原始
Loader.Call方法里有个参数错误:ld.CallInternal(dll, typename, method, parameters)中的typename未定义,应该是笔误,我在修改后的代码里已经修正了。
内容的提问来源于stack exchange,提问作者Kurosh D.

