(翻译)《Expert .NET 2.0 IL Assembler》 第一章 简单示例 1.2 简单示例(三)

返回目录

全局项

     以下是OddOrEven应用程序的全局项:

These are the global items of the OddOrEven application:
{


// End of namespace
.field public static valuetype CharArray8 Format at FormatData

     .field public static valuetype CharArray8 Format at FormatData 声明了一个类型为valuetype CharArray8、名为Format的静态字段。正如你可能记得的,你在Odd.or.Even::Check方法中使用了指向这个字段的引用。

     举例来说,这个字段不同于Odd.or.Even::val,因为它是在类的范围外声明的,因此不属于任何类。它被称为全局项(global item)。属于这个模块的全局项,包括了它们的声明。正如你所了解的,模块是一个托管的可执行文件(EXEDLL);一个或更多的模块组成了一个程序集,程序集是托管应用程序的主要构造块;每个程序集都有一个主模块,它在元数据中携带了程序集的识别信息。

     实际上,一个小诡计与不属于任何类的全局项概念相关联。每个模块的元数据包括了一个特殊的名为<Module>TpyeDef,代表了——能猜到什么?对,你绝对能猜得到。

     TypeDef总是存在于元数据中,而且它总是保持着在TypeDef表中的头等位置。然而,<Module>并不是一个合适的TypeDef,因为对比于“正常的”TypeDef(类、值类型等等),它的特性受到了限制。这听起来差不多像真实的生活——你越是处在头等的位置,你的选择就受到更多的限制。

     <Module>不能是public的,也就是说在它的程序集外部是不可见的。<Module>只能有静态成员,这意味着所有的全局字段和方法必须是静态的。此外,<Module>不能有字段或属性,因为字段和属性不可能是静态的。(查阅第15章获取细节。)限制的原因是显然的:给定的程序集中的每个模块总是正好包括一个实例,实例化的概念就变得没有意义了。

     全局字段和方法的可访问性,不同于一个“正常的”类的成员字段和方法的可访问性。甚至于公有的全局项在程序集外部也不能被访问。<Module>并没有任何扩展——它没有基类——同时也没有类可以从<Module>派生。然而,所有声明在模块中的类对所在模块中的全局项具有完全的访问性,包括私有的全局项。

     最后一个特性类似于类的内嵌性,是不同于类的继承性。(派生类并不具有对基类私有项的访问性。)内嵌类是一个声明在另一个类的范围中的类。那些其他的类通常被称为外包类(enclosing class)或外壳(encloser)。内嵌类不是一个成员类或一个内部类,从这种角度讲,它不能隐式地访问这个外包类的实例引用(this)。一个内嵌类只能通过三种情形连接到它的外围类:将内嵌类声明在外包类的词法范围中;内嵌类的可见性通过外包类的可见性的过滤(也就是说,如果外包类是私有的,内嵌类对外部程序集将是不可见的,而无视于其自身的可见性);内嵌类可以访问外包类的所有成员。

     因为所有声明在模块中的类在定义上是声明在模块的词法范围内的,所以只有把模块和声明在其中类的关系理解为外包类及其嵌套类的关系才符合逻辑。

     结果,全局项的可访问性修饰符publicassemblyfamorassem都等同于assemblyprivatefamilyfamandassem等同于private;而privatescope还是privatescope。元数据的验证规则明确规定,对于全局字段和全局方法,只允许三种可访问性修饰符:public(准确地说是assembly)、privateprivatescope。然而,加载器,对于全局项的可访问性修饰符的要求更加宽松:它允许设置任何可访问性修饰符,并像刚刚描述的(如assemblyprivateprivatescope)那样来解释它们。

  

映射字段

     以下是OddOrEven应用程序的映射字段:

.field public static valuetype CharArray8 Format at FormatData

     Format字段的声明包括了一个新:at FormatData字句。这个子句表明Format字段位于模块的数据段,而它的位置是由FormatData这个数据标签识别的。(下面的部分将讨论数据声明和打标签)

     编译器为字段初始化广泛的使用了映射字段到数据的技术。而这种技术并没有什么限制。首先,被映射的字段必须是静态的。这是符合逻辑的。毕竟,映射本身就是静态的,因为它是在编译期发生的。即使你决定映射一个实例字段,那么这个字段的所有不同实例,将会被自然地映射到同一块内存上,这就意味着你最终还是要使用静态字段。因为加载器,遭遇到一个被映射到实例的字段时,会决定支持“实例化”并完全忽略字段映射,这个被映射的实例字段就会如同其他所有实例字段一样被呈现。

     其次,映射字段位于数据段中,因此在CLR垃圾收集子体系中是不可到达的,它会自动销毁没用的对象。出于这个原因,映射字段并不是主动进行垃圾收集的类型(如同类或数组)。值类型也被允许作为映射字段的类型,只要这些值类型中,没有成员的类型是主动进行垃圾收集的。如果这条规则被违反了,加载器就会抛出一个类型加载异常并终止加载这个模块。

     再其次,将一个字段映射到一块预分配的内存位置,使得这个字段广泛地支持读写。这从安全角度看是非常好的,只要这个字段没有内部的结构——该结构的一部分并不打算用于公共访问。这是为什么映射字段的类型不能是任何带有非公有字段成员的值类型。加载器严格执行这个规则,并一直向下检查非公有字段。例如,如果被映射字段的类型是值类型A,加载器将检查它的字段是否全都为公有的。如果在这些字段中有一个值类型B,加载器就会检查B的字段是否全都是公有的。如果在这些字段中,有两个值类型字段CD——好吧,你应该能明白我的意思。如果加载器在任何级别的映射字段类型上发现了非公有字段,就会抛出一个类型加载异常并终止加载。

  

数据声明

以下是OddOrEven应用程序的数据声明:

.field public static valuetype CharArray8 Format at FormatData
.data FormatData = bytearray(25 64 00 00 00 00 00 00)

     .field public static valuetype CharArray8 Format at FormatData定义了标记为FormatData的数据段。这个段有8位的长度,其中前两位,以ASCII码的形式表示字符%0x25)和d0x64),剩下的6位都是0

     这个段是用bytearray来描述的,这是ILAsm中描述数据的普遍方式。圆括号中的数字表示字节的16进制值而没有0x前缀。字节值应该用空格分隔,我建议你总是使用两位的形式,即使一位也足够的时侯(正如示例中的0这种情形)。

     显然,你可以在文字上将任何数据表示为一个bytearray。例如,不在ldstr "Enter a number"中使用引用型字符串,而是使用字符串的bytearray表示:ldstrbytearray (6F 00 64 00 64 00 21 00 00 00)

     圆括号中的数字表示了Unicode字符:odd!,并以0作为休止符。当你使用ILDASM的时候,你会发现bytearray是随处可见的。bytearray是一种通用的、类型无关(type-neutral)的数据表示形式,当ILDASM不能将数据所属的类型识别为某个基元类型(如int32)时,它就会用字节数组来表示。

     另一方面,你可以定义FormatData数据如下:

