电脑net是个什么软件,net格式用什么软件可以打开

首页 > 科技 > 作者:YD1662024-04-27 00:48:39

c#源码第一行代码:string rootDirectory = Environment.CurrentDirectory;被翻译成IL代码: call string [mscorlib/*23000001*/]System.Environment/*01000004*/::get_CurrentDirectory() /* 0A000003 */

这句话意思是调用 System.Environment类的get_CurrentDirectory()方法(属性会被编译为一个私有字段 对应get/set方法)。

点击视图=>元信息=>显示,即可查看该程序集的元数据。

我们可以看到System.Environment标记值为01000004,在TypeRef类型引用表中找到该项:

电脑net是个什么软件,net格式用什么软件可以打开(25)

注意图,TypeRefName下面有该类型中被引用的成员,其标记值为0A000003,也就是get_CurrentDirectory了。

而从其ResolutionScope指向位于0x23000001而得之,该类型存在于mscorlib程序集。

电脑net是个什么软件,net格式用什么软件可以打开(26)

于是我们打开mscorlib.dll的元数据清单,可以在类型定义表(TypeDef)找到System.Environment,可以从元数据得知该类型的一些标志(Flags,常见的public、sealed、class、abstract),也得知继承(Extends)于System.Object。在该类型定义下还有类型的相关信息,我们可以在其中找到get_CurrentDirectory方法。 我们可以得到该方法的相关信息,这其中表明了该方法位于0x0002b784这个相对虚地址(RVA),接着JIT在新地址处理CIL,周而复始。

元数据在运行时的作用: https://docs.microsoft.com/zh-cn/dotnet/standard/metadata-and-self-describing-components#run-time-use-of-metadata

程序集的规则

上文我通过ILDASM来描述CLR执行代码的方式,但还不够具体,还需要补充的是对于程序集的搜索方式。

对于System.Environment类型,它存在于mscorlib.dll程序集中,demo.exe是个独立的个体,它通过csc编译的时候只是注册了引用mscorlib.dll中的类型的引用信息,并没有记录mscorlib.dll在磁盘上的位置,那么,CLR怎么知道get_CurrentDirectory的代码?它是从何处读取mscorlib.dll的?

对于这个问题,.NET有个专门的概念定义,我们称为 程序集的加载方式。

程序集的加载方式

对于自身程序集内定义的类型,我们可以直接从自身程序集中的元数据中获取,对于在其它程序集中定义的类型,CLR会通过一组规则来在磁盘中找到该程序集并加载在内存。

CLR在查找引用的程序集的位置时候,第一个判断条件是 判断该程序集是否被签名。

什么是签名?

强名称程序集

就比如大家都叫张三,姓名都一样,喊一声张三不知道到底在叫谁。这时候我们就必须扩展一下这个名字以让它具有唯一性。

我们可以通过sn.exe或VS对项目右键属性在签名选项卡中采取RSA算法对程序集进行数字签名(加密:公钥加密,私钥解密。签名:私钥签名,公钥验证签名),会将构成程序集的所有文件通过哈希算法生成哈希值,然后通过非对称加密算法用私钥签名,最后公布公钥生成一串token,最终将生成一个由程序集名称、版本号、语言文化、公钥组成的唯一标识,它相当于一个强化的名称,即强名称程序集。

mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

我们日常在VS中的项目默认都没有被签名,所以就是弱名称程序集。强名称程序集是具有唯一标识性的程序集,并且可以通过对比哈希值来比较程序集是否被篡改,不过仍然有很多手段和软件可以去掉程序集的签名。

需要值得注意的一点是:当你试图在已生成好的强名称程序集中引用弱名称程序集,那么你必须对弱名称程序集进行签名并在强名称程序集中重新注册。

之所以这样是因为一个程序集是否被篡改还要考虑到该程序集所引用的那些程序集,根据CLR搜索程序集的规则(下文会介绍),没有被签名的程序集可以被随意替换,所以考虑到安全性,强名称程序集必须引用强名称程序集,否则就会报错:需要强名称程序集。

.NET Framework 4.5中对强签名的更改:https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/enhanced-strong-naming

