(翻译)《Expert .NET 2.0 IL Assembler》 第八章 基本类型和签名(一)

返回目录

  

看过类型是如何定义在CLRILAsm中的,让我们进入下一个问题:这些类型以及它们的派生是如何被分配到程序项——字段、变量等等。定义程序项类型的结果的结构被称为这些项的签名。签名创建于编码引用到各种各样的类和值类型;我将在本章详细讨论签名。

CLR中的基本类型

所有的类型必须要被定义于某处。Microsoft .NET Framework类库定义了上百个类型,而且其它程序集基于这个类库中的类型生成了它们自己的类型。一些定义在这个类库中的类型被CLR认为是基本类型并且在签名中给出特殊的编码。这样做只是为了性能——理论上,签名只能创建于类型符号,假设每个类型都定义在某处并因此有一个符号。但是解析所有这些类型就会轻易发现它们引用了很多琐碎的项诸如一个4字节整数或一个布尔值,几乎不能将其认为是一种在运行时的明智的工作方式。

基本数据类型

术语“基本数据类型”涉及到了定义在.NET Framework类库的类型,这个类型给出了特定而且独立使用在签名中的类型编码。因为所有这些类型都定义在Mscorlib程序集中并且都属于System命名空间,在为一个类型提供类库的类型名称的时候,我省略了前缀[mscorlib]System

独立的类型代码定义在头文件CorHdr.hCorElementType枚举中。所有这些代码的名称开始于ELEMENT_TYPE_元素,而我在本章将这个元素省略或者简写为E_T_

       8-1描述了基本数据类型和它们相应的ILAsm符号。

 

8-1定义在CLR中的基本数据类型

编码

常量名称

.NET Framework类型名称

ILAsm符号

注释

0x01

VOID

Void

void

 

0x02

BOOLEAN

Boolean

bool

单字节值,true=1,false=0

0x03

CHAR

Char

char

2字节无符号整数,表示一个Unicode字符

0x04

I1

Sbyte

int8

1字节有符号整数,与C/C++中的char相同

0x05

U1

Byte

unsigned int8

1字节无符号整数

0x06

I2

Int16

int16

2字节有符号整数

0x07

U2

UInt16

unsigned int16

2字节无符号整数

0x08

I4

Int32

int32

4字节有符号整数

0x09

U4

UInt32

unsigned int32

4字节无符号整数

0x0A

I8

Int64

int64

8字节有符号整数

0x0B

U8

UInt64

unsigned int64

8字节无符号整数

0x0C

R4

Single

float32

4字节浮点数

0x0D

R8

Double

float64

8字节浮点数

0x16

TYPRDBYREF

TypeReference

typeref

类型引用,携带了指向一个类型的引用和识别这个被引用类型的信息

0x18

I

IntPtr

native int

指针大小的整数值;大小取决于目标平台,解释了关键字native的用途

0x19

U

UIntPtr

native unsigned int

指针大小的无符号整数值

 

数据指针类型

CLR将指向分配在垃圾收集堆上的对象(称为对象引用;参见“类的表示”)开始位置的指针与其它指针区别开。

CLR中定义了两种数据指针类型:托管指针和非托管指针。它们的区别是托管指针是由CLR的垃圾收集子系统管理的,并且即使在垃圾收集过程期间,被引用的项在内存上移动,托管指针仍然保持有效的;然而,非托管指针只有与“不可移动的”项结合才能被安全地使用。

这两种指针类型必须紧跟在这些指针指向的引用类型之后。正如类型由引用类型所构造,指针没有相应的定义在.NET Framework类库的类型并且不可以被装箱。表8-2描述了两个指针类型以及它们的ILAsm符号。它们都没有一个.NET Framework关联的类型。

 

8-2定义在CLR中的指针类型

编码

常量名称

ILAsm符号

注释

0x0F

PTR

<type>*

指向<type>的非托管指针

0x10

BYREF

<type>&

指向<type>的托管指针

 

注意:虽然ILAsm符号将指针字符放在了被指向的类型之后,在签名E_T_PTRE_T_BYREF中总是位于引用类型引用类型之前。

       这两种类型的指针受标准指针算法的支配:可以从一个指针中加上或减去一个整数值,产生一个新指针;而一个指针可以从另一个指针中减去,产生一个一个整数值。在C/C++IL的指针算法之间的区别是,在IL中——并由此在ILAsm中(参见清单8-2)——指针的增加和减少总是以字节指定,不管该指针所表示的项的大小。

 