.data FormatData = int64 (0x0000000000006425)

     这会导致同样的数据段大小和内容。当你指定一个声明了数据段(例如int64)的类型时,没有有关的记录这个类型被放入元数据还是别的什么地方。ILAsm编译器使用特定的类型,只是为了两个意图:识别数据段分配到的大小,识别段中字节的外观。

作为占位符的值类型

以下是用作占位符的值类型:

.field public static valuetype CharArray8 Format at FormatData
.data FormatData = bytearray(25 64 00 00 00 00 00 00)
.class public explicit CharArray8
     
extends [mscorlib]System.ValueType { .size 8 }

     .class public explicit CharArray8 extends [mscorlib]System.ValueType { .size 8 } 声明了一个没有成员的值类型,但是有一个显示指定的大小:8个字节。声明这样一个值类型是声明“仅仅一块内存”的一种普遍方式。在此情形中,你不需要声明值类型的任何成员,因为你对这块内存的内部结构不感兴趣;你只是想简单将它作为指明了字段大小的全局字段Format来使用。在某种意义上,值类型只是一个占位符。

     你能替代地使用一个8字节的数组而不用声明另一个值类型么?如果你不打算将字段映射到数据,那么就是可以的。因为数组主动的进行垃圾收集,所以不允许它们作为映射字段的类型。

     使用值类型作为占位符,在C/C++编译器中是很流行的,因为需要对大量的ANSI字符串常量进行存储和记录它们的地址。Visual C#Visual Basic.NET编译器,主要处理Unicode字符串,对于这项技术不是很关心,因为它们直接使用CLR字符串常量——这将以Unicode格式存储在元数据中。

 

