揭秘.netcore剪裁器背后的技术(代码片段)

杨中科 杨中科     2022-10-20     735

关键词:

十天前,我发布了对.NET Core程序进行瘦身的开源软件Zack.DotNetTrimmer,与.NET Core内置的剪裁器相比,Zack.DotNetTrimmer不仅对程序的剪裁效果更好,而且还支持WPF、WinForm程序。

很多朋友对于这个开源项目的原理很感兴趣,因此我将通过这篇文章为大家介绍它的工作原理。

技术1、检测程序加载的程序集和类

微软提供了用于对.NET Core的运行时行为进行分析的库Diagnostics,它可以获取丰富的运行时信息,比如类的实例创建、程序集加载、类加载、方法调用、GC运行、文件读写操作、网络连接等。Visual Studio中对每个方法的调用时间进行评估的工具就是使用Diagnostics实现的。

要使用Diagnostics库,我们首先需要安装Microsoft.Diagnostics.NETCore.Client和Microsoft.Diagnostics.Tracing.TraceEvent这两个程序集,然后使用DiagnosticsClient类来连接被分析的.NET Core程序的进程。代码如下所示:

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);//启动程序

var providers = new List<EventPipeProvider>()//要监听的事件

 

 new EventPipeProvider("Microsoft-Windows-DotNETRuntime",

 EventLevel.Informational, (long)ClrTraceEventParser.Keywords.All)

 ;

var client = new DiagnosticsClient(p.Id);//设定DiagnosticsClient监听的进程

using EventPipeSession session = client.StartEventPipeSession(providers, false);//启动监听

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();

不同类型的消息对应source.Clr.All事件中的不同类型的对象,这些类都继承自TraceEvent,我这里分析的是程序集加载事件ModuleLoadUnloadTraceData和类加载事件TypeLoadStopTraceData。

这样我们就可以得知程序运行过程中加载的程序集和类型信息,这样就知道哪些程序集和类型没有被加载,从而我们就知道要删除哪些程序集和类型了。

技术2、删除程序集中用不到的类

Zack.DotNetTrimmer中提供了可以删除程序集中用不到的类的IL的功能,这个功能使用dnlib这个库来完成的程序集文件的编辑。Dnlib是一个对.NET程序集文件进行读、写、编辑的开源项目。

在Dnlib中,我们使用ModuleDefMD.Load来加载一个现有的程序集,Load方法的返回值是ModuleDefMD类型。ModuleDefMD代表程序集信息,比如其中的Types属性就代表程序集中的所有的类型。我们可以对ModuleDefMD以及其中的对象进行修改后,把修改完成的程序集调用Write方法再保存到磁盘中。

比如,下面的代码用来把一个程序集中的所有非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;//修改类的访问级别

 

 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");

 


如下是修改后的程序集的反编译结果:


public class Class1



 public void AA()

 

 Console.WriteLine("hello");

 


可以看到我们对于程序集的修改起作用了。

掌握了使用Dnlib对程序集进行修改的方法,我们就可以实现删除程序集中用不到的类型的功能了,我们只要把对应的类型从ModuleDefMD的Types属性中删除掉即可。不过在实际操作中,这样做会遇到问题,因为我们要删除的类可能被其他的地方引用,尽管那些地方只是引用我们要删除的类,并没有真的调用,但是为了保证修改后程序集的校验合法性,ModuleDefMD的Write方法仍然会做合法性校验,否则Write方法就会抛出ModuleWriterException异常,比如:

ModuleWriterException: \'A method was removed that is still referenced by this module.\'

因此,我们编写代码需要对程序集做仔细的检查,确保删除每一个引用要被删除的类的地方。因为类定义本身占用的文件尺寸很少,主要的代码的空间占用都在类的方法体中,因此我找了一个替代方案,那就是并不删除类,只是把类的方法体清空。

Dnlib中,方法对应的类型是MethodDef类型,MethodDef的CilBody 类型的Body属性代表方法的方法体。如果方法拥有方法体(也就是不是抽象方法等),那么CilBody的Instructions就代表方法体代码的IL指令的集合。因此我立即想到了通过下面的代码来清空方法的方法体:

methodDef.Body.Instructions.Clear();

但是在运行的时候,使用上面的代码清理后的ModuleDefMD进行保存的时候,可能会引起程序集结构非法的问题,比如有的方法定义了返回值,如果我们直接清空方法体,就会造成方法没有返回值被返回的问题。因此我换了一种思路,也就是把所有的方法体都改成throw null;这个C#代码对应的IL代码,因为所有的方法体都是可以改成抛出一个异常的形式来保证逻辑的正确性。因此我编写如下的代码来进行方法体的清理:


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代码就是对应throw null这行C#代码。

请查看项目的github地址获取全部源代码,项目地址:https://github.com/yangzhongke/Zack.DotNetTrimmer

