标签:
比较认同一个专业的说法,称对象之间的调用为 “消息传递”,正如其描述“通过发送和接受消息”。为什么说其专业?因为对于单机开发,调用者(用户)和被调用者(服务)处于同一台计算机内存之中,CPU执行指令仅仅是根据地址取出内存数据进行处理。但实质上仍然是“消息传递”,而分布式应用通过socket实现不同协议间的远程服务调用,更加直观。
有一个知识点十分重要,这被一些新手所忽视,导致他们有时很难理解一些技术书籍的内存原理分析。就是程序从编译到运行的过程。借此文,我来解释一下,如有错误,望大家指出,一定要指出。
当我们依赖google或者CTRL+C、CTRL+V编写完成代码之后,点击发布或生成,本机安装的编译器会对代码文件进行分析编译(C/C++针对特定CPU编译过程发生在宿主服务器上)。针对与.NET,机器上安装的CLR虚拟机,会让其内置的对应语言的编译器将代码编译为版本相符的MSIL语言,并且保存文件格式为dll,这个dll称之为托管程序集,也是一个PE文件。
.PE文件一般包含以下几个部分:
PE头
Data Directory(数据目录)
.text segment
.data segment
.rdata segment
.idata segment
.edata segment
....其他部分不再列出。
“那么CLR的托管程序集是怎么样的?我听说会有一个CLR头?还有元数据以及IL代码?(CLR via C#)” 当然,这是准确无误的,《CLR via C#》圣经读物,怎会轻易出错?
在windows XP之前的操作系统,这些section都被保存在 PE 文件中的.text segment中,并且.text 块中还保存了“非托管的存根函数”
。
什么意思呢?你可以理解为是一段在.net编译时(非.net编译可能是其他的存根代码或者没有存根代码)加入的非托管代码
。而PE头中的某一块域(其实是 AddressOfEntryPoint 域
)指向这段代码,当windows加载器加载PE文件时(托管程序集),便会执行这段代码,这段代码会调用_CoreExeMain
或_CoreDllMain
的代码(前者针对exe文件,后者针对dll文件
),由于这两个函数都是导入函数(依赖别的程序集),则windows加载包含此函数的mscoree.dll
,再去找到 data directory
(数据目录,这个目录也是在.net编译时生成的)。data directory包含了CLR头的位置和大小,在根据CLR头的位置在.text块中找到CLR头,CLR头包含了CLR的版本信息、程序集签名等很多信息,根据这些信息,非托管存根代码调用对应的CLR,并启动CLR,注意:如果CLR已经启动,则CLR直接加载程序集到内存。
但是XP以及之后的操作的系统,对windows加载器进行了升级,使其能够自动识别.net程序集,无需通过“非托管存根函数”来调用“_CoreExeMain”或“_CoreDllMain”,而是直接根据CLR头启动CLR。
那么,至此,.NET 的PE文件的编译和加载启动过程已经清晰了。接下来总结一下:
XP 之前的.net程序集PE文件:
PE文件各部分名称 | 用处 | 说明 |
---|---|---|
PE Header | 存储了PE文件格式(exe还是dll),创建信息,entry point(入口点) data directory 位置信息等 | |
data directory(数据目录) | 包含了CLR头的位置和大小 | 注意,这个是.net编译时添加的块 |
.text | 包含了CLR头,元数据,IL代码以及 非托管存根函数 | 注意,这个非托管存根函数也是.net编译时加入的 |
.idata | 导入函数,即此PE运行依赖的其他程序集的函数 _CoreExeMain和_CoreDllMain函数便是从mscoree.dll导入的函数 | |
.edata | 导出函数,即此PE公开的可对外服务的函数 | |
.rdata | 常量以及只读数据 | |
..... | 其他块,不赘述 |
XP以及之后操作系统.NET的PE文件:
PE文件各部分名称 | 用处 | 说明 |
---|---|---|
PE Header | 存储了PE文件格式(exe还是dll),创建信息等。如果程序集包含非托管代码, PE头还存储非托管代码的一些信息(entry point) | |
CLR Header | 记录了CLR版本,Main方法(如果有),还记录了元数据、IL代码 的位置和大小 | 注意:不在生成 data directory部分 |
.text | 元数据,IL代码 | 注意: 1.不在生成专门用于调用_CoreExeMain和_CoreDllMain函数 的非托管存根函数 2.CLR头不在.text中,单独被放置为一个部分。 3.元数据,IL代码还是在.text 段中的,只是CLR via c#书中 为了更好说明,将其提取出来了。 |
.idata | 导入函数,即此PE运行依赖的其他程序集的函数 _CoreExeMain和_CoreDllMain函数便是从mscoree.dll导入的函数 | |
.edata | 导出函数,即此PE公开的可对外服务的函数 | |
.rdata | 常量以及只读数据 | |
..... | 其他块,不赘述 |
CLR头是用来启动CLR,那么元数据和IL代码是用来干什么的呢?这里不再详述,比较麻烦,建议阅读《CLR via C#》。我这里只提供书中的一幅图:
从这幅图中可以看出,元数据其中之一用途就是当IL代码再被JIT(即时编译)时,如果IL引用到了一个类型则会根据元数据去正确的创建类型对象。 当然,还有十分重要的一个作用就是:为开发人员提供反射机制。
应用程序域 应用程序域的作用,我想大家都应该知晓,主要是为了隔离代码影响。比如IIS中启动多个web应用,每个应用的错误不会影响到其他站点。这就是应用程序域的典型案例。 当CLR被启动后,首先会创建一个系统级别应用程序域。该程序域的创建由CLR主导,对开发者完全透明,系统应用程序域的主要功能:
我们注意到,系统应用程序域会自动创建共享应用程序域
和默认应用程序域
,这对于开发人员也是透明的,也就是说一个.net程序一旦启动,就会有三个应用程序域。
系统应用程序域的作用已经说明了,而共享应用程序域主要是用于加载一些与 默认应用程序域无关 的代码,即“非用户代码”
,比如mscoree.dll
,一些system命名空间的类型等,由其名称可知,其内部加载的成程序集可被所有应用程序域访问。
默认应用程序域就是我们代码(用户代码)执行的地方
,开发者还可以通过系统应用程序域创建多个应用程序域,但往往很少有这种需求。
上面说完了程序集的编译以及加载原理,下面来主要说一下.net程序内存的工作方式。
现在假设我们的程序已经编译OK,并且被保存再磁盘上的一处位置。我们准备双击运行它,首先这必须是一个exe文件,否则,无法直接运行。随后,这个exe文件被加载到内存的代码区,(这里也说明一下,程序集尽量实现单一职责,否则加载了一个很大的程序集却只用小部分功能,简直是浪费内存!)当exe文件被加载到内存代码区
后,windows加载器开始加载CLR
,而后,CLR启动后根据exe文件的CLR头信息中的Main方法入口
,从而进入用户代码。从Main方法开始,IL代码就被不断取出交给JIT即时编译成机器指令,从而达到功能实现。
OK,有人会问,所有的代码都会一次性交给IL进行即时编译吗?No!面向对象只是我们进行业务设计时更易建模领域模型,对于计算机,它永远只知道,你让它做什么,他就去做什么。绝不会多做任何一点儿。所以,一切都是面向函数!函数即指令!一个dll,加载到内存,没有被调用,没有main方法,它则永不会被JIT!当然我们只是举例,没有被调用,也根本不可能被加载到内存! 而Main方法就是所有程序的入口函数,Main方法其内部的所有代码都会被JIT(Main方法所在线程即主线程)。 如下文件:
class Program{
static void main(string[] args){
Console.WeiteLine("Test");
int a=1;
int b=2;
int c=add(a,b);
Console.WriteLine(c);
}
static int add(int a,int b){
return a+b;
}
}
当CLR调用Main方法时,首先将Main方法的IL指令交给JIT翻译为机器指令,此时,发现Main方法属于Program类型,且是静态方法,便会在内存堆上分配空间,根据元数据中的TypeDef创建类型内部结构,即Type对象
。注意:非program对象,因为并没有new指令。然后将Main方法的IL指令翻译为CPU指令,紧接着进入Main方法内部的其他IL代码,从而正式开始应用程序主线程的运行。内存分配图如下:
1.CLR调用Main方法;
2.JIT到代码区查找元数据和IL代码
3.JIT根据找到的信息编译出CPU指令,static构造函数和main函数
4.编译后的Main函数开始执行,进入应用程序主线程
5.调用add方法;(不应该指向 JIT?待思考)
6.JIT将add方法的IL代码翻译为CPU指令并执行。
注意:JIT有非常强大的缓存功能,也就是说相同的代码在同一应用程序域不会被JIT第二次,而是使用之前JIT出的CPU指令
本文主要针对.NET的运行原理进行了分析,但运行在虚拟机之上的语言的编一个运行过程大致相同,可能由于一些语言的性质不同,会做一些编译优化,比如F#函数式语言,scala多范式语言等。特别强调:本文为作者个人多年工作经验和书籍阅读带来的个人理解,并通过博文进行分享,有一个目的是希望有更加清晰明白的人能指出其中的不足或错误,以免误人子弟,同时学习进步。
有什么疑问,请留言,我们一起讨论,谢谢!
参考:
《CLR via c#》
《.NET高级调试》
标签:
原文地址:http://www.cnblogs.com/phoozyan/p/4998727.html