.Net学习笔记(一) —— .NET平台结构

为了得到一些东西,我们必须放弃一些东西,在每个人的人生天平上,孰重孰轻,只有我们自己知道,但是一旦选择了,我们就要尽量去做好它,保留我们内心中那少少值得骄傲的地方。

                                                                                                                                                                                                        ——————————-学习题记

看到小麟写的很好,就拿过来。大家一起共勉。毕业到工作,有4个月了,突然发现有些不是自己想要的。即便现在的环境和安逸和轻松。谈到.net,或许自己连个菜鸟都算不上。但,为了做自己喜欢的事,从现在起,我努力把自己变为一只菜鸟。《.net框架程序设计》是一本非常好的书,我通过学习此书,顺便做点笔记,尽量用自己的理解。笔记中或许会有许多错误,有许多不正确的认识。也请新人,菜鸟,老鸟,高手们多多指教。

 

一 平台介绍

 

NET Framework 具有两个主要组件:公共语言运行库和 .NET Framework 类库。

公共语言运行库是 .NET Framework 的基础。公共语言运行库管理内存、线程执行、代码执行、代码安全验证、编译以及其他系统服务。这些功能是在公共语言运行库上运行的托管代码所固有的。代码管理的概念是运行库的基本原则。以运行库为目标的代码称为托管代码,而不以运行库为目标的代码称为非托管代码。

.NET Framework 的另一个主要组件是类库,它是一个综合性的面向对象的可重用类型集合,您可以使用它开发多种应用程序,这些应用程序包括传统的命令行或图形用户界面 (GUI) 应用程序,也包括基于 ASP.NET 所提供的最新创新的应用程序(如 Web 窗体和 XML Web services)。

 

二 将源文件编译为托管模块

 

.net实现了跨平台,语言无关,这些都是因为他采用了CLR,无论你使用VB还是C#或是其他CLR所支持的语言,都会通过相应的编译器把源文编译为一个中间代码,MSIL。MSIL是一种接近汇编的语言。通过编译的模块是一个托管模块(MSIL只是一部分)。托管模块是一个需要CLR才能运行的标准WINDOWS可移植可执行文件(PE文件)。如果想在其他OS上也能运行,那么就需要其他系统也能支持这种PE文件。

一个PE文件包括了:

PE表头:它主要描述的是这个PE文件的类型,包括CUI,GUI,DLL(这里的DLL不同于传统的动态连接库,他是.net中的程序集的概念),文件创建的时间,以及一些本地的CPU信息。

CLR表头:因为这个PE文件需要CLR才能执行,所以包括了托管模块的一些信息,如需要CLR的版本号,托管模块代码的入口点(Main方法)的标记,模块的源数据,资源,强命名等信息。

元数据:元数据是以表格的形式存在的,用来描述程序中所定义和引用的各种类型,变量,方法,事件。它使得程序的代码有了自描述性,他是。他省去了编译时(JIT编译)对头文件和库文件的需求,因为编译器可以直接从元数据表中获得需要引用的类型和成员。而在写程序时的 智能感知技术也是通过分析元数据表实现的。利用它。垃圾回收器可以跟踪对象。(常用的元数据表可以自己查下,这里就不列出来了)。

IL代码:这快就是通过CLR所支持的编译器,把源代码编译成IL代码,也就是实际的程序代码,但这个代码要运行,就需要上面的信息。在执行此PE文件时,CLR会把IL代码编译成真正的CPU指令,进行执行。

 

 

 三:将托管模块组合为程序集

 

实际上CLR处理的不是托管模块,而是处理程序集。所谓的程序集,我个人理解就是,一个或多个托管模块,类型定义文件和资源的文件集合。可以认为程序集是对托管模块的封装,封装的同时加入了另一个新的元数据表集—-清单。他描述了程序集中的一些信息,包括程序集的名称,版本,语言标记,包含的资源,包括的各个模块信息等等。使用程序集,有助于部署程序,还可以把资源同代码放在一起,而不必欠到程序中,还可以把不同语言的模块放在一起。