Dnlib使用的其他问题

在使用Dnlib过程中,我还有一些其他的收获,在这里记录下来与大家分享。

收获一、Dnlib保存含有本地代码的程序集时候遇到的问题

在使用上面我提到的方法清理程序集的时候,对于我们编写的自定义程序集以及第三方NuGet包的程序集的时候,大部分是没问题的。但是在使用同样的方法处理PresentationCore.dll、System.Private.CoreLib.dll等.NET Core基础程序集的时候遇到了问题,那就是即使我对程序集只是Load之后,不做任何的改动后,直接Write,程序集也会发生明显的变小。比如我用下面的代码处理一下PresentationFramework.dll:


using (var mod = ModuleDefMD.Load(@"E:\\temp\\PresentationFramework.dll"))



 mod.Write(@"E:\\temp\\PresentationFramework.New.dll");


原始的PresentationFramework.dll大小是15.9MB,而保存后新的文件大小只有5.7MB。经过询问Dnlib作者得知,这些程序集含有本地代码(比如使用C++/CLI编写的代码或者ReadyToRun / NGEN / CrossGen等格式的程序集),使用Write方法保存的时候会忽略这些本地代码,这就是保存后的程序集尺寸明显变小的原因。我们可以使用NativeWrite方法代替Write方法,因为这个方法会保留本地代码。

