.NET Core分析程式集最優美的方法,不用Assembly.LoadFile(),超越ReflectionOnlyLoad

楊中科發表於2022-02-08

在編寫.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-Package System.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 = new PEReader(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 in mdReader.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($"company name:{strValue}");
foreach(var typeDef in moduleDef.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程式碼倉庫來檢視原始碼。

相關文章