清单8-1 C/C++

long L, *pL=&L;

pL += 
4//  pL is incremented by 4*sizeof(long) = 16 bytes

清单8-2 ILAsm

.locals init(int32 L, int32& pL)
ldloca L   // Load pointer to L on stack
stloc pL  // pL = &L

ldloc pL  // Load pL on stack
ldc.i4 4  // Load 4 on stack
add
stloc pL   // pL += 4, pL is incremented by 4 bytes

通过同样的符号......现在,这仅仅是一个普通的表达式。我并不是指一个元数据符号。(我想在本书中,我最好格外小心像“通过同样的符号”和“应用程序的符号”这样的术语。)同样地,IL中这两个指针的delta总是以字节——而不是以这个项所指向的——所表示。

IL中使用非托管的指针并不被认为是安全的(可验证的)。因为没有限制的使用C风格的指针算法允许任何人访问任何东西,IL代码,解除了对非托管的指针,被认为是不可验证的,并且在代码来自一个可信任的源(例如一个本地驱动器)的时候可以运行。

       托管指针是听话的、接受管理的指针,为CLR类型控制和垃圾收集子系统所完全拥有。这些指针只能用在一个很小的范围中,下面介绍了这个范围的边界:

l         托管指针总是指向一个现有的项——一个字段、一个元素数组、一个本地变量,或一个方法参数。

l         元素数组和字段不能带有托管指针类型。

l         托管指针类型只能用于方法特性——本地变量、参数或返回类型,而且所有这些项都与栈相关联并不是一个简单的事情。

l         指向“托管内存”(垃圾收集堆,包括了对象实例盒数组)的托管指针可以被转换为非托管指针。

l         不指向“托管内存”的托管指针可以被转换为非托管指针,但是这样的转换生成了不可信任的IL代码。

l         一个托管指针的基础类型不可以是另一个指针,但是它可以是一个对象引用。

托管指针是不同于对象引用的。在第7章中,描述了值类型的装箱和拆箱,你看到它使用了装箱来为一个值类型创建一个对象引用。使用一个简单的引用——就是说,一个托管的指针——并不是足够的。

       区别在于一个对象引用指向一个对象的开始(方法表),然而一个托管指针指向这个对象的内部——这个项的值(数据)部分。当你得到一个指向一个值类型实例的托管指针时,你会寻址这个数据部分。你可以只得到这些,因为值类型的实例,不是作为对象,并没有方法表。

当你装箱一个值类型的实例时,你创建了一个对象,一个类的实例,包括复制自这个值类型实例的自身方法表和数据部分。这个对象由一个对象引用所表示。

 

函数指针类型

7章简要描述了托管函数指针的使用并将它们与委托类型比较。托管函数指针通过E_T_FNPTR类型所表示,由值0x1B指出并且没有关联到.NET Framework的一个类型。

       就如同一个数据指针类型,函数指针类型是一个并不单独存在的结构化类型,并且必须紧跟在它所指向的方法的完整签名。(托管方法在本章稍后讨论;参加“签名”。)

       对于一个方法函数的ILAsm符号如下所示:

method <call_conv> <return_type> * (<type>[,<type>*])

这里<call_conv>是一个调用约定,<return_type>是一个返回类型,而括号中的<type>序列是一个参数列表。你将在“签名”章节发现更多细节。

 

向量和数组

CLR承认两种类型的数组:向量和多维数组,正如表8-3所描述的。向量是一个一维的下限为0的数组。多维数组,我将其称为arrays,可以有大于1的维数以及非0的下限。这两种类型都是结构化类型,所以它们都没有关联到.NET Framework的一个类型。

 

8-3 CLR支持的数组

编码

常量名称

ILAsm符号

注释

0x1D

SZARRAY

<type>[ ]

<type>向量

0x14

ARRAY

<type>[<bounds>[,<bounds>*]]

