头图

众所周知,用Assembly.LoadFile()方法对一个程序集文件进行分析存在一定的局限性,如果只想分析程序集,但是并不需要执行程序集,应该怎么办呢?今天,通过一个简单的实验来教给大家。

在编写.NET程序的时候,如果需要对一个程序集文件进行分析,我们可以使用 Assembly.LoadFile() 来加载这个程序集,然后对 LoadFile() 方法返回的 Assembly 对象进行进一步的分析。但是 Assembly.LoadFile() 方法会以执行为目的把程序集加载到程序中,因此它对于被加载的程序集文件有严格的要求,比如,如果被程序集所依赖的程序集不存在,那么 LoadFile() 会抛出异常,再比如,在 .NET Core 中加载 .NET Framework 的程序集,LoadFile() 也会抛出异常。如果我们只想分析程序集,但是并不需要执行程序集,那么我们就需要一种单纯地分析程序集文件的方式。

.NET Framework提供了Assembly.ReflectionOnlyLoad()来实现类似的效果,但是这个方法由于依赖于 AppDomain,因此在.NET Core中不被支持。微软曾经在实验室项目中提出过一个在.NET Core 中实现这个功能的 System.Reflection.TypeLoader,但不知道什么原因,没有在 .NET Core 的正式版中提供这个类。

我们知道,.NET 程序集是PE格式的文件,.NET 中提供了用来分析PE文件的类 PEReader(位于 System.Reflection.Metadata 这个 NuGet 包中),因此我们可以用 PEReader 来分析程序集文件。

在 PEReader 中,我们可以通过 TypeDefinitions 获取到程序集中的所有类,我们可以用 GetMethods()获取某个类中定义的所有方法。为了提升效率,TypeDefinitions、GetMethods()等成员获得到的对象都是 TypeDefinitionHandle、MethodDefinitionHandle 等句柄类型的,这些对象只包含地址信息,并不包含类型的名字、方法的名字、方法的参数等详细信息,要获取这些信息,我们需要调用MetadataReader 的 GetTypeDefinition()、GetMethodDefinition() 等方法来获取。如下的代码用来加载一个程序集,并且输出程序集中所有的类型信息以及类型中定义的方法:


//Install-PackageSystem.Reflection.Metadata
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;

string file =@"E:\Microsoft.AspNetCore.Components.Web.dll";
using FileStream fileStream =File.OpenRead(file);
using PEReader peReader = newPEReader(fileStream);
if(!peReader.HasMetadata)
{
   Console.WriteLine($"{file} doesn't contain CLI metadata.");
   return;
}
var mdReader =peReader.GetMetadataReader();
if (!mdReader.IsAssembly)
{
   Console.WriteLine($"{file} is not an assembly.");
   return;
}
foreach (var typeHandler inmdReader.TypeDefinitions)
{
   var typeDef = mdReader.GetTypeDefinition(typeHandler);
   string name = mdReader.GetString(typeDef.Name);
   string nameSpace = mdReader.GetString(typeDef.Namespace);
   Console.WriteLine($"***********{nameSpace}.{name}***********");
   foreach (var methodHandler in typeDef.GetMethods())
    {
       var methodDef = mdReader.GetMethodDefinition(methodHandler);
       Console.WriteLine(mdReader.GetString(methodDef.Name));
    }
}

使用PEReader的时候,我们需要先获得XXXHandler,然后再调用MetadataReader 获取句柄的详细信息,这样做尽管性能比较高,但是代码比较繁琐,而且在实现某些高级操作的时候比较麻烦。比如,如果我们要获取一个程序集的 CustomAttribute 信息,PEReader 并没有提供比较简单的方法,需要我们对 PE 格式非常精通,才能编写出来对应的代码。

我们可以使用 AsmResolver.DotNet 这个第三方 Nuget 包来简化程序集文件的读取分析,它是对 PEReader 的一个高级封装。如下的代码用来加载一个程序集,输出程序集的公司信息,并且输出程序集中所有的类型信息以及类型中定义的方法:


string file =@"E:\Microsoft.AspNetCore.Components.Web.dll";
var moduleDef =AsmResolver.DotNet.ModuleDefinition.FromFile(file);//用的不是System.Reflection.Metadata命名空间下的ModuleDefinition类
var asmCompanyAttr =moduleDef.Assembly.CustomAttributes.FirstOrDefault(c =>c.Constructor.DeclaringType.FullName =="System.Reflection.AssemblyCompanyAttribute");
var utf8Value =(Utf8String?)asmCompanyAttr.Signature.FixedArguments[0].Element;
var strValue = (string?)utf8Value;
Console.WriteLine($"companyname:{strValue}");
foreach(var typeDef inmoduleDef.GetAllTypes())
{
   string name = typeDef.Name;
   string nameSpace = typeDef.Namespace;
   Console.WriteLine($"***********{nameSpace}.{name}***********");
   foreach (var methodDef in typeDef.Methods)
    {
       Console.WriteLine(methodDef.Name);
    }
}

总之,如果我们需要分析一个程序集并且要运行其中的代码,我们可以使用 Assembly.LoadFile();如果我们不需要运行程序集,只是想分析程序集,那么使用 PEReader 是更好的选择,当然我们也可以选择对PEReader进行封装的AsmResolver.DotNet 这个NuGet包。本文作者杨中科在 Zack.Commons 这个开源项目中实现“判断一个程序集是否是微软开发的”这个功能的时候就用到了 AsmResolver.DotNet,大家可以查看这个项目的 GitHub 代码仓库来查看源代码。


微软技术栈
418 声望994 粉丝

微软技术生态官方平台。予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。