Microsoft.NET 框架程序设计 —— 应用程序与类型

前言

当今的应用程序一般都包含着好几个类型。在.NET框架中,类型(type)又被称作组件(component)但本书将使用类型这个术语,而避免使用组件。通常情况下,应用程序既包括我们自己创建的类型,也包括微软和其他一些组织创建的类型。如果这些类型都采用支持CLR的语言开发,那么它们将可以无缝地在一起协作执行。我们甚至可以使用一种语言开发一个基类,然后再使用另一种语言开发它的子类。

2.1.NET框架部署目标

        多年以来,Windows -直背负着一个复杂和不稳定的坏名声。不管这种不良名声是否应得,它们的产生却是来源于许多不同的因素。首先,所有的应用程序都会使用到一些来自微软和其他厂商的动态链接库(DLL)。因为应用程序执行的是来自于不同厂商的代码,而任何一段代码的开发者都不可能确定其他人将怎样使用这些代码,这就会引起各种各样潜在的问题。但从实践来看,这类因交互弓起的问题还不算严重,因为它们在部署之前往往会经过严格的评测与调试。

        然而,当一个公司决定升级它的代码,并把新文件发布给用户时,往往就会出现问题。这些新文件理应和先前的文件保持向后兼容,但是谁又能保证呢?实际上当一个公司升级它的代码时,要重新评测和调试所有已经发布出去的应用程序通常是不可能的。

        当安装新的应用程序时,却发现它损坏了个现有的应用程序。这种情况被称为“DLL hel1”(DLL地狱)。这种困境甚至给一些计算机用户带来了某种恐惧,结果是用户每次在他们的机器上安装新软件时都要做很慎重的考虑。就我个人而言,因为担心一些现存的应用程序受影响,我一般不会去尝试安装某些软件。

        造成 Windows不良名声的第2个原因是其复杂的安装过程。目前大多数应用程序的安装都会景响到系统的各个部分。例如,安装一个应用程序会导致文件被复制到许多目录中,注册表设置要被更新,快捷链接也要被安装到桌面、开始菜单和快速启动工具栏上。这样带来的问题是应用程序不能被隔离为一个单独的实体。备份一个应用程序就显得特别困难,因为必须复制应用程序所有的文件和相关的注册表内容。另外,将应用程序从一个机器移到另一个机器上也很困难。必须重新运行安装程序才能使所有的文件和注册表条目都得到正确的配置。最后,卸载或者删除应用程序也很麻烦,因为总免不了担心应用程序的某些部分仍然遗留在机器中。

        第3个原因与安全有关。当应用程序被安装到用户的机器中时,它们往往会带来各种各样的文件其中许多都是来自不同公司之手。另外,“Web应用程序”通常还会通过网络下载代码,而用户甚至根本意识不到一些代码正在被安装到他们的机器中。这些被安装到用户计算机中的代码可以执行任何操作,包括删除文件、发送电子邮件。由于它们可能引起潜在的危害,用户自然会对安装新的应用程序感到担心。为了使用户彻底放心,系统必须建立一种安全机制,来使得用户显式地允许或者禁止各个公司开发的代码去访问系统资源。

        .NET框架在很大程度上解决了“DLLhel”问题。在解决应用程序状态遍布用户硬盘的问题方面,它也有长足的进步。例如,组件不再需要像COM那样在注册表中进行注册。但还有一点比较遗憾,那就是目前的应用程序仍然需要快捷链接,但以后的Window可能会解决这一问题。至于安全问题,.NET框架包含了一个称作代码访问安全(code access security)的新型安全模型。我们知道,Windows安全基于用户的身份,而代码访问安全则基于程序集的标识。利用代码访问安全模型,我们可以自己决定程序集的安全许可,比如信任微软发布的所有程序集,或者不信任任何从互联网上下载的程序集。相对于Windows操作系统而言,.NET框架使得我们对自己机器中的安装内容和运行内容有了更多的控制权。

2.2 将类型生成为模块

本节向大家展示怎样将包含各种类型的源代码文件变成一个可部署的文件。先来看一个简单的应用程序:

public class App(
    static public void Main(System.Stringl]args){
        System.Console.WriteLine("Hi");
    }
}

        上面的应用程序定义了一个名为 App的类型。该类型有一个静态公有方法 Main。Main 里面又弓用了另外一个类型 System.Console。System.Console是微软实现的一个类型,该类型所有方法的IL代码都位于 MSCorLib.d 文件中。可以看到,这个应用程序既定义了自己的类型,也使用了其他公司的类型。