不过,根据AsmResolver(一个和DnLib类似的开源项目)的作者Washi1337所说,NativeWrite方法会尽量保存本地代码的结构因此无法减小程序集的尺寸,甚至有可能反而增大程序集的尺寸(详见https://github.com/Washi1337/AsmResolver/issues/267)。而且在实际使用的时候,我发现对于这些程序集进行修改之后,程序就会启动失败,查看Windows事件日志,我发现是程序启动的时候CLR启动失败造成的。根据Washi1337所说,如果只是程序集中含有ReadyToRun的本地代码,那么只要去掉程序集中的ILLibrary标志,让CLR跳过ReadyToRun本地代码,而直接执行IL代码就行了,毕竟对于ReadyToRun优化后的程序集仍然保存了原始的IL代码。但是我如Washi1337所说的操作之后,程序依旧启动失败,不清楚是什么原因,因为含有本地代码的程序集无法被很好的剪裁,因此我没有再深入研究,欢迎对CLR精通的朋友分享经验。

收获二、Dnlib的其他应用

由于DnLib可以修改程序集,因此我们可以使用它做很多的事情,比如修改程序的默认行为(你懂的)。我们可以使用DnLib编写一个自己的代码混淆器或者实现面向切面编程(AOP)的静态织入。

你还想到了哪些DnLib的应用场景?欢迎分享。

《asp.netcore6框架揭秘》实例演示[15]:针对控制台的日志输出(代码片段)

...化由ConsoleFormatter对象来完成。[本文节选《ASP.NETCore6框架揭秘》第9章][S901]SimpleConsoleFormatter格式化器(源代码)[S902]SystemdConsoleFormatter格式化器(源代码)[S903]JsonConsoleFormatter格式化器(源代码)[S904]改变Con... 查看详情

.netcore剪裁器zack.dotnettrimmer升级瘦身引擎,并支持剪裁计划的录制和回放

上周,我发布了对.NETCore程序进行瘦身的开源软件Zack.DotNetTrimmer,与.NETCore内置的剪裁器相比,Zack.DotNetTrimmer不仅对程序的剪裁效果更好,而且还支持WPF、WinForm程序。下面是Zack.DotNetTrimmer与.NET内置的剪裁器的对比图: 项目地... 查看详情

.netcore剪裁器升级瘦身引擎,并支持剪裁计划的录制和回放

上周,我发布了对.NETCore程序进行瘦身的开源软件Zack.DotNetTrimmer,与.NETCore内置的剪裁器相比,Zack.DotNetTrimmer不仅对程序的剪裁效果更好,而且还支持WPF、WinForm程序。下面是Zack.DotNetTrimmer与.NET内置的剪裁器的对比... 查看详情

黑客爱用的hook技术大揭秘!(代码片段)

什么是HOOK技术?病毒木马为何惨遭杀软拦截?商业软件为何频遭免费破解?系统漏洞为何能被补丁修复?这一切的背后到底是人性的扭曲,还是道德的沦丧,尽请收看今天的专题文章:《什么是HOOK技... 查看详情

揭秘|双11逆天记录背后的数据库技术革新

每一个数字背后都需要强大的技术支撑Higher,Faster,Smarter是我们不变的追求技术无边界创新无止境 查看详情

降价背后,函数计算规格自主选配功能揭秘(代码片段)

在刚刚结束的2022杭州·云栖大会上,阿里云宣布函数计算FC开启全面降价,vCPU单价降幅 11%,其他的各个独立计费项最高降幅达 37.5%。函数计算FC全面降价,让Serverless更加普惠,用户可随用随取,按量计费... 查看详情

一个更好用的.netcore程序瘦身器,减小程序尺寸到1/3

一、为什么要开发.NETCore程序瘦身器? .NETCore具有【剪裁未使用的代码】的功能,但是由于它是使用静态分析来实现的,因此它的剪裁效果并不是最优的。它有如下两个缺点:不支持WindowsForms和WPF,而对于程序剪裁功能需求... 查看详情

[asp.netcore3框架揭秘]依赖注入:一个mini版的依赖注入框架(代码片段)

在前面的章节中,我们从纯理论的角度对依赖注入进行了深入论述,我们接下来会对.NETCore依赖注入框架进行单独介绍。为了让读者朋友能够更好地理解.NETCore依赖注入框架的设计与实现,我们按照类似的原理创建了一个简易版... 查看详情

ai绘画火爆,以昆仑万维aigc为例,揭秘ai绘画背后的模型算法(代码片段)

AI绘画火爆,以昆仑万维AIGC为例,揭秘AI绘画背后的模型算法一、前言最近AI绘画让人工智能再次走进大众视野。在人工智能发展早起,一直认为人工智能能实现的功能非常有限。通常都是些死板的东西,像是下棋... 查看详情

ai绘画火爆,以昆仑万维aigc为例,揭秘ai绘画背后的模型算法(代码片段)

AI绘画火爆,以昆仑万维AIGC为例,揭秘AI绘画背后的模型算法一、前言最近AI绘画让人工智能再次走进大众视野。在人工智能发展早起,一直认为人工智能能实现的功能非常有限。通常都是些死板的东西,像是下棋... 查看详情

asp.netcore6框架揭秘实例演示[21]:如何承载你的后台服务(代码片段)

借助.NET提供的服务承载(Hosting)系统,我们可以将一个或者多个长时间运行的后台服务寄宿或者承载我们创建的应用中。任何需要在后台长时间运行的操作都可以定义成标准化的服务并利用该系统来承载,ASP.NETCore应用最终也... 查看详情

重磅干货!揭秘波士顿动力背后的专利技术

【导读】本文从波士顿动力背后申请的专利入手,从动力系统、步态分析,详细探讨了波士顿动力机器人背后的技术实现。波士顿动力四足机器人的发展历程(前世今身)相比于轮式或履带机器人,波士顿足式机器人具有更强的... 查看详情

asp.netcore6框架揭秘实例演示[22]:如何承载你的后台服务[补充](代码片段)

借助.NET提供的服务承载(Hosting)系统,我们可以将一个或者多个长时间运行的后台服务寄宿或者承载我们创建的应用中。任何需要在后台长时间运行的操作都可以定义成标准化的服务并利用该系统来承载,ASP.NETCore应用最终也... 查看详情

“猜你喜欢”的背后揭秘--10分钟教你用python打造推荐系统(代码片段)

欲直接下载代码文件,关注我们的公众号哦!查看历史消息即可!话说,最近的瓜实在有点多,从我科校友李雨桐怒锤某男、陈羽凡吸毒被捕、蒋劲夫家暴的三连瓜,到不知知网翟博士,再到邓紫棋解约蜂鸟、王思聪花千芳隔空... 查看详情

免费活动字节跳动背后的音视频技术揭秘

音视频技术在近几年呈现突飞猛进的发展,一方面满足了企业对于业务高速增长的需求,另一方面也为业务的发展创造了更多的可能性。活动介绍10月29日|北京LiveVideoStack将联合火山引擎的5位技术专家在本专题中,展... 查看详情

字节跳动背后的音视频技术揭秘

在过去的一年中,我们可以看到多媒体特别是音视频技术的能力在严峻的挑战下,为各行各业带来了巨大的变化。疫情过后,又会有哪些多媒体新技术、新实践呈现在大众的视野当中?为行业的发展与应用带来哪... 查看详情

华为联手“北斗”4年打磨昆仑玻璃……揭秘mate50背后的技术故事!(代码片段)

整理|朱珂欣  出品|CSDN(ID:CSDNnews)近年来,华为总是自带“热搜”的体质。年初时,“华为又给员工分红了”的话题,引发不少网友的讨论热潮。1月31日,《品牌强国之路》纪录片华为篇上线央视&#... 查看详情

抖音背后的视频体验分析体系与优化技术揭秘

LiveVideoStackCon2022音视频技术大会北京站将于11月4日至5日在北京丽亭华苑酒店召开,本次大会「火山引擎」品牌技术专场重磅加盟,火山引擎视频云团队基于抖音亿级DAU实践,构建了一套能真正体现用户体验优化的指... 查看详情