在編寫(xiě).NET程序的時(shí)候癣籽,如果需要對(duì)一個(gè)程序集文件進(jìn)行分析挽唉,我們可以使用Assembly.LoadFile()來(lái)加載這個(gè)程序集,然后對(duì)LoadFile()方法返回的Assembly對(duì)象進(jìn)行進(jìn)一步的分析才避。但是Assembly.LoadFile()方法會(huì)以執(zhí)行為目的把程序集加載到程序中橱夭,因此它對(duì)于被加載的程序集文件有嚴(yán)格的要求,比如桑逝,如果被程序集所依賴(lài)的程序集不存在棘劣,那么LoadFile()會(huì)拋出異常,再比如楞遏,在.NET Core中加載.NET Framework的程序集茬暇,LoadFile()也會(huì)拋出異常。如果我們只想分析程序集寡喝,但是并不需要執(zhí)行程序集糙俗,那么我們就需要一種單純地分析程序集文件的方式。
.NET Framework提供了Assembly.ReflectionOnlyLoad()來(lái)實(shí)現(xiàn)類(lèi)似的效果预鬓,但是這個(gè)方法由于依賴(lài)于AppDomain巧骚,因此在.NET Core中不被支持。微軟曾經(jīng)在實(shí)驗(yàn)室項(xiàng)目中提出過(guò)一個(gè)在.NET Core中實(shí)現(xiàn)這個(gè)功能的System.Reflection.TypeLoader,但不知道什么原因劈彪,沒(méi)有在.NET Core的正式版中提供這個(gè)類(lèi)竣蹦。
我們知道,.NET程序集是PE格式的文件沧奴,.NET中提供了用來(lái)分析PE文件的類(lèi)PEReader(位于System.Reflection.Metadata這個(gè)NuGet包中)痘括,因此我們可以用PEReader來(lái)分析程序集文件。
在PEReader中滔吠,我們可以通過(guò)TypeDefinitions獲取到程序集中的所有類(lèi)纲菌,我們可以用GetMethods()獲取某個(gè)類(lèi)中定義的所有方法。為了提升效率疮绷,TypeDefinitions翰舌、GetMethods()等成員獲得到的對(duì)象都是TypeDefinitionHandle、MethodDefinitionHandle等句柄類(lèi)型的冬骚,這些對(duì)象只包含地址信息灶芝,并不包含類(lèi)型的名字、方法的名字唉韭、方法的參數(shù)等詳細(xì)信息,要獲取這些信息犯犁,我們需要調(diào)用MetadataReader的GetTypeDefinition()属愤、GetMethodDefinition()等方法來(lái)獲取。如下的代碼用來(lái)加載一個(gè)程序集酸役,并且輸出程序集中所有的類(lèi)型信息以及類(lèi)型中定義的方法:
//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的時(shí)候住诸,我們需要先獲得XXXHandler,然后再調(diào)用MetadataReader獲取句柄的詳細(xì)信息涣澡,這樣做盡管性能比較高贱呐,但是代碼比較繁瑣,而且在實(shí)現(xiàn)某些高級(jí)操作的時(shí)候比較麻煩入桂。比如奄薇,如果我們要獲取一個(gè)程序集的CustomAttribute信息,PEReader并沒(méi)有提供比較簡(jiǎn)單的方法抗愁,需要我們對(duì)PE格式非常精通馁蒂,才能編寫(xiě)出來(lái)對(duì)應(yīng)的代碼。
我們可以使用AsmResolver.DotNet這個(gè)第三方Nuget包來(lái)簡(jiǎn)化程序集文件的讀取分析蜘腌,它是對(duì)PEReader的一個(gè)高級(jí)封裝沫屡。如下的代碼用來(lái)加載一個(gè)程序集,輸出程序集的公司信息撮珠,并且輸出程序集中所有的類(lèi)型信息以及類(lèi)型中定義的方法:
string file = @"E:\Microsoft.AspNetCore.Components.Web.dll";
var moduleDef = AsmResolver.DotNet.ModuleDefinition.FromFile(file);//用的不是System.Reflection.Metadata命名空間下的ModuleDefinition類(lèi)
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);
}
}
總之沮脖,如果我們需要分析一個(gè)程序集并且要運(yùn)行其中的代碼,我們可以使用Assembly.LoadFile();如果我們不需要運(yùn)行程序集勺届,只是想分析程序集驶俊,那么使用PEReader是更好的選擇,當(dāng)然我們也可以選擇對(duì)PEReader進(jìn)行封裝的AsmResolver.DotNet這個(gè)NuGet包涮因。本文作者楊中科在Zack.Commons這個(gè)開(kāi)源項(xiàng)目中實(shí)現(xiàn)“判斷一個(gè)程序集是否是微軟開(kāi)發(fā)的”這個(gè)功能的時(shí)候就用到了AsmResolver.DotNet废睦,大家可以查看這個(gè)項(xiàng)目的GitHub代碼倉(cāng)庫(kù)來(lái)查看源代碼。