揭秘.NET Core剪裁器背后的技术

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

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

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

微软提供了用于对.NETCore的运行时行为进行分析的库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;
usingMicrosoft.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 = newProcessStartInfo(filepath);
psInfo.UseShellExecute = true;
using Process? p = Process.Start(psInfo);//启动程序
var providers = newList<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 = newEventPipeEventSource(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")]publicvoid AA(){Console.WriteLine("hello");}
}

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

public class Class1
{publicvoid AA(){Console.WriteLine("hello");}
}

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

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

ModuleWriterException: 'A method was removedthat 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(newInstruction(OpCodes.Nop) { Offset = 0 });
method.Body.Instructions.Add(newInstruction(OpCodes.Ldnull) { Offset = 1 });
method.Body.Instructions.Add(newInstruction(OpCodes.Throw) { Offset = 2 });

         最后三行添加的IL代码就是对应thrownull这行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的应用场景?欢迎分享。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/290196.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C和指针之动态内存分配之编程练习3

1、问题 编写一个函数,从标准输入读取一个字符串,把字符串复制到动态内存分配的内存中,并返回该字符串的拷贝,这个函数不应该对读入字符串的长度作任何限制! 2、代码实现 #include <stdio.h> #include <stdlib.h>#define OFFSET 5char *my_strcpy…

freetds 移植

移植freetds主要是为了能够在linux下&#xff0c;使用C语言访问微软的sqlserver数据库。 参考连接 http://blog.csdn.net/neighbor1000/article/details/8824084 http://blog.csdn.net/lovehere33/article/details/41118405 在ubuntu上安装 从官网下载最新的稳定版本。 http://…

优秀程序员的 18 大法则【转载】

DRY原则 不要重复&#xff08;Don’t repeat yourself&#xff09;——程序设计中一个最根本的原则就是要避免重复。许多编程结构&#xff08;比如循环、函数、类等&#xff09;的存在就是为了避免重复。一旦重复&#xff08;例如&#xff0c;一个长表达式&#xff0c;一系列语…

java邮件系统(java邮件收发系统源代码和下载地址)

2019独角兽企业重金招聘Python工程师标准>>> 本软件包包括源文件和可执行的jar文件 项目下载地址&#xff1a; 下载 1. 运行方式 A可以直接运行jar文件&#xff08;电脑上必须安装jdk1.6而且关联jar文件&#xff09; B可以用eclipse导入源文件然后运行 2功能简介…

两对光纤收发器用网线连接_为什么现在的人不喜欢用网线,反而更爱用光纤来传输呢?涨知识了...

随着通信技术的不断发展&#xff0c;信号传输介质已从原来的同轴电缆逐渐变为光纤。光纤传输完全满足大容量数据通信正确&#xff0c;可靠&#xff0c;高速传输和处理的要求&#xff0c;已成为世界上主要的通信方式。本文主要详细介绍光纤传输的基本知识&#xff0c;希望对您有…

openGauss学习笔记-170 openGauss 数据库运维-备份与恢复-导入数据-更新表中数据-使用合并方式更新和插入数据

文章目录 openGauss学习笔记-170 openGauss 数据库运维-备份与恢复-导入数据-更新表中数据-使用合并方式更新和插入数据170.1 前提条件170.2 操作步骤 openGauss学习笔记-170 openGauss 数据库运维-备份与恢复-导入数据-更新表中数据-使用合并方式更新和插入数据 在用户需要将…

C和指针之动态内存分配之编程练习4

1、问题 4.编写一个程序&#xff0c;按照下图中的样子创建数据结构&#xff0c;最后三个对象都是动态分配的结构。第一个对象则可能是一个静态的指向结构的指针。你不必使这个程序过于全面--我们将在下一章讨论这个结构。 2、代码实现 #include <stdio.h> #include <s…

.NET 6 攻略大全(四)

点击上方蓝字关注我们&#xff08;本文阅读时间&#xff1a;15分钟)接上篇内容&#xff0c;本篇文章将介绍&#xff1a;DependentHandle 现已公开、RyuJIT、即用型代码/Crossgen 2、.NET 诊断&#xff1a;EventPipe、SDK 的相关攻略。 DependentHandle 现已公开该 DependentHan…

[原创]同一个Tomcat,配置多个context、多个Host

