.NET?Core剪裁器背后的技術(shù)及工作原理介紹
十天前,我發(fā)布了對(duì).NET Core程序進(jìn)行瘦身的開源軟件Zack.DotNetTrimmer,與.NET Core內(nèi)置的剪裁器相比,Zack.DotNetTrimmer不僅對(duì)程序的剪裁效果更好,而且還支持WPF、WinForm程序。
很多朋友對(duì)于這個(gè)開源項(xiàng)目的原理很感興趣,因此我將通過這篇文章為大家介紹它的工作原理。
技術(shù)1、檢測程序加載的程序集和類
微軟提供了用于對(duì).NET Core的運(yùn)行時(shí)行為進(jìn)行分析的庫Diagnostics,它可以獲取豐富的運(yùn)行時(shí)信息,比如類的實(shí)例創(chuàng)建、程序集加載、類加載、方法調(diào)用、GC運(yùn)行、文件讀寫操作、網(wǎng)絡(luò)連接等。Visual Studio中對(duì)每個(gè)方法的調(diào)用時(shí)間進(jìn)行評(píng)估的工具就是使用Diagnostics實(shí)現(xiàn)的。
要使用Diagnostics庫,我們首先需要安裝Microsoft.Diagnostics.NETCore.Client和Microsoft.Diagnostics.Tracing.TraceEvent這兩個(gè)程序集,然后使用DiagnosticsClient類來連接被分析的.NET Core程序的進(jìn)程。代碼如下所示:
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Parsers.Clr;
using System.Diagnostics;
using System.Diagnostics.Tracing;
string filepath = @"E:\temp\test6\ConsoleApp1.exe";//被分析的程序路徑
ProcessStartInfo psInfo = new ProcessStartInfo(filepath);
psInfo.UseShellExecute = true;
using Process? p = Process.Start(psInfo);//啟動(dòng)程序
var providers = new List<EventPipeProvider>()//要監(jiān)聽的事件
{
new EventPipeProvider("Microsoft-Windows-DotNETRuntime",
EventLevel.Informational, (long)ClrTraceEventParser.Keywords.All)
};
var client = new DiagnosticsClient(p.Id);//設(shè)定DiagnosticsClient監(jiān)聽的進(jìn)程
using EventPipeSession session = client.StartEventPipeSession(providers, false);//啟動(dòng)監(jiān)聽
var source = new EventPipeEventSource(session.EventStream);
source.Clr.All += (TraceEvent obj) =>
{
if (obj is ModuleLoadUnloadTraceData)//程序集加載事件
var data = (ModuleLoadUnloadTraceData)obj;
string path = data.ModuleILPath;//獲取程序集的路徑
Console.WriteLine($"Assembly Loaded:{path}");
}
else if (obj is TypeLoadStopTraceData)//類加載事件
var data = (TypeLoadStopTraceData)obj;
string typeName = data.TypeName;//獲取類名
Console.WriteLine($"Type Loaded:{typeName}");
};
source.Process();不同類型的消息對(duì)應(yīng)source.Clr.All事件中的不同類型的對(duì)象,這些類都繼承自TraceEvent,我這里分析的是程序集加載事件ModuleLoadUnloadTraceData和類加載事件TypeLoadStopTraceData。
這樣我們就可以得知程序運(yùn)行過程中加載的程序集和類型信息,這樣就知道哪些程序集和類型沒有被加載,從而我們就知道要?jiǎng)h除哪些程序集和類型了。
技術(shù)2、刪除程序集中用不到的類
Zack.DotNetTrimmer中提供了可以刪除程序集中用不到的類的IL的功能,這個(gè)功能使用dnlib這個(gè)庫來完成的程序集文件的編輯。Dnlib是一個(gè)對(duì).NET程序集文件進(jìn)行讀、寫、編輯的開源項(xiàng)目。
在Dnlib中,我們使用ModuleDefMD.Load來加載一個(gè)現(xiàn)有的程序集,Load方法的返回值是ModuleDefMD類型。ModuleDefMD代表程序集信息,比如其中的Types屬性就代表程序集中的所有的類型。我們可以對(duì)ModuleDefMD以及其中的對(duì)象進(jìn)行修改后,把修改完成的程序集調(diào)用Write方法再保存到磁盤中。
比如,下面的代碼用來把一個(gè)程序集中的所有非public類型都給改成public類型,并且把方法上修飾的Attribute全部清除:
using dnlib.DotNet;
string filename = @"E:\temp\net6.0\AppToBeTested1.dll";
ModuleDefMD module = ModuleDefMD.Load(filename);
foreach(var typeDef in module.Types)
{
if (typeDef.IsPublic == false)
{
typeDef.Attributes |= TypeAttributes.Public;//修改類的訪問級(jí)別
}
foreach(var methodDef in typeDef.Methods)
methodDef.CustomAttributes.Clear();//清除方法的Attribute??
}
module.Write(@"E:\temp\net6.0\1.dll");//保存修改下面是待測試的程序集的源代碼:
internal class Class1
{
[DisplayName("AAA")]
public void AA()
{
Console.WriteLine("hello");
}
}如下是修改后的程序集的反編譯結(jié)果:
public class Class1
{
?public void AA()
?{
?Console.WriteLine("hello");
?}
}可以看到我們對(duì)于程序集的修改起作用了。
掌握了使用Dnlib對(duì)程序集進(jìn)行修改的方法,我們就可以實(shí)現(xiàn)刪除程序集中用不到的類型的功能了,我們只要把對(duì)應(yīng)的類型從ModuleDefMD的Types屬性中刪除掉即可。不過在實(shí)際操作中,這樣做會(huì)遇到問題,因?yàn)槲覀円獎(jiǎng)h除的類可能被其他的地方引用,盡管那些地方只是引用我們要?jiǎng)h除的類,并沒有真的調(diào)用,但是為了保證修改后程序集的校驗(yàn)合法性,ModuleDefMD的Write方法仍然會(huì)做合法性校驗(yàn),否則Write方法就會(huì)拋出ModuleWriterException異常,比如:
ModuleWriterException: 'A method was removed that is still referenced by this module.'
因此,我們編寫代碼需要對(duì)程序集做仔細(xì)的檢查,確保刪除每一個(gè)引用要被刪除的類的地方。因?yàn)轭惗x本身占用的文件尺寸很少,主要的代碼的空間占用都在類的方法體中,因此我找了一個(gè)替代方案,那就是并不刪除類,只是把類的方法體清空。
Dnlib中,方法對(duì)應(yīng)的類型是MethodDef類型,MethodDef的CilBody 類型的Body屬性代表方法的方法體。如果方法擁有方法體(也就是不是抽象方法等),那么CilBody的Instructions就代表方法體代碼的IL指令的集合。因此我立即想到了通過下面的代碼來清空方法的方法體:
methodDef.Body.Instructions.Clear();
但是在運(yùn)行的時(shí)候,使用上面的代碼清理后的ModuleDefMD進(jìn)行保存的時(shí)候,可能會(huì)引起程序集結(jié)構(gòu)非法的問題,比如有的方法定義了返回值,如果我們直接清空方法體,就會(huì)造成方法沒有返回值被返回的問題。因此我換了一種思路,也就是把所有的方法體都改成throw null;這個(gè)C#代碼對(duì)應(yīng)的IL代碼,因?yàn)樗械姆椒w都是可以改成拋出一個(gè)異常的形式來保證邏輯的正確性。因此我編寫如下的代碼來進(jìn)行方法體的清理:
method.Body.ExceptionHandlers.Clear();
method.Body.Instructions.Clear();
method.Body.Variables.Clear();
method.Body.Instructions.Add(new Instruction(OpCodes.Nop) { Offset = 0 });
method.Body.Instructions.Add(new Instruction(OpCodes.Ldnull) { Offset = 1 });
method.Body.Instructions.Add(new Instruction(OpCodes.Throw) { Offset = 2 });最后三行添加的IL代碼就是對(duì)應(yīng)throw null這行C#代碼。
請(qǐng)查看項(xiàng)目的github地址獲取全部源代碼,項(xiàng)目地址:https://github.com/yangzhongke/Zack.DotNetTrimmer
Dnlib使用的其他問題
在使用Dnlib過程中,我還有一些其他的收獲,在這里記錄下來與大家分享。
收獲一、Dnlib保存含有本地代碼的程序集時(shí)候遇到的問題
在使用上面我提到的方法清理程序集的時(shí)候,對(duì)于我們編寫的自定義程序集以及第三方NuGet包的程序集的時(shí)候,大部分是沒問題的。但是在使用同樣的方法處理PresentationCore.dll、System.Private.CoreLib.dll等.NET Core基礎(chǔ)程序集的時(shí)候遇到了問題,那就是即使我對(duì)程序集只是Load之后,不做任何的改動(dòng)后,直接Write,程序集也會(huì)發(fā)生明顯的變小。比如我用下面的代碼處理一下PresentationFramework.dll:
using (var mod = ModuleDefMD.Load(@"E:\temp\PresentationFramework.dll"))
{
mod.Write(@"E:\temp\PresentationFramework.New.dll");
}原始的PresentationFramework.dll大小是15.9MB,而保存后新的文件大小只有5.7MB。經(jīng)過詢問Dnlib作者得知,這些程序集含有本地代碼(比如使用C++/CLI編寫的代碼或者ReadyToRun / NGEN / CrossGen等格式的程序集),使用Write方法保存的時(shí)候會(huì)忽略這些本地代碼,這就是保存后的程序集尺寸明顯變小的原因。我們可以使用NativeWrite方法代替Write方法,因?yàn)檫@個(gè)方法會(huì)保留本地代碼。
不過,根據(jù)AsmResolver(一個(gè)和DnLib類似的開源項(xiàng)目)的作者Washi1337所說,NativeWrite方法會(huì)盡量保存本地代碼的結(jié)構(gòu)因此無法減小程序集的尺寸,甚至有可能反而增大程序集的尺寸(詳見https://github.com/Washi1337/AsmResolver/issues/267)。而且在實(shí)際使用的時(shí)候,我發(fā)現(xiàn)對(duì)于這些程序集進(jìn)行修改之后,程序就會(huì)啟動(dòng)失敗,查看Windows事件日志,我發(fā)現(xiàn)是程序啟動(dòng)的時(shí)候CLR啟動(dòng)失敗造成的。根據(jù)Washi1337所說,如果只是程序集中含有ReadyToRun的本地代碼,那么只要去掉程序集中的ILLibrary標(biāo)志,讓CLR跳過ReadyToRun本地代碼,而直接執(zhí)行IL代碼就行了,畢竟對(duì)于ReadyToRun優(yōu)化后的程序集仍然保存了原始的IL代碼。但是我如Washi1337所說的操作之后,程序依舊啟動(dòng)失敗,不清楚是什么原因,因?yàn)楹斜镜卮a的程序集無法被很好的剪裁,因此我沒有再深入研究,歡迎對(duì)CLR精通的朋友分享經(jīng)驗(yàn)。
收獲二、Dnlib的其他應(yīng)用
由于DnLib可以修改程序集,因此我們可以使用它做很多的事情,比如修改程序的默認(rèn)行為(你懂的)。我們可以使用DnLib編寫一個(gè)自己的代碼混淆器或者實(shí)現(xiàn)面向切面編程(AOP)的靜態(tài)織入。
你還想到了哪些DnLib的應(yīng)用場景?歡迎分享。
到此這篇關(guān)于揭秘.NET Core剪裁器背后的技術(shù)的文章就介紹到這了,更多相關(guān).NET Core剪裁器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET 獲取存儲(chǔ)過程返回值的實(shí)現(xiàn)代碼
ASP.NET 獲取存儲(chǔ)過程返回值的實(shí)現(xiàn)代碼,需要的朋友可以參考下。2011-12-12
ASP.NET MVC+EF框架+EasyUI實(shí)現(xiàn)權(quán)限管系列
在學(xué)習(xí)MVC之前,我們有必要知道這些知識(shí)點(diǎn)(自動(dòng)屬性,隱式類型var,對(duì)象初始化器和集合初始化器,匿名類,擴(kuò)展方法,Lambda表達(dá)式),如果你還不知道的話就請(qǐng)看我下面的簡單的介紹,看下面我建立的項(xiàng)目的初步圖像,然后下篇我們開始簡單的介紹。2014-11-11
js實(shí)現(xiàn)網(wǎng)頁防止被iframe框架嵌套及幾種location.href的區(qū)別
首先我們了解一下幾種location.href的區(qū)別簡單的說:幾種location.href的區(qū)別js實(shí)現(xiàn)網(wǎng)頁被iframe框架功能,感興趣的朋友可以了解下2013-08-08
asp.net基于session實(shí)現(xiàn)購物車的方法
這篇文章主要介紹了asp.net基于session實(shí)現(xiàn)購物車的方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了asp.net使用session存儲(chǔ)臨時(shí)數(shù)據(jù)實(shí)現(xiàn)購物車功能的相關(guān)技巧,需要的朋友可以參考下2015-11-11
基于localStorge開發(fā)登錄模塊的記住密碼與自動(dòng)登錄實(shí)例
下面小編就為大家?guī)硪黄趌ocalStorge開發(fā)登錄模塊的記住密碼與自動(dòng)登錄實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08
集合類List與Dictonary實(shí)例練習(xí)
本文將詳細(xì)介紹下List<>泛型集合/Dictonary<>字典/泛型集合練習(xí) /中日期轉(zhuǎn)換提取為方法以及泛型集合練習(xí)之翻譯軟件,感興趣的你可不要錯(cuò)過了哈2013-02-02
asp.net下linkbutton的前后臺(tái)使用方法
asp.net LinkButton傳遞參數(shù)2008-08-08