让我们回记下,之前先把源代码编译成模块,模块中元数据描述了模块中定义和引用的类型等信息。然后把模块组成一个程序集,程序集中包括的模块,以及描述其中的模块信息和程序集的信息。

在使用VS进行编译时,是直接编译成程序集的。让我们在看看前面编译成托管模块的时候,模块中不包含 清单元数据。因此他是不能被CLR执行的,而只有连接成程序集后,有了清单,才能被执行。而且VS下是不能把多个模块和资源编译到一个程序集合里的,也就是说不支持多文件程序集,所以我们要利用一些工具。

 

方法1:用CSC.EXE把托管模块加入到一个程序集中

 

//CSC编译器可以把源文件直接便宜为一个程序集,也可以把他编译为一个模块
//支持把一个模块添加到一个程序集中


csc /t:module ex1.cs   //把ex1.cs源文件编译为一个模块(而非程序集),得到的模块名为ex1.netmodule

csc /out:ex.dll /t:library /addmodule:ex1.netmodule ex2.cs       //这里是把ex2.cs编译为程序集,同时把模块ex1,加入其中。

通过上面,一共生成了2个文件,ex1.netmodule模块和ex.dll的程序集(只是和DLL扩展名相同)。实际上把模块加入程序集的过程,只是把模块的信息加入到了程序集的清单中,而不是把整个模块放入到其中。这样做的好处就是,可以把不常用的代码放到ex1中,这样在不使用时是不会调用ex1模块的,减少了部署程序时文件大小,特别是网络部署时,如不使用,可以不下载ex1模块。

 

方法2:用AL.EXE把托管模块组合成程序集

 

这就需要用到程序集连接器AL。AL作用是只生成清单而不包括其他任何类容的DLL和EXE文件。但大多数情况是生成DLL文件,因为生成的EXE文件中,没有Main函数入口。虽然可以通过AL.EXE /main命令行来,但不是很有用。

//把前面的ex1,ex2放在一个程序集里

//生成2个托管模块(不包含清单,不能被CLR运行)
csc /t:module ex1.cs
csc /t:module ex2.cs 

//利用AL,连接2个模块,并产生清单
al /out:ex.dll /t:library ex1.netmoudle ex2.netmoudle

经过上面一共产生了3个文件,ex1模块,ex2模块和ex程序集合。在程序集中不包含任何IL代码,他没有把2个模块放入程序集,而只是在程序集清单中记录了模块的信息。CLR能处理的是ex.dll,通过清单找到2个模块进行调用。

通过上面,看到了什么是程序集,他和托管模块的区别。对于只包含一个托管模块的程序集而言,他就是在模块中加入了一个清单,使他成为了一个程序集,而非用个把清单和模块装在程序集(可以看作一个外壳)里。而对于包含多个模块的程序集,他则是把多个模块和资源还有清单放到程序集(外壳)中。一个托管模块是一个PE,当然一个程序集也是一个PE。

当然,程序集中也可以放一些资源文件,可以使用al /line,al /embed,来添加资源的引用或是把文件直接欠入到程序集中。

下面是分别用AL和CSC把2个CS文件(ex1.cs ,ex2.cs)编译到一个程序集中,大家可以看下差别:

    (左图)(右图)

上面左图是使用CSC直接把2个源文件编译到同一个程序集中,于是其中包含了2个文件IL代码和元数据,然后元数据中还有一个清单,是存储整个程序集相关信息的。

在看看右图,右边是先把2个源文件编译成2个模块,其中模块中包含了个自的IL代码和元数据,然后通过AL,把2个连接起来,可以看带程序集里没有任何IL和模块的元数据,只有一个清单,包含了程序集信息,以及连接的2个模块信息。

具体不同,通过ILDASM来查看元数据就很清楚了。

ps:以上工具默认在 C:/WINDOWS/Microsoft.NET/Framework/.net版本目录

 

四:运行可托管代码

 