调用非托管代码

以下是OddOrEven应用程序如何声明非托管代码,这将会被托管方法check调用:

.method public static pinvokeimpl("msvcrt.dll"  cdecl)
     vararg 
int32 sscanf(stringint8*) cil managed{ }

     以上声明了一个非托管方法,它是由托管代码调用的。pinvokeimpl("msvcrt.dll"  cdecl)特性指出这是一个非托管方法,使用了称为P/InkokePlatform Invocation)的机制进行调用。这个特性还指出了这个方法驻留在非托管的Msbcrt.dll中,并有cdecl的调用约定。这个调用约定意味着非托管方法处理参数的方式与ANSI C函数相同。

     这个方法有两个为string类型和int8*类型(等价于C/C++char*)的强制性参数,并返回int32。作为一个vararg方法,sscanf可以有任意数量任意类型的可选性参数,但是正如你已经知道的,在一个vararg方法被声明的时候,可选性参数或sentinel都不会被指定。

         P/Inkoke是这样一种机制:CLR为从托管代码调用非托管代码提供了方便。在这个场景背后,运行时构造了所谓的stubthunk,这将允许记录非托管函数的地址,以及在托管参数类型和适当的非托管类型之间转换的约定。这种约定被称为参数(param封送eter marshaling)。

     这里所声明的不光是一个确切的被调用的非托管方法,而是一个由运行时生成的占位程序(stub),正如在托管代码中所看到的——解释了cil managed标记的实现。将这个方法签名指定为int32(string, int8*),你指定了封送参数的“托管部分”(managed side)。封送参数的未托管部分则由确切的被调用的非托管方法的签名定义。

     C语言中,sscanf这个非托管函数的确切的签名是int sscanf(const char*, const char*, …)。因此,第一个参数被从托管的string类型封送到非托管的char*类型。回忆一下当我们声明Odd.or.Even这个类时,我指明了ansi标记,这意味着托管字符串默认封送ANSI C字符串:char*。而且因为对sscanf的调用,是由Odd.Or.Even的成员方法生成的,你不需要提供关于封送托管字符串的特殊信息。

     sscanf第二个参数的声明是int8*,这直接等价于char*;结果,很少需要封送。(ILAsm也有char类型,但是它指示了一个Unicode字符而不是ANSI,等同于C语言的“unsigned short”,因此你不能在这里使用此类型。)

     原始的(非托管)sscanf的可选性的参数被认为是一种指针,在语法分析缓冲字符串的时候,指向你想填充的项(变量)。这些指针的数量和基本类型是根据格式化标准字符串(sscanf的第二个参数)定义的。在此情形下,给定一个格式化标准字符串“%d”,sscanf将希望一个单独的可选性int*类型的参数。当我调用sscanf——这个托管的Thunk时,我提供了int32*类型的可选性参数,这可能需要编组到一个本地的整型指针,只有当你正在处理的平台不是一个32Intel平台时(例如,AMD或一个32Intel平台)。

     包包译注:Thunk技术,一般认为是在程序中直接构造出可执行代码的技术(在正常情况下,这是编译器的任务)。

     P/Inkoke机制是非常有用的,因为它可以使你完整的访问大量丰富的本地的库和平台API。但是不要过高估计这个到处存在的P/Inkoke。平台的不同导致了它们的API不同,因此过度使用API会限制应用程序的可移植性。最好坚持使用.NET Framework类库并留有些许安慰——想一想到目前为止,你可以对这个类库的底层是什么做出相当美好的遐想。

     既然我已经完成了对源代码的展示,请访问Apress网站:http://www.apress.com/.,获取源文件Simple.il,将其复制到你的工作目录,使用控制台命令ilasm simple编译(假设你已经安装了.NET FrameworkSDK),试着运行Simple.exe这个编译结果。

原文地址:https://www.cnblogs.com/Jax/p/1250720.html