需求前提&#xff1a; 系统结束后&#xff0c;需要部署到服务器上。 目前只可以映射到一个固定IP的非80端口。 而server端和web端都要暴露到外网。 所以配置两个context&#xff0c;使得client应用不需要添加服务名&#xff0c;直接使用IP即可访问&#xff1b;server可以通过ht…

[No000022]他们说:得诺贝尔奖到底有多难?

转载于:https://www.cnblogs.com/Chary/p/No000022.html

java操作redis简单学习3

2019独角兽企业重金招聘Python工程师标准>>> package com.hanchao.testredis;import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Set;import redis.clients.jedis.Jedis;import com.alibaba.fastjson.JSON; import com.al…

Xamarin效果第三篇之手机底部弹窗

前面一篇文章把基本的大框架搞定了,这不再逐个去实现里面的细节;今天主要是分享点击了CollectionView内的点点点然后从手机底部弹出一个可以交互的窗口;直接看看最终实现的效果:作为初来乍到的小萌新只能求助万能的群友让大佬们给指条光明通畅的大道,不然容易跑偏;最终给的方向…

自定义sql_一个简单易用的开源BI软件,专为SQL用户设计的开源库

poli一个易于使用的SQL报告应用程序&#xff0c;专为SQL爱好者而设计。SQL中的电源数据分析&#xff0c;可获得更快的业务洞察力。特性⚡️ 自托管和轻松设置平台独立的Web应用程序 单个JAR文件单个SQLite DB文件。在5分钟内启动并运行。连接任何支持JDBC驱动程序的数据库Postg…

Android之ndk编译出现这个错误error: unused variable ‘a‘ [-Werror=unused-variable]

1、问题 在jni里面就加了一行代码 编译的时候出现这个错误 error: unused variable a [-Werrorunused-variable] 2、解决办法 找到相应的Makefile文件 LOCAL_CFLAGS -Wall -Werror 去掉上面的 -Werror&#xff0c;改成如下 LOCAL_CFLAGS -Wall 就可以了

ELK 中的elasticsearch 集群的部署

本文内容 背景ES集群中第一个master节点ES slave节点本文总结 Elasticsearch&#xff08;以下简称ES&#xff09;搭建集群的经验。以 Elasticsearch-rtf-2.2.1 版本为例。 我搭过三个集群&#xff1a;研究ELK时搭了一个&#xff1b;测试环境搭了一个&#xff1b;生产环境搭了一…

Group Box组合框的简单使用 [大三TJB_708]

http://blog.csdn.net/misskissc/article/details/9317783 Group Box组合框的简单使用 [大三TJB_708] 转载于:https://www.cnblogs.com/chuangyiyuan/p/4885637.html

.NetCore中EF Core为迁移的数据库表统一添加前缀

在项目开发的过程中我们往往需要将项目数据库中的表添加一个统一的前缀。我们为什么要添加表前缀呢&#xff1f;有的可能是公司规定&#xff0c;更多的原因是项目和业务的区分。每个项目针对不同的需求或业务场景&#xff0c;追加相应的标识。当项目到达一定规模后&#xff0c;…

Java对象生命周期

目录 1. 创建阶段(Created)2. 应用阶段(In Use)3. 不可见阶段(Invisible)4. 不可达阶段(Unreachable)5. 收集阶段(Collected)6. 终结阶段7. 对象空间的重新分配1. 创建阶段(Created) 为对象分配存储空间开始构造对象从父类到子类对static成员进行初始化父类成员变量按照顺序初始…

C和指针之动态内存分配常见问题和总结

1、动态内存分配常见问题 1) 结构体成员指针未初始化 2)为指针分配内存太小 3)内存分配成功,但是没初始化 4)内存越界,内存泄漏,free多次或者释放之后没有设置为NULL 2、测试代码 #include <stdio.h> #include <stdlib.h> #inclu…

echarty轴自定义显示不全_表格打印不全怎么办?这招超简单!

私信回复关键词【福利】~获取丰富办公资源&#xff0c;助你高效办公早下班&#xff01;大家好&#xff0c;我是小E~最近&#xff0c;秋叶 Excel 学习班花生 同学&#xff0c;从同事那里收到了一份开会要用的表格&#xff0c;需要马上就要打印出来。会议主题是&#xff0c;从五个…