<type>数组

 

       所有的向量都是派生于抽象类[mscorlib]System.Array的对象(类的实例)。

       向量的编码是简单的:基础类型的编码紧跟在E_T_SZARRAY之后,它可以是除了void之外的任意值。向量的大小是编码的一部分。由于数组和向量都是对象引用,所以仅仅声明一个数组是不足够的——而必须创建它的一个实例,要为向量使用指令newarr,或者调用一个数组构造函数。在这一点上就是说向量或数组实例的大小要详细指出。因此,一个数组的大小是一个数组实例的特性,而不是这个数组类型的特性。

 

数组编码是更加高级的:

E_T_ARRAY<underlying_type><rank><num_sizes><size1>...<sizeN>

<num_lower_bounds><lower_bound1>...<lower_boundM>

这里下面的描述是正确的:

<underlying_type> 不可以是void

<rank> 是数组维数的数字。(K>0

<num_sizes> 是一个指定了维数大小的数字(N <= K

<sizen>是一个无符号的指定了大小的整数(n = 1,...,N

<num_lower_bounds>是一个指定了下限的数字(M <= K

<lower_boundm> 是一个有符号的指定了下限的整数(m = 1,...,M)。

 

       在前面所有的无符号整数值,都是根据第5章讨论的长度压缩公式进行压缩。为了节省你向后翻三章的时间,我将在表8-4中重复这个公式。

 

8-4 无符号整数的长度压缩公式

值的范围

压缩大小

压缩值(Big Endian)

0-0x7F

1字节

<value>

0x80-0x3FFF

2字节

0x8000|<value>

0x4000-0x1FFFFFFF

4字节

0xC0000000|<value>

 

有符号整数值(下限值)根据一种不同的压缩过程来进行压缩。首先这个有符号的整数被编码为一个无符号的整数:获取原始整数的绝对值,左移1位,并根据原始值的最重要的位来设置最不重要的位。然后压缩根据表8-4显示的工具被应用。

       如果一个维数的大小和/或下限被指定,它们不是被假定为0;而是被标记为没有被指定。大小和下限的规范不可以有“漏洞”——就是说,如果你有一个5维数组并想指定它的第三维的大小(或下限),你必须还要指定第一维和第二维的大小(或下限)

       ILAsm中的一个数组规范就像这样:

<type>  [<bounds>[, <bounds>*] ]
where
<bounds> ::= [<lower_bound>]  [<upper_bound>]

 

下面是一个示例:

int32[..., ...]  // 二维数组,未定义下限和大小

int32[2...5] //一维数组,下限为2并且大小为4

int32[0..., 0...] // 二维数组,下限为0并且未定义大小

 

       如果在多维数组的声明中,对于一个维度,既没有指定它的下限也没有指定它的上限,省略符号就可以被忽略掉。从而,int32[…, …]int32[ , ]表示同样的意思:一个二维的数组,而没有指定下限和大小。

       可是,这种等价在一维数组的情形中并不工作。int32[]符号表示一个向量(<E_T_SZARRAY><E_T_I4>),而int32[…]表示一个维度为1、下限和大小都没有定义的数组(<E_T_ARRAY><E_T_I4><1><0><0>)。

       CLR将多维数组和向量的向量视为完全不同的。int32[ , ]int32[ … ]的规格,导致了不同的编码类型,以不同的方式创建,并且在创建的时候布局也是不一样的:

       int32[ , ]:这个规格的编码为<E_T_ARRAY><E_T_I4><2><0><0>,由一个对数组构造函数的单独调用所创建,并被排列为一个连续的二维int32数组。

       int32[ … , … ]:这个规格的编码为<E_T_SZARRAY><E_T_SZARRAY><E_T_I4>,由一系列newarr指令所创建,并被排列为一个向量的向量的引用,每一个指向了一个连续的int32向量,不能保证关于每个向量的位置。向量的向量经常用于描述交错数组(jagged arrays),在第二个维度的大小改变对第一维索引的依赖时。

 

修饰符

在表8-5描述了4中内嵌的CLR类型编码,并不代表任何特定的数据或指针类型,而是用作数据或指针类型的修饰符。这些类型都没有关联到.NET Microsoft的类型。

 

8-5 定义在CLR中的修饰符

编码

常量名称

ILAsm符号

注释

0x1F

CMOD_REQD

modreq(<class_ref>)

必须的自定义修饰符

0x20

ARRAY

modopt(<class_ref>)

可选的自定义修饰符

0x41

SENTINEL

一个vararg方法的调用中可选参数的开始

0x45

PINNED

pinned

将一个本地变量标记为垃圾收集不可移动的

 

修饰符modreqmodopt指出当前项关联到什么——参数、返回类型或者字段,例如,必须以特殊的方式处理。这些修饰符紧跟在TypeDefTypeRef之后,并且对应到这些符号的类指出了处理当前项的特殊方式。

紧跟在modreqmodopt之后的符号根据下面的算法进行压缩。正如你可能记得的,一个未编码的(外部的)元数据符号是一个4字节无符号整数,在其高位字节上是这个符号的类型而在其3个低位字节上是一个RID。碰巧这些符号出现在签名中并因此需要的压缩仅由3种类型组成:TypeDefTypeRefTypeSpec。(参见本章后面的“签名”获取更多TypeSpec的信息。)因为如此,只有2位,而不是一个完整的字节,是符号类型所需要的:00表示TypeDef01用于TypeRef10指定了TypeSpec。符号压缩的过程类似于用于压缩有符号整数的过程:这个符号的RID部分是左移两位,而2位类型的编码则放置在最不重要的位上。这个压缩的结果仅仅是用于无符号整数,根据表8-4所示的公式。

       修饰符modreqmodopt主要由除了CLR之外的工具使用,例如编译器或程序分析器。modreq修饰符指出这个修饰符必须要被考虑在内,尽管modopt指出这个标志符是可选的并且可以忽略的。ILAsm编译器出于它的内部意图而没有使用这些修饰符。

CLR识别的modreqmodopt修饰符的唯一用途是,这些修饰符被应用于方法的返回类型或参数的时候,这些方法从事于托管或非托管的封送。例如,为了指定一个托管方法必须有cdecl的调用约定,当它被封送为非托管的,你可以使用下面的关联到方法的返回类型的符号:

modopt ([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)

当使用于托管或非托管的封送的上下文中的时候,modreqmodopt修饰符是等效的。

       虽然modreqmodopt修饰符对它们所关联的托管类型的项没有任何影响,但是带有修饰符的签名和不带修饰符的签名被认为是不同的。同样适用于只在由这些修饰符引用的类中不同的签名。这就允许,例如,具有intlong类型参数的方法重载。在C++中,intlong是两种不同的类型,但是对于CLR来说它们是一样的——32位有符号证书(E_T_I4)。因此为了区别这两种类型,C++编译器发布longmodopt([mscorlib]System.Runtime.CompilerServices.IsLong)int32。另一个由C++编译器经常使用的修饰符是modopt([mscorlib]System.Runtime.CompilerServices.IsConst),为了区别,例如,C类型的int*const int*。自定义修饰符被引进到C++类型系统,但是它们不是特定于C++的。其它高级别语言可能也需要区别某些类型,从CLR的观点看是不可分辨的。

Sentinel修饰符在第1章介绍过,在我们分析声明和调用带有一个变量长度的参数列表(vararg方法)的方法时。Sentinel表示了提供给vararg方法调用的可选参数的开始。这个修饰符只可以出现在下面一种上下文中:在调用站点,因为vararg方法的可选参数在这样一个方法被声明时还没有被指定。CLR将出现在其它任意上下文中的sentinel视为一个错误。在调用站点的方法参数只能包括一个sentinel,而且只有提供了可选参数,才可以使用sentinel。

// Declaration of vararg method –  mandatory parameters only:
.method public static vararg int32 Print(string Format)
{

}

//  Calling vararg method with two optional arguments:
call vararg int32 Print(stringint32int32)

// Calling vararg method without optional arguments:
call vararg int32 Print(string)

 

Pinned修饰符只适用于本地变量。它的使用意味着由本地变量引用的对象不可以被垃圾收集重新部署并且必须贯穿方法执行是原位不动的。如果一个代表了对象引用的本地变量或者一个托管指针是“pinned”,那么将其转换为一个非托管的指针并且取消这个非托管指针的引用就是安全的,因为非托管指针在被取消引用得时候,仍然被保证为有效的。(它在取消引用的情况下是安全的,但它仍然是不可信任的,正如一个非托管指针的用法):

.locals init(class FooA, class Foopinned B, int32* pA, int32* pB)
ldloc A
ldflda int32 Foo::x
stloc pA      // pA = &A->x
ldloc B
ldflda int32 Foo::x   
stloc pB      // pB = &B->x

ldloc pA
ldc.i4 123

 

本地类型

当托管代码调用非托管的方法或者将托管字段暴露为非托管代码时,提供关于托管类型应该如何分组为非托管类型并且和如何得到非托管类型的分组,有时是非常必要的。非托管类型由CLR所识别,也被称为“本地的”(native),并列表在CorHdr.h中的枚举CorNativeType里。所有在这个枚举中的常量都具有以_NATIVE_TYPE_*开头的名称,出于这次讨论的意图,我省略了名称的这些部分或者将它们简写为N_T_。同样的常量也列表在.NET Framework类库的枚举System.Runtime.InteropServices.UnmanagedType中。

一些本地类型被废弃了并且被CLR互操作子系统所忽略。但是这些本地类型没有全部过期,ILAsm一定有表示它们的方式——并且由于ILAsm表示这些类型,我不能不把过期的类型和其它的一起列出来,正如你在表8-6中看到的所有类型。

 

8-6 定义在CLR中的本地类型

编码

常量名称

.NET Framework类名

ILAsm符号

注释

0x01

VOID

 

void

过期的并且因此不应该再使用;由IlAsm识别,但是被CLR互操作子系统忽略

0x02

BOOLEAN

Bool

bool

4字节布尔值;true=nonzero,false=0

0x03

I1

I1

int8

有符号1字节整数

0x04

U1

U1

unsigned int8,uint8

无符号1字节整数

0x05

I2

I2

int16

有符号2字节整数

0x06

U2

U2

unsigned int16,uint16

无符号2字节整数

0x07

I4

I4

int32

有符号4字节整数

0x08

U4

U4

unsigned int32,uint32

无符号4字节整数

0x09

I8

I8

int64

有符号8字节整数

0x0A

U8

U8

unsigned int64,uint64

无符号8字节整数

0x0B

R4

R4

float32

4字节浮点型

0x0C

R8

R8

float64

4字节浮点型

0x0D

SYSCHAR

 

syschar

过期的

0x0E

VARIANT

 

VARIANT

过期的

0x0F

CURRENCY

Currency

currency

货币值

0x10

PTR

 

*

过期的;使用native int

0x11

DECIMAL

 

decimal

过期的

0x12

DATE

 

date

过期的

0x13

BSTR

Bstr

bstr

Visual Basic风格的Unicode字符串,在COM互操作中使用

0x14

LPSTR

LPStr

lpstr

指向一个0休止符的ANSI字符串

0x15

LPWSTR

LPSWStr

lpwstr

指向一个0休止符的Unicode字符串

0x16

LPTSTR

LPTStr

lptstr

指向一个0休止符的ANSI或Unicode字符串

0x17

FIXEDSYSSTRING

ByValTStr

fixed sysstring[<size>]

具有<size>字节固定大小的系统字符串

0x18

OBJECTREF

 

objectref

过期的

0x19

IUNKNOWN

IUnknown

iunknown

Iunknown接口指针

0x1A

IDISPATCH

IDispatch

idispatch

IDispatch接口指针

0x1B

STRYCT

Struct

struct

C风格的结构,用于封送格式化的托管类型

0x1C

INTF

Interface

interface

接口指针

0x1D

SAFEARRAY

SafeArray

safearray <variant_type>

<variant_type>类型的安全数组

0x1E

FIXEDARRAY

ByValArray

fixed array[<size>]

具有<size>字节固定大小的数组

0x1F

INT

IntPtr

int

有符号指针大小的整数

0x20

UINT

UIntPtr

unsigned int,uint

无符号指针大小的整数

0x21

NESTEDSTRUCT

nested struct

过期的;使用struct

0x22

BYVALSTR

VBByRefStr

byvalstr

在固定长度缓冲中的Visual Basic风格的字符串

0x23

ANSIBSTR

AnsiBStr

ansi bstr

Visual Basic风格的ANSI字符串

0x24

TBSTR

RBSTr

tbstr

bstr或ansi bstr,依赖于平台

0x25

VARIANTBOOL

VariantBool

variant bool

2字节布尔值;true=-1,false=0

0x26

FUNC

FunctionPtr

method

函数指针

0x28

ASANY

AsAny

as any

对象;在CLR定义的类型

0x2A

ARRAY

LPArray

<n_type>[<sizes>]

有本地类型<n_type>组成的固定大小的数组

0x2B

LPSTRUCT

LPStruct

lpstruct

指向一个C风格的结构

0x2C

CUSTOMMARSHALER

CustomMarshaler

custom(<class_str>,<cookie_str>)

自定义封送

0x2D

ERROR

Error

error

映射int32到VT_HRESULT

      ILAsm符号中的<sizes>参数符号用于N_T_ARRAY,显示在表8-6中,可以是空或者可以被格式化为<size> + <size_param_number>

<sizes> ::= <>
| <size>
| + <size_param_number>
| <size> + <size_param_number>

如果<size>为空,本地数组的大小派生于被封送的托管数组的大小。

       <sizes>参数指定了在数组项中的本地数组大小。基于0的方法参数数量< size_param_number >指定了哪些数组指定了本地数组的大小。本地数组的大小是<size>加上由方法参数<size_param_number>指定的额外大小。

       一个自定义的封送声明(如表8-6所示)有两个参数,它们都是带引号的字符串。<class_str>参数是表示自定义封送的类名称,使用了字符串约定Reflection.Emit<cookie_str>参数是一个在运行时传递到自定义封送的参数字符串(cookie)。这个字符串标志了所需封送的形式,而且它的符号是特点于自定义封送机制的。

变量类型

变量类型(在COM很流行)定义在Wtypes.h文件中的VARENUM枚举里,它是和Microsoft Visual Studio一起分布的。并不是所有的变量类型都适于成为安全数组类型,根据Wtypes.h,但是ILAsm仍然为所有的类型提供了符号,正如表8-7所示。这看起来有点奇怪,考虑到出现在ILAsm中的变量类型只是在安全数组规范的上下文中,但是我们不应该忘记ILAsm主要的应用程序是测试程序的生成,这包括了已知的,程序错误。

8-7 定义在CLR中的变量类型

编码

常量名称

适用于安全数组吗?

ILAsm符号

0x00

VT_EMPTY

<empty>

0x01

VT_NULL

null

0x02

VT_I2

int16

0x03

VT_I4

int32

0x04

VT_R4

float32

0x05

VT_R8

float64

0x06

VT_CY

currency

0x07

VT_DATE

date

0x08

VT_BSTR

bstr

0x09

VT_DISPATCH

idispatch

0x0A

VT_ERROR

error

0x0B

VT_BOOL

bool

0x0C

VT_VARIANT

variant

0x0D

VT_UNKNOWN

iunknown

0x0E

VT_DECIMAL

decimal

0x10

VT_I1

int8

0x11

VT_UI1

unsigned int8, uint8

0x12

VT_UI2

unsigned int16, uint16

0x13

VT_UI4

unsigned int32, uint32

0x14

VT_I8

int64

0x15

VT_UI8

unsigned int64, uint64

0x16

VT_INT

int

0x17

VT_UINT

unsigned int, uint

0x18

VT_VOID

void

0x19

VT_HRESULT

hresult

0x1A

VT_PTR

*

0x1B

VT_SAFEARRAY

safearray

0x1C

VT_CARRAY

carray

0x1D

VT_USERDEFINED

userdefined

0x1E

VT_LPSTR

lpstr

0x1F

VT_LPWSTR

lpwstr

0x24

VT_RECORD

record

0x40

VT_FILETIME

filetime

0x41

VT_BLOB

blob

0x42

VT_STREAM

stream

0x43

VT_STORAGE

storage

0x44

VT_STREAMED_OBJECT

streamed_object

0x45

VT_STORED_OBJECT

stored_object

0x46

VT_BLOB_OBJECT

blob_object

0x47

VT_CF

cf

0x48

VT_CLSID

clsid

0x1000

VT_VECTOR

<v_type>vector

0x2000

VT_ARRAY

<v_type> [ ]

0x4000

VT_BYREF

<v_type> &

 

posted @ 2008-09-13 16:50  包建强  Views(1468)  Comments(1Edit  收藏  举报