程序集搜索规则

事实上,按照存储位置来说,程序集分为共享(全局)程序集和私有程序集。

CLR查找程序集的时候,会先判断该程序集是否被强签名,如果强签名了那么就会去共享程序集的存储位置(后文的GAC)去找,如果没找到或者该程序集没有被强签名,那么就从该程序集的同一目录下去寻找。

强名称程序集是先找到与程序集名称(VS中对项目右键属性应用程序->程序集名称)相等的文件名称,然后 按照唯一标识再来确认,确认后CLR加载程序集,同时会通过公钥效验该签名来验证程序集是否被篡改(如果想跳过验证可查阅https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/how-to-disable-the-strong-name-bypass-feature),如果强名称程序集被篡改则报错。

而弱名称程序集则直接按照与程序集名称相等的文件名称来找,如果还是没有找到就以该程序集名称为目录的文件夹下去找。总之,如果最终结果就是没找到那就会报System.IO.FileNotFoundException异常,即尝试访问磁盘上不存在的文件失败时引发的异常。

注意:此处文件名称和程序集名称是两个概念,不要模棱两可,文件CLR头内嵌程序集名称。

举个例子:

我有一个控制台程序,其路径为D:\Demo\Debug\demo.exe,通过该程序的元数据得知,其引用了一个程序集名称为aa的普通程序集,引用了一个名为bb的强名称程序集,该bb.dll的强名称标识为:xx001。

现在CLR开始搜索程序集aa,首先它会从demo.exe控制台的同一目录(也就是D:\Demo\Debug\)中查找程序集aa,搜索文件名为aa.dll的文件,如果没找到就在该目录下以程序集名称为目录的目录中查找,也就是会查 D:\Demo\Debug\aa\aa.dll,这也找不到那就报错。

然后CLR开始搜索程序集bb,CLR从demo.exe的元数据中发现bb是强名称程序集,其标识为:xx001。于是CLR会先从一个被定义为GAC的目录中去通过标识找,没找到的话剩下的寻找步骤就和寻找aa一样完全一致了。

当然,你也可以通过配置文件config中(配置文件存在于应用程序的同一目录中)人为增加程序集搜索规则:

1.在运行时runtime节点中,添加privatePath属性来添加搜索目录,不过只能填写相对路径:

<runtime>

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

<probing privatePath="relative1;relative2;" /> //程序集当前目录下的相对路径目录,用;号分割

</assemblyBinding>

</runtime>

2.如果程序集是强签名后的,那么可以通过codeBase来指定网络路径或本地绝对路径。

<runtime>

<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

<dependentAssembly>

<assemblyIdentity name="myAssembly"

publicKeyToken="32ab4ba45e0a69a1"

culture="neutral" />

<codeBase version="2.0.0.0"

href="http://www.litwareinc.com/myAssembly.dll" />

</dependentAssembly>

</assemblyBinding>

</runtime>

当然,我们还可以在代码中通过AppDomain类中的几个成员来改变搜索规则,如AssemblyResolve事件、AppDomainSetup类等。

有关运行时节点的描述:https://docs.microsoft.com/zh-cn/dotnet/framework/configure-apps/file-schema/runtime/runtime-element

项目的依赖顺序

如果没有通过config或者在代码中来设定CLR搜索程序集的规则,那么CLR就按照默认的也就是我上述所说的模式来寻找。

所以如果我们通过csc.exe来编译项目,引用了其它程序集的话,通常需要将那些程序集复制到同一目录下。故而每当我们通过VS编译器对项目右键重新生成项目(重新编译)时,VS都会将引用的程序集给复制一份到项目bin\输出目录Debug文件夹下,我们可以通过VS中对引用的程序集右键属性-复制本地 True/Flase 来决定这一默认行为。

值得一提的是,项目间的生成是有序生成的,它取决于项目间的依赖顺序。

比如Web项目引用BLL项目,BLL项目引用了DAL项目。那么当我生成Web项目的时候,因为我要注册Bll程序集,所以我要先生成Bll程序集,而BLL程序集又引用了Dal,所以又要先生成Dal程序集,所以程序集生成顺序就是Dal=>BLL=>Web,项目越多编译的时间就越久。