当源程序被编译成中间代码,成为程序集后(exe文件),就可以被CLR运行了。因为程序集是需要CLR的,所以,在机器上运行他,必须安装CLR。也就是为什么要装.net 框架才能运行.net程序。微软提供了NET Framework 下载,装了SDK或VS的会自动安装。

当生成exe程序集时,编译器/连接器会产生一些特殊信息,嵌入到程序集的PE文件表头及各个组成文件的.text部分。当EXE文件被调用时,这些信息将导致CLR被加载并初始化。CLR确定程序集的入口点,在MAIN函数被执行之前,CLR检查MAIN中代码应用到的所有类型。这会导致CLR分配一个内部存储结构,用于管理对所引用的类型的访问。其中每个条目保存了每个方法实现代码的地址。CLR初始化时,把这些条目设置为一个函数。

然后开始执行MAIN,要调用其中的一个方法时,找到他对应的函数条目,函数把他对应的实现代码转为本地的CPU指令。这个编译过程是又JIT编译器来完成的。本地CPU指令保存在一个动态分配的内存区域中。然后这个函数会跳转到此区域,来执行这些CPU指令,执行完成后,跳转回MAIN函数。而当第二次在执行这个方法时,就会直接执行CPU指令,而不需要在进行编译了。但因为是临时区域,一旦程序关闭,这些指令也会丢失。

而在编译成本地CPU指令的同时,会对IL代码进行安全检查和验证。主要检查的内容包括:不能从为分配的内存区域中读取数据,每个方法必须传入正确的参数个数,类型也需要匹配,每个方法返回值必须被正确使用等等。托管模块中的元数据包含了所有需要被验证的方法和类型信息,如果IL代码被认为‘不安全’,系统会抛出一个验证不合法的异常,阻止程序继续进行。(注意:C#,VB.NET编译器产生的代码都是安全的,这些代码是可以被验证++的。但在其他语言中,如C++可以利用unsafe关键字来创建不能被CLR验证的代码,在系统配置中可以选择也把不可验证的代码编译成本地CPU指令)

 

具体的运行过程大家可以自行查阅。

 

 

五:ILDASM

 

原文件经过编译,成为了MSIL代码。而IL代码是可以通过ILDASM这样一个工具进行反编译,来查看程序集其中包括类型,方法,元数据等信息。这些工具的使用大家可以自行查阅

//以下为一个简单的程序的元数据信息。

//程序
public class app
{
    static public void Main(System.String[] args)
    {
        System.Console.WriteLine("Hello");
    }
}



//利用ildasm /adv App.exe,查看到的元数据信息
//托管模块清单
copeName : App.exe
MVID      : {0B76BBCA-3B45-4328-97C1-D261BB06A449}
===========================================================
Global functions
-------------------------------------------------------

Global fields
-------------------------------------------------------

Global MemberRefs
-------------------------------------------------------

TypeDef #1  
-------------------------------------------------------
    TypDefName: app  (02000002)
    Flags     : [Public] [AutoLayout] [Class] [AnsiClass]  (00100001)
    Extends   : 01000001 [TypeRef] System.Object
    Method #1 [ENTRYPOINT]
    -------------------------------------------------------
        MethodName: Main (06000001)
        Flags     : [Public] [Static] [HideBySig] [ReuseSlot]  (00000096)
        RVA       : 0x00002050
        ImplFlags : [IL] [Managed]  (00000000)
        CallCnvntn: [DEFAULT]
        ReturnType: Void
        1 Arguments
            Argument #1:  SZArray String
        1 Parameters
            (1) ParamToken : (08000001) Name : args flags: [none] (00000000)

    Method #2 
    -------------------------------------------------------
        MethodName: .ctor (06000002)
        Flags     : [Public] [HideBySig] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor]  (00001886)
        RVA       : 0x00002068
        ImplFlags : [IL] [Managed]  (00000000)
        CallCnvntn: [DEFAULT]
        hasThis 
        ReturnType: Void
        No arguments.


