揭秘.NET Core剪裁器背后的技术
十天前,我发布了对.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的应用场景?欢迎分享。
揭秘.NET Core剪裁器背后的技术的更多相关文章
- .NET Core剪裁器Zack.DotNetTrimmer升级瘦身引擎,并支持剪裁计划的录制和回放
上周,我发布了对.NET Core程序进行瘦身的开源软件Zack.DotNetTrimmer,与.NET Core内置的剪裁器相比,Zack.DotNetTrimmer不仅对程序的剪裁效果更好,而且还 ...
- 新建.Net Core应用程序后引用项一直黄色感叹号怎么办?
我们在vs中创建.Net Core应用程序后,引用项可能出现黄色感叹号,正常情况下,这种黄色感叹号时能在项目创建成功之后迅速消失的,可也有些时候一直不消失,怎么办? 我们可以选中异常的项目,然后右键菜 ...
- 使用ASP.NET Core构建RESTful API的技术指南
译者荐语:利用周末的时间,本人拜读了长沙.NET技术社区翻译的技术标准<微软RESTFul API指南>,打算按照步骤写一个完整的教程,后来无意中看到了这篇文章,与我要写的主题有不少相似之 ...
- Win32 GDI 非矩形区域剪裁,双缓冲技术
传统的Win32通过GDI提供图形显示的功能,包括了基本的绘图功能,如画线.方块.椭圆等等,高级功能包括了多边形和Bezier的绘制.这样app就不用关心那些图形学的细节了,有点类似于UNIX上的X- ...
- 在ASP.NET CORE 2.0使用SignalR技术
一.前言 上次讲SignalR还是在<在ASP.NET Core下使用SignalR技术>文章中提到,ASP.NET Core 1.x.x 版本发布中并没有包含SignalR技术和开发计划 ...
- Asp.Net Core IIS发布后PUT、DELETE请求错误405.0 - Method Not Allowed 因为使用了无效方法(HTTP 谓词)
一.在使用Asp.net WebAPI 或Asp.Net Core WebAPI 时 ,如果使用了Delete请求谓词,本地生产环境正常,线上发布环境报错. 服务器返回405,请求谓词无效. 二.问题 ...
- .NET Standard - 揭秘 .NET Core 和 .NET Standard[转自MSDN]
作为 .NET 系列的最新成员,.NET Core 和 .NET Standard 的概念及其与 .NET Framework 的区别并不十分明确.在本文中,我将准确介绍每个产品及其适用场景. 在详细 ...
- 揭秘IPHONE X刷脸认证的技术奥秘
苹果最新发布的Iphone X具有一个全新的功能叫做刷脸认证,背后的技术其实是生物密码的更新,通过人脸识别取代了传统的指纹识别,大家肯定对这种新技术非常感兴趣,下面我们通过这篇文章为大家介绍人脸识别的 ...
- asp.net.core网站重启后登陆无效问题(部署在IIS)
一.问题 在使用asp.net.core时,把网站发布到IIS后,在后续更新中需要停止网站,然后重启网站,发现已经登陆的用户会退出登陆.过程如下 1.登陆代码(测试) [AllowAnonymous] ...
随机推荐
- 你还不懂java的日志系统吗
一.背景 在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志.经常听到的这样一句话就是"打个日志调试下",没错在日常的开发.调 ...
- Solution -「POI 2011」「洛谷 P3527」MET-Meteors
\(\mathcal{Description}\) Link. 给定一个大小为 \(n\) 的环,每个结点有一个所属国家.\(k\) 次事件,每次对 \([l,r]\) 区间上的每个点点权加上 ...
- Nginx安装启用
安装版本为1.17.8. 1.安装Nginx依赖, pcre. openssl. gcc. zlib(推荐使⽤yum源⾃动安装) yum -y install gcc zlib zlib-devel ...
- WAF、IDS、IPS
WAF:https://blog.csdn.net/gufenchen/article/details/93485351 IDS:https://blog.csdn.net/coldeye/artic ...
- 把SQLAlchemy查询对象转换成字典
1-假设查出来的为单个对象 1-1 在model.py中为模型对象添加字典转换函数: from exts import db class User(db.Model): __tablename__ = ...
- Java8新特性系列-默认方法
Java8 Interface Default and Static Methods 原文连接:Java8新特性系列-默认方法 – 微爱博客 在 Java 8 之前,接口只能有公共抽象方法. 如果不强 ...
- 信而泰IPv6协议一致性测试解决方案
信而泰IPv6协议一致性测试解决方案 背景 中国已经开始逐步进入万物互联的社会,相比原来的手机.电脑等接入网络,万物互联时代接入网络的智能终端会海量增加,而且在万物互联时代,网络的流量巨大,互联的 ...
- BI工具入门:如何做关系数据源的连接?
以往咱们分享的操作步骤都稍微有些复杂,大家跟着步骤操作也有些二丈摸不着头脑,看来简单的操作步骤和功能概念还是有必要普及的,那今天就来说一点简单的入门操作知识,以Smartbi为例子,跟大家说说BI工 ...
- 换行符号(\r\n)的历史
文章来源:https://cloud.tencent.com/developer/article/1730918 \r\n与\n是有区别的. 如果要通用的则是\r\n,因为有些编辑器它不认\n &qu ...
- 基于angularJs坐标转换指令(经纬度中的度分秒转化为小数形式 )
最近项目中,需要用户输入经纬度信息,因为数据库设计的时候,不可能分三个字段来存储这种信息,只能用double类型来进行存储. 计算公式 double r=度+分/60+秒/3600 <!DOC ...