.NET Core 分析程式集更優方法,超越ReflectionOnlyLoad

微軟技術棧發表於2022-03-22

眾所周知,用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 程式碼倉庫來檢視原始碼。

相關文章