要生成上面的示例应用程序,首先将前面的代码放在一个源代码文件中,比如App.cs,然后执行下面的命令:

        csc.exe /out:App.exe /t:exe /r:MSCorLib.dll App.cs

        该命令告诉 C#编译器产生一个可执行文件 App.exe(out:App.exe),且产生的文件类型为 Win32 控制台应用程序(/t[arget]:exe)。

        当 C#编译器处理上面的源文件时,它会发现代码中引用了 System.Console类型的 WriteLine方法这时,编译器需要确定 System.Console类型存在,且它有一个 WriteLine 方法,同时还要确定该方法期望参数的类型和代码中提供的相匹配。为了使C#编译器顺利编译这段代码,我们需要给它指定组可以用来辨析外部类型引用的程序集。(后面会对程序集下定义,目前可以暂时把程序集看作是组 DLL 文件。)在上面的命令行中,/[eference]:MSCorLib.dll 命令行开关告诉编译器到 MSCorLib.dl文件标识的程序集中查找外部类型。

        MSCorLib.dl 是一个特殊的文件,它包含了,NET框架中所有的核心类型,例如字节、整数、字符、字符串、等等。由于这些类型的使用非常频繁,C#编译器会自动引用该程序集。换句话说,下面的命令(/r命令行开关已经被去掉)和前面的命令是等效的:

        csc.exe /out:App.exe /t:exe App.cs

        另外,因为/out:App.exe和/t:exe命令行开关也正好是C#编译器的默认设置,所以下面的命令也和前面的命令等效:

        csc.exe App.cs

        如果由于某种原因,我们不想让 C#编译器引用 MSCorLib.dl 程序集,我们可以使用/nostdlib 命令行开关。例如,下面的命令在编译 App.cs 文件时将会出现错误,这是因为 System.Console 类型定义在 MSCorLib.dll 文件中。

        csc.exe /out;App.exe /t:exe /nostdlib App.cs

        现在让我们来更进一步看一看C#编译器输出的文件App.exe。这个文件究竟是什么呢?对于初学者而言,它是一个标准 PE 文件。这意味着一台运行32位或者 64位 Windows的机器应该能够加载并使用该文件。Windows 支持两种类型的应用程序,即控制台用户界面(consoleuser interface,简称 CUI)应用程序和图形用户界面(graphical user interface,简称 GUI)应用程序。因为前面我们指定的是/t:exe命令行开关,所以 C#编译器产生的将是一个CUI应用程序。我们也可以用/t:winexe 命令行开关来使C#编译器产生-个 GUI应用程序。

        现在我们已经知道了我们创建的是哪一种PE文件。但是在App.exe 文件中究竟是什么呢?一个托管 PE 文件包含4部分:PE表头、CLR 表头、元数据和代码。PE表头是 Windows 操作系统要求的标准信息。CLR 表头专门用于那些需要 CLR才能运行的模块(托管模块)。CLR 表头中包含和模块一起创建的元数据的主版本号和次版本号,一些标记,如果模块是CUI或者GUI可执行文件还有个标识入口点方法的 MethodDef 标记(后面会有讲述),以及一个可选的强命名数字签名(将在第3章讨论)。最后该表头中还包括模块内某些元数据表的大小和偏移量。大家可以通过査看CorHdrh 头文件中定义的IMAGECOR20 HEADER来得到CLR 表头的准确格式。

        元数据实际上就是一块二进制数据,其中包含着一些表。我们可以将元数据表划分为3大类:定义表、引用表和清单表。表 2.1描述了一个托管模块的元数据块中包含的一些比较常用的定义表。