程序集之间的依赖顺序决定了编译顺序,所以在设计项目间的分层划分时不仅要体现出层级职责,还要考虑到依赖顺序。代码存放在哪个项目要有讲究,不允许出现互相引用的情况,比如A项目中的代码引用B,B项目中的代码又引用A。

为什么Newtonsoft.Json版本不一致?

而除了注意编译顺序外,我们还要注意程序集间的版本问题,版本间的错乱会导致程序的异常。

举个经典的例子:Newtonsoft.Json的版本警告,大多数人都知道通过版本重定向来解决这个问题,但很少有人会琢磨为什么会出现这个问题,找了一圈文章,没找到一个解释的。

比如:

A程序集引用了 C盘:\Newtonsoft.Json 6.0程序集

B程序集引用了 从Nuget下载下来的Newtonsoft.Json 10.0程序集

此时A引用B,就会报:发现同一依赖程序集的不同版本间存在无法解决的冲突 这一警告。

A:引用Newtonsoft.Json 6.0

Func()

{ var obj= Newtonsoft.Json.Obj;

B.Convert();

}

B: 引用Newtonsoft.Json 10.0

JsonObj()

{ return Newtonsoft.Json.Obj;

}

A程序集中的Func方法调用了B程序集中的JsonObj方法,JsonObj方法又调用了Newtonsoft.Json 10.0程序集中的对象,那么当执行Func方法时程序就会异常,报System.IO.FileNotFoundException: 未能加载文件或程序集Newtonsoft.Json 10.0的错误。

这是为什么?

1.这是因为依赖顺序引起的。A引用了B,首先会先生成B,而B引用了 Newtonsoft.Json 10.0,那么VS就会将源引用文件(Newtonsoft.Json 10.0)复制到B程序集同一目录(bin/Debug)下,名为Newtonsoft.Json.dll文件,其内嵌程序集版本为10.0。

2.然后A引用了B,所以会将B程序集和B程序集的依赖项(Newtonsoft.Json.dll)给复制到A的程序集目录下,而A又引用了C盘的Newtonsoft.Json 6.0程序集文件,所以又将C:\Newtonsoft.Json.dll文件给复制到自己程序集目录下。因为两个Newtonsoft.Json.dll重名,所以直接覆盖了前者,那么只保留了Newtonsoft.Json 6.0。

3.当我们调用Func方法中的B.Convert()时候,CLR会搜索B程序集,找到后再调用 return Newtonsoft.Json.Obj 这行代码,而这行代码又用到了Newtonsoft.Json程序集,接下来CLR搜索Newtonsoft.Json.dll,文件名称满足,接下来CLR判断其标识,发现版本号是6.0,与B程序集清单里注册的10.0版本不符,故而才会报出异常:未能加载文件或程序集Newtonsoft.Json 10.0。

以上就是为何Newtonsoft.Json版本不一致会导致错误的原因,其也诠释了CLR搜索程序集的一个过程。

那么,如果我执意如此,有什么好的解决方法能让程序顺利执行呢?有,有2个方法。

第一种:通过bindingRedirect节点重定向,即当找到10.0的版本时,给定向到6.0版本

如何在编译时加载两个相同的程序集?

注意:我看过有的文章里写的一个AppDomain只能加载一个相同的程序集,很多人都以为不能同时加载2个不同版本的程序集,实际上CLR是可以同时加载Newtonsoft.Json 6.0和Newtonsoft.Json 10.0的。

第二种:对每个版本指定codeBase路径,然后分别放上不同版本的程序集,这样就可以加载两个相同的程序集。

电脑net是个什么软件,net格式用什么软件可以打开(27)

如何同时调用两个两个相同命名空间和类型的程序集?

除了程序集版本不同外,还有一种情况就是,我一个项目同时引用了程序集A和程序集B,但程序集A和程序集B中的命名空间和类型名称完全一模一样,这个时候我调用任意一个类型都无法区分它是来自于哪个程序集的,那么这种情况我们可以使用extern alias外部别名。