TypeRef #1 (01000001)
-------------------------------------------------------
Token:             0x01000001
ResolutionScope:   0x23000001
TypeRefName:       System.Object
    MemberRef #1
    -------------------------------------------------------
        Member: (0a000003) .ctor: 
        CallCnvntn: [DEFAULT]
        hasThis 
        ReturnType: Void
        No arguments.

TypeRef #2 (01000002)
-------------------------------------------------------
Token:             0x01000002
ResolutionScope:   0x23000001
TypeRefName:       System.Diagnostics.DebuggableAttribute
    MemberRef #1
    -------------------------------------------------------
        Member: (0a000001) .ctor: 
        CallCnvntn: [DEFAULT]
        hasThis 
        ReturnType: Void
        2 Arguments
            Argument #1:  Boolean
            Argument #2:  Boolean

TypeRef #3 (01000003)
-------------------------------------------------------
Token:             0x01000003
ResolutionScope:   0x23000001
TypeRefName:       System.Console
    MemberRef #1
    -------------------------------------------------------
        Member: (0a000002) WriteLine: 
        CallCnvntn: [DEFAULT]
        ReturnType: Void
        1 Arguments
            Argument #1:  String

//程序集清单

Assembly
-------------------------------------------------------
    Token: 0x20000001
    Name : App
    Public Key    :
    Hash Algorithm : 0x00008004
    Major Version: 0x00000000
    Minor Version: 0x00000000
    Build Number: 0x00000000
    Revision Number: 0x00000000
    Locale: <null>
    Flags : [SideBySideCompatible]  (00000000)
    CustomAttribute #1 (0c000001)
    -------------------------------------------------------
        CustomAttribute Type: 0a000001
        CustomAttributeName: System.Diagnostics.DebuggableAttribute :: instance void .ctor(bool,bool)
        Length: 6
        Value : 01 00 00 01 00 00                                >                <
        ctor args: ( <can not decode> )


AssemblyRef #1
-------------------------------------------------------
    Token: 0x23000001
    Public Key or Token: b7 7a 5c 56 19 34 e0 89 
    Name: mscorlib
    Major Version: 0x00000001
    Minor Version: 0x00000000
    Build Number: 0x00001388
    Revision Number: 0x00000000
    Locale: <null>
    HashValue Blob:
    Flags: [none] (00000000)


User Strings
-------------------------------------------------------
70000001 : ( 5) L"Hello"

补充:

 

1:何防止别人通过反向工程获得我的代码?

目前唯一的办法是运行带有 /owner 选项的 ilasm。这样生成的元件的 IL 不能通过 ildasm 来查看。然而,意志坚定的代码破译者能够破解 ildasm 或者编写自己的 ildasm 版本,所以这种方法只能吓唬那些业余的破译者。
不幸的事,目前的 .NET 编译器没有 /owner 选项,所以要想保护你的 C# 或 VB.NET 元件,你需要像下面那样做:

csc helloworld.cs
ildasm /out=temp.il helloworld.exe
ilasm /owner temp.il

 

2:我能直接用 IL 编程吗?

是的。Peter Drayton 在 DOTNET 邮件列表里贴出了这个简单的例子:

.assembly MyAssembly ...{}
  .class MyApp ...{
    .method static void Main() ...{
    .entrypoint
    ldstr "Hello, IL!"
    call void System.Console::WriteLine(class System.Object)
    ret
  }
}

将其放入名为 hello.il 的文件中,然后运行 ilasm hello.il,将产生一个 exe 元件。

 

3:ilasm和csc

前者是对il文件进行汇编,生成可执行的PE文件,而后者是对cs文件进行编译,生成PE文件。具体的区别好象就这,其他区别还不清楚

 

 

六 通用系统类型和语言规范

 

.NET之所以能够实现多种语言集成,是因为CLR。微软顶制定了一个CTS来描述类型的定义和行为,制定了CLS通用语言规范。而所有语言之间想要无缝集成,就必须使用满足CLS。CLR/CTS是一个大的集合,C#,VB.NET这些语言提的功能是CLR/CTS的子集,想要使用CLR所有的功能,只有通过IL了。而CLS则是这些语言的一个交集。要编写一个可以被其他语言使用的程序,都必须满足CLS的规范。、

 

 