表2.1 常用的元数据定义表
元数据定义表名称 描 述
ModuleDef ModuleDef表中总是包含一个标识托管模块的条目。该条目包括模块的文件名和扩展名(不含路径),以及-个模块版本ID(由编译器创建的GUID形式)这使得文件在重命名时,可以保留它原来的名称记录。然而,强烈建议大家不要重命名PE文件,因为这将阻止CLR在运行时定位程序集
TypeDef 托管模块中定义的每一个类型在TypeDef表中都有一个对应的条目。每个条目包括类型的名称及其基类型,一些标记(如public、private等),以及此指针(这些指针指向 MethodDef表中属于该类型的方法、FieldDef表中属」该类型的字段、PropertyDef表中属于该类型的属性以及EventDef表中属该类型的事件)
MethodDef 托管模块中定义的每一个方法在MethodDef表中都有一个对应的条目。每一个条目包括方法名称,一些标记(如 private、public、virtual、abstract、staticfinal等),方法签名,以及该方法的代码在模块中的偏移量。另外,每个条目还包含一个指向ParamDef表中对应条目(其中包含着方法的参数信息的指针
FieldDef 托管模块中定义的每一个字段在FieldDef表中都有一个对应的条目。每一个条目包括一个字段名称,一些标记(如private、public 等),以及字段类型
ParamDef 托管模块中定义的每一个参数在 ParamDef表中都有一个对应的条目。每个条目包括一个参数名称和一些标记(如in、out、retval等)
PropertyDef 托管模块中定义的每一个属性在 PropertyDef表中都有一个对应的条目。每一个条目包括属性名称,一些标记,属性类型,以及属性对应的后端字段(可能为 null)
EventDef 托管模块中定义的每一个事件在EventDef表中都有一个对应的条目。个条日包括一个事件名称和一些标记

        当编译器编译源代码时,代码中定义的任何内容都会导致一个条目在表2.1所描述的相应表中被创建。另外,编译器还会检测源代码中引用到的类型、字段、方法、属性和事件。元数据用一组引用表来记录这些内容。表2.2展示了一些比较常用的元数据引用表。

表2.2常用的元数据引用表
元数据引用表名称 描 述
AssemblyRef 托管模块中引用的每一个程序集在AssemblyRef表中都有个对应的条目。每一个条目包括绑定程序集所必需的信息:程序集名称(不含路径和扩展名)、版本号、语言文化(culture)以及一个公有密钥标记(通常为一个小的散列值,由发布者给出的公有密钥产生,标识被引用程序集的发布者)。另外,每一个条目还包括一些标记和一个散列值。该散列值是被引用程序集中的位的一个校验和。CLR完全忽略该散列值,并且在将来也可能继续如此
ModuleRef 托管模块中有时会引用到实现在同一程序集中其他不同模块内的些类型,这些模块每一个都在ModuleRef表中有一个对应的条目。每一个条目包括模块的文件名和扩展名(不含路径)。该表用来绑定那些实现在相同程序集中不同模块内的类型
TypeRef 托管模块中引用的每-个类型在TypeRef表中都有一个对应的条目。每个条目包括类型的名称和一个指向类型所在位置的指针。如果该类型在另一个类型内实现,那么这个指针指向一个TypeRef条目。如果该类型在同一个模块中实现,那么这个指针指向一个ModuleDef条目。如果该类型在同一程序集的其他模块中实现,那么这个指针指向一个ModuleRef条目。如果该类型在其他不同的程序集中实现,那么这个指针指向个AssemblyRef条目
MemberRef 托管模块中引用的每一个成员(字段、方法以及属性方法和事件方法)(译注:这里的属性方法指的是属性编译为L代码后产生的get访问器方法和/或set访问器方法,事件方法指的是事件编译为代码后产生的add访问器方法和/或remove访问器方法。当托管模块引用一个属性或者事件时,它并不会引用属性或者事件本身的元数据,而是直接引用相应的访问器方法元数据)在MemberRef表中都有一个对应的条目。每个条目包括成员的名称,成员的签名,以及一个指向TypeRef表中对应条目(其所标识的类型中定义了该成员)的指针

        还有许多表未列入表 2.1和表2.2,这里仅仅是希望能让大家了解一下编译器产生的各种元数据信息。前面还提到过清单元数据表,它将放在本章稍后讨论。
        有许多工具可以用来査看托管PE文件中的元数据。我个人喜欢用LDasm.exe,即IL 反汇编器可以执行下面的命令行米查看元数据表:
                                ILDasm /Adv App.exe
        上面的命令将导致ILDasm.exe 运行,并加载 App.exe程序集。其中/Adv命令行开关告诉 ILDasm启用:些“高级”的菜单项。这些高级菜单项可以在【视图】菜单中找到。如果想以一种漂亮、可读的形式来查看元数据,可以选

  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜飞鼠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值