我们需要在所有代码前定义别名,extern alias a;extern alias b;,然后在VS中对引用的程序集右键属性-别名,分别将其更改为a和b(或在csc中通过/r:{别名}={程序集}.dll)。

在代码中通过 {别名}::{命名空间}.{类型}的方式来使用。

extern-alias介绍: https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/extern-alias

共享程序集GAC

我上面说了这么多有关CLR加载程序集的细节和规则,事实上,类似于mscorlib.dll、System.dll这样的FCL类库被引用的如此频繁,它已经是我们.NET编程中必不可少的一部分,几尽每个项目都会引用,为了不再每次使用的时候都复制一份,所以计算机上有一个位置专门存储这些我们都会用到的程序集,叫做全局程序集缓存(Global Assembly Cache,GAC),这个位置一般位于C:\Windows\Microsoft.NET\assembly和3.5之前版本的C:\Windows\assembly。

既然是共享存放的位置,那不可避免的会遇到文件名重复的情况,那么为了杜绝该类情况,规定在GAC中只能存在强名称程序集,每当CLR要加载强名称程序集时,会先通过标识去GAC中查找,而考虑到程序集文件名称一致但版本文化等复杂的情况,所以GAC有自己的一套目录结构。我们如果想将自己的程序集放入GAC中,那么就必须先签名,然后通过如gacutil.exe工具(其存在于命令行工具中 https://docs.microsoft.com/zh-cn/dotnet/framework/tools/developer-command-prompt-for-vs中)来注册至GAC中,值得一提的是在将强名称程序集安装在GAC中,会效验签名。

GAC工具: https://docs.microsoft.com/en-us/dotnet/framework/tools/gacutil-exe-gac-tool

延伸

CLR是按需加载程序集的,没有执行代码也就没有调用相应的指令,没有相应的指令,CLR也不会对其进行相应的操作。 当我们执行Environment.CurrentDirectory这段代码的时候,CLR首先要获取Environment类型信息,通过自身元数据得知其存在mscorlib.dll程序集中,所以CLR要加载该程序集,而mscorlib.dll又由于其地位特殊,早在CLR初始化的时候就已经被类型加载器自动加载至内存中,所以这行代码可以直接在内存中读取到类型的方法信息。

在这个章节,我虽然描述了CLR搜索程序集的规则,但事实上,加载程序集读取类型信息远远没有这么简单,这涉及到了属于.NET Framework独有的"应用程序域"概念和内存信息的查找。

简单延伸两个问题,mscorlib.dll被加载在哪里?内存堆中又是什么样的一个情况?

应用程序域

传统非托管程序是直接承载在Windows进程中,托管程序是承载在.NET虚拟机CLR上的,而在CLR中管控的这部分资源中,被分成了一个个逻辑上的分区,这个逻辑分区被称为应用程序域,是.NET Framework中定义的一个概念。

因为堆内存的构建和删除都通过GC去托管,降低了人为出错的几率,在此特性基础上.NET强调在一个进程中通过CLR强大的管理建立起对资源逻辑上的隔离区域,每个区域的应用程序互不影响,从而让托管代码程序的安全性和健壮性得到了提升。

熟悉程序集加载规则和AppDomain是在.NET技术下进行插件编程的前提。AppDomain这部分概念并不复杂。

当启动一个托管程序时,最先启动的是CLR,在这过程中会通过代码初始化三个逻辑区域,最先是SystemDomain系统程序域,然后是SharedDoamin共享域,最后是{程序集名称}Domain默认域。

系统程序域里维持着一些系统构建项,我们可以通过这些项来监控并管理其它应用程序域等。共享域存放着其它域都会访问到的一些信息,当共享域初始化完毕后,会自动加载mscorlib.dll程序集至该共享域。而默认域则用储存自身程序集的信息,我们的主程序集就会被加载至这个默认域中,执行程序入口方法,在没有特殊动作外所产生的一切耗费都发生在该域。

我们可以在代码中创建和卸载应用程序域,域与域之间有隔离性,挂掉A域不会影响到B域,并且对于每一个加载的程序集都要指定域的,没有在代码中指定域的话,默认都是加载至默认域中。

AppDomain可以想象成组的概念,AppDomain包含了我们加载的一组程序集。我们通过代码卸载AppDomain,即同时卸载了该AppDomain中所加载的所有程序集在内存中的相关区域。

AppDomain的初衷是边缘隔离,它可以让程序不重新启动而长时间运行,围绕着该概念建立的体系从而让我们能够使用.NET技术进行插件编程。

当我们想让程序在不关闭不重新部署的情况下添加一个新的功能或者改变某一块功能,我们可以这样做:将程序的主模块仍默认加载至默认域,再创建一个新的应用程序域,然后将需要更改或替换的模块的程序集加载至该域,每当更改和替换的时候直接卸载该域即可。 而因为域的隔离性,我在A域和B域加载同一个程序集,那么A域和B域就会各存在内存地址不同但数据相同的程序集数据。

跨边界访问

事实上,在开发中我们还应该注意跨域访问对象的操作(即在A域中的程序集代码直接调用B域中的对象)是与平常编程中有所不同的,一个域中的应用程序不能直接访问另一个域中的代码和数据,对于这样的在进程内跨域访问操作分两类。

一是按引用封送,需要继承System.MarshalByRefObject,传递的是该对象的代理引用,与源域有相同的生命周期。

二是按值封送,需要被[Serializable]标记,是通过序列化传递的副本,副本与源域的对象无关。

无论哪种方式都涉及到两个域直接的封送、解封,所以跨域访问调用不适用于过高频率。

(比如,原来你是这样调用对象: var user=new User(); 现在你要这样:var user=(User){应用程序域对象实例}.CreateInstanceFromAndUnwrap("Model.dll","Model.User"); )

值得注意的是,应用程序域是对程序集的组的划分,它与进程中的线程是两个一横一竖,方向不一样的概念,不应该将这2个概念放在一起比较。我们可以通过Thread.GetDomain来查看执行线程所在的域。

应用程序域在类库中是System.AppDomain类,部分重要的成员有:

获取当前 System.Threading.Thread 的当前应用程序域 public static AppDomain CurrentDomain { get; }

使用指定的名称新建应用程序域 public static AppDomain CreateDomain(string friendlyName);

卸载指定的应用程序域。 public static void Unload(AppDomain domain);

指示是否对当前进程启用应用程序域的 CPU 和内存监视,开启后可以根据相关属性进行监控 public static bool MonitoringIsEnabled { get; set; }

当前域托管代码抛出异常时最先发生的一个事件,框架设计中可以用到 public event EventHandler<FirstChanceExceptionEventArgs> FirstChanceException;

当某个异常未被捕获时调用该事件,如代码里只catch了a异常,实际产生的是 b异常,那么b异常就没有捕捉到。 public event UnhandledExceptionEventHandler UnhandledException;

为指定的应用程序域属性分配指定值。该应用程序域的局部存储值,该存储不划分上下文和线程,均可通过GetData获取。 public void SetData(string name, object data);

如果想使用托管代码来覆盖CLR的默认行为https://msdn.microsoft.com/zh-cn/library/system.appdomainmanager(v=vs.85).aspx

public AppDomainManager DomainManager { get; }

返回域的配置信息,如在config中配置的节点信息 public AppDomainSetup SetupInformation { get; }

应用程序域: https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/application-domains

AppDomain和AppPool

注意:此处的AppDomain应用程序域 和 IIS中的AppPool应用程序池 是2个概念,AppPool是IIS独有的概念,它也相当于一个组的概念,对网站进行划组,然后对组进行一些如进程模型、CPU、内存、请求队列的高级配置。

内存

应用程序域把资源给隔离开,这个资源,主要指内存。那么什么是内存呢?

要知道,程序运行的过程就是电脑不断通过CPU进行计算的过程,这个过程需要读取并产生运算的数据,为此我们需要一个拥有足够容量能够快速与CPU交互的存储容器,这就是内存了。对于内存大小,32位处理器,寻址空间最大为2的32次方byte,也就是4G内存,除去操作系统所占用的公有部分,进程大概能占用2G内存,而如果是64位处理器,则是8T。

而在.NET中,内存区域分为堆栈和托管堆。

堆栈和堆的区别

堆和堆栈就内存而言只不过是地址范围的区别。不过堆栈的数据结构和其存储定义让其在时间和空间上都紧密的存储,这样能带来更高的内存密度,能在CPU缓存和分页系统表现的更好。故而访问堆栈的速度总体来说比访问堆要快点。

线程堆栈

操作系统会为每条线程分配一定的空间,Windwos为1M,这称之为线程堆栈。在CLR中的栈主要用来执行线程方法时,保存临时的局部变量和函数所需的参数及返回的值等,在栈上的成员不受GC管理器的控制,它们由操作系统负责分配,当线程走出方法后,该栈上成员采用后进先出的顺序由操作系统负责释放,执行效率高。

而托管堆则没有固定容量限制,它取决于操作系统允许进程分配的内存大小和程序本身对内存的使用情况,托管堆主要用来存放对象实例,不需要我们人工去分配和释放,其由GC管理器托管。

为什么值类型存储在栈上

不同的类型拥有不同的编译时规则和运行时内存分配行为,我们应知道,C# 是一种强类型语言,每个变量和常量都有一个类型,在.NET中,每种类型又被定义为值类型或引用类型。

使用 struct、enum 关键字直接派生于System.ValueType定义的类型是值类型,使用 class、interface、delagate 关键字派生于System.Object定义的类型是引用类型。

对于在一个方法中产生的值类型成员,将其值分配在栈中。这样做的原因是因为值类型的值其占用固定内存的大小。

C#中int关键字对应BCL中的Int32,short对应Int16。Int32为2的32位,如果把32个二进制数排列开来,我们要求既能表达正数也能表达负数,所以得需要其中1位来表达正负,首位是0则为 ,首位是1则为-,那么我们能表示数据的数就只有31位了,而0是介于-1和1之间的整数,所以对应的Int32能表现的就是2的31次方到2的31次方-1,即2147483647和-2147483648这个整数段。

1个字节=8位,32位就是4个字节,像这种以Int32为代表的值类型,本身就是固定的内存占用大小,所以将值类型放在内存连续分配的栈中。

托管堆模型

而引用类型相比值类型就有点特殊,newobj创建一个引用类型,因其类型内的引用对象可以指向任何类型,故而无法准确得知其固定大小,所以像对于引用类型这种无法预知的容易产生内存碎片的动态内存,我们把它放到托管堆中存储。

托管堆由GC托管,其分配的核心在于堆中维护着一个nextObjPtr指针,我们每次实例(new)一个对象的时候,CLR将对象存入堆中,并在栈中存放该对象的起始地址,然后该指针都会根据该对象的大小来计算下一个对象的起始地址。不同于值类型直接在栈中存放值,引用类型则还需要在栈中存放一个代表(指向)堆中对象的值(地址)。

而托管堆又可以因存储规则的不同将其分类,托管堆可以被分为3类:

加载程序集就是将程序集中的信息给映射在加载堆,对产生的实例对象存放至垃圾回收堆。前文说过应用程序域是指通过CLR管理而建立起的逻辑上的内存边界,那么每个域都有其自己的加载堆,只有卸载应用程序域的时候,才会回收该域对应的加载堆。

而加载堆中的高频堆包含的有一个非常重要的数据结构表---方法表,每个类型都仅有一份方法表(MethodTables),它是对象的第一个实例创建前的类加载活动的结果,它主要包含了我们所关注的3部分信息:

那么,实例一个对象,CLR是如何将该对象所对应的类型行为及信息的内存位置(加载堆)关联起来的呢?

原来,在托管堆上的每个对象都有2个额外的供于CLR使用的成员,我们是访问不到的,其中一个就是类型对象指针,它指向位于加载堆中的方法表从而让类型的状态和行为关联了起来, 类型指针的这部分概念我们可以想象成obj.GetType()方法获得的运行时对象类型的实例。而另一个成员就是同步块索引,其主要用于2点:1.关联内置SyncBlock数组的项从而完成互斥锁等目的。 2.是对象Hash值计算的输入参数之一。

电脑net是个什么软件,net格式用什么软件可以打开(28)

上一页34567下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.