七 程序部署

 

写好的程序,要部署到客户机,以往的程序需要安装到文件夹,写注册表,活动目录等等。而.net程序,因为他拥有元数据,可以自我描述,所以不需要进行这些操作。直接把文件复制到硬盘中就可以执行。而卸载时直接删除就摁可以。.net部署主要有私有部署和全局部署。

私有部署的程序集,把程序集和应用程序放在同一个目录下成为私有部署。应用程序是绑定到和他同时生成的类型上的,所以私有部署的程序集只能被一起编译时的应用程序使用,而应用程序也只能使用此程序集。CLR不会调用一个仅仅是同名的程序集。

而全局部署的则解决了程序集共享的问题。,NET FCL就是使用的这种方法。需要共享程序集必须指定一个搜索的目录,这个目力是c:/windows/assemably/GAC。只要把程序集部署到这个地方,就可以被其他程序加载了。而进行全局部署的程序集需要是‘强命名程序集’,这种程序集的特点是,需要一个唯一的标识。他是通过程序集名、版本号、语言文化标志,公有密钥标记来区分的。而这些信息被插入到程序集的清单中。

全局部署方便共享,但却破坏了.NET所说的,简单‘拷贝安装’,‘删除卸载’。而只有很多程序需要使用时,才考虑上面的方式。为了解决部署方便和私有部署(非强命名程序集)的‘DLL HELL’的问题,可以让强命名程序集部署为私有程序集。这样做到了不同版本的共存,也使得部署和删除都很方便。但每次使用这个程序集时都会对他进行安全检查,防止被篡改过。而全局部署,只会在第一次使用时检查。

具体的部署知识大家可以自己查阅,这里只是简单说下。

 

八 性能

 

.NET的编译过程类似于JAVA的虚拟机,其实基本是一样的。2次编译,在运行速度上可能会比普通WIN32要慢一些。因为他需要对执行的环境了解的更多。但JIT编译也有一些好处,他可以针对CPU进行代码优化;可以不对那些总是返回错误的BOOL表达试进行编译,减少代码;在运行时,CLR能评估和分析代码执行情况,重新组织以提高分支预测成功率。具体性能差距如何,我想应该不会很大的。

 

 

总结

 

通过看书,自己对.net平台有了一个大概的了解,以上内容也基本是用自己的话组织起来的。通过学习这个平台结构,让我对平台有了比较深入的认识。.net平台解决了以前COM中存在的一些问题。他使得语言之间可以无缝集成,而且提供了统一的模型,简化了COM,WIN32中复杂的基础构造。提供了广泛的平台支持,方便部署,解决了DLL HELL问题(以前不同厂家的同名DLL无法共存,导致1方程序出错,而通过强命名程序集解决了这个问题),实现了自动垃圾回收等等。然后了解了整个编译以及运行的过程。

 

—————————-

2007-11-29更新:

.net类库和WIN32 API

前段时间在CSDN上问了.net的类库是否是封装的WIN32 API。现在总算有了个结论。之前自己对跨平台认识不是很深。对于WINDOWS来说有一个WIN32的框架,对于Linux有一个Linux的框架。他们下面的实现肯定不一样,都是调用系统功能,但上层提供的接口是一样的。在WINDOWS上的.net,他的类库也是封装了WIN32 API的。需要跨平台的话就要使用跨平台的库。而能在linux上使用的话,linux平台的.net的类库也是封装的linux的API。

 

上面是我自己理解后画的一个图,不知道对不对。

在.net中不是不使用WIN32 API,而是通过自己的类库来调用API。比如程序总调用了某个类的方法A,他内部通过调用WIN32 API实现。但我们把此程序放到其他平台的.net框架上运行时,这个类名和方法A的名字没有变,但不同平台上的.net 框架调用的系统API不同。也就是所有平台上.net的FCL是相同的,但框架调用的系统API不同。

不知道我上面的理解对不对。


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

1 评论

S.E.F进行回复 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注