能否使用Assembly.TryGetRawMetadata和System.Reflection.Metadata访问方法体?
问题解答:Assembly.TryGetRawMetadata 无法访问方法体的原因及解决方案
你说得没错,Assembly.TryGetRawMetadata确实没办法直接获取方法体——这不是你忽略了什么细节,而是这个API的设计本来就只负责暴露程序集的元数据部分,而方法体根本不在这个元数据块的范围内。
为什么会这样?
元数据(也就是rawMetadata指向的那块内存)只包含描述程序结构的信息:比如类、方法的定义,引用的类型、成员签名等,以及方法体在PE文件中的相对虚拟地址(RVA)。但方法体本身是存储在PE文件的代码段(.text)里,和元数据是完全分开的两个内存区域。你拿到的rawMetadata指针只对应元数据的起始地址,和整个程序集的加载基地址没有偏移关联,所以自然没法用RVA直接从这里定位到方法体。
解决方案
这里提供两种可行的方案,根据你的需求选择:
方案1:用PEReader读取磁盘文件(推荐)
这个方法不需要把程序集再次加载到应用程序域,只是通过内存映射读取磁盘上的PE文件,既能访问元数据,又能直接通过RVA获取方法体,而且.NET已经封装了解析逻辑,不用自己处理复杂的PE格式。
using System.Reflection; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.IO; class Program { unsafe static void Main() { var thisAssembly = Assembly.GetExecutingAssembly(); // 直接打开磁盘上的程序集文件,创建PEReader using var peReader = new PEReader(File.OpenRead(thisAssembly.Location)); if (!peReader.HasMetadata) { return; } var metadataReader = peReader.GetMetadataReader(); foreach (var methodDefHandle in metadataReader.MethodDefinitions) { var methodDef = metadataReader.GetMethodDefinition(methodDefHandle); // 跳过没有方法体的抽象/外部方法 if (methodDef.RelativeVirtualAddress.IsZero) { continue; } // 通过PEReader直接获取方法体块 var methodBodyBlock = peReader.GetMethodBody(methodDef.RelativeVirtualAddress); // 获取IL字节码 var ilBytes = methodBodyBlock.GetILBytes(); Console.WriteLine($"方法 {metadataReader.GetString(methodDef.Name)} 的IL字节数:{ilBytes.Length}"); // 读取局部变量签名(如果有的话) var localVarSigHandle = methodBodyBlock.LocalVariablesSignature; if (!localVarSigHandle.IsNil) { var localVarSig = metadataReader.GetLocalVariableSignature(localVarSigHandle); Console.WriteLine($"局部变量数量:{localVarSig.LocalVariables.Count}"); } } } }
方案2:从已加载的程序集内存镜像中获取(不安全代码)
如果你必须基于已加载到内存的程序集来操作,可以通过获取模块的基地址,结合RVA计算方法体的绝对地址,不过需要手动解析PE方法体的格式,相对麻烦一些:
using System; using System.Reflection; using System.Reflection.Metadata; using System.Runtime.CompilerServices; class Program { unsafe static void Main() { var thisAssembly = Assembly.GetExecutingAssembly(); var manifestModule = thisAssembly.ManifestModule; // 获取已加载模块的PE基地址 IntPtr moduleBaseAddress = RuntimeHelpers.GetRuntimeModule(manifestModule).ModuleBase; if (thisAssembly.TryGetRawMetadata(out byte* rawMetadata, out int rawMetadataLength)) { var metadataReader = new MetadataReader(rawMetadata, rawMetadataLength); foreach (var methodDefHandle in metadataReader.MethodDefinitions) { var methodDef = metadataReader.GetMethodDefinition(methodDefHandle); if (methodDef.RelativeVirtualAddress.IsZero) { continue; } // 计算方法体的绝对内存地址 byte* methodBodyPtr = (byte*)moduleBaseAddress + methodDef.RelativeVirtualAddress; // 解析方法体格式:先判断是tiny还是fat格式 byte flags = *methodBodyPtr; if ((flags & 0x03) == 0x02) { // Fat格式:需要解析4字节大小、局部变量签名RVA、IL大小等 uint bodySize = *(uint*)(methodBodyPtr + 1) & 0xFFFFFFFC; // 去掉最低两位标记 uint localVarSigRva = *(uint*)(methodBodyPtr + 5); uint ilSize = *(uint*)(methodBodyPtr + 9); byte* ilStart = methodBodyPtr + 12; Console.WriteLine($"Fat方法体:IL大小 {ilSize} 字节"); // 可以直接读取ilStart指向的IL字节码 } else { // Tiny格式:IL大小存储在flags的高6位 byte ilSize = (byte)(flags >> 2); byte* ilStart = methodBodyPtr + 1; Console.WriteLine($"Tiny方法体:IL大小 {ilSize} 字节"); } } } } }
总结
总的来说,Assembly.TryGetRawMetadata的职责就是提供元数据访问,方法体不在它的覆盖范围内。如果不想折腾手动解析PE格式,优先用PEReader的方案,既简单又可靠。
内容的提问来源于stack exchange,提问作者stakx - no longer contributing
相关产品推荐
相关产品推荐

