《C#不安全代码18.doc》由会员分享,可在线阅读,更多相关《C#不安全代码18.doc(24页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、【精品文档】如有侵权,请联系网站删除,仅供学习与交流C#不安全代码18.精品文档.18. 不安全代码31318.1 不安全上下文31318.2 指针类型31518.3 固定和可移动变量31818.4 指针转换31818.5 表达式中的指针31918.5.1 指针间接寻址32018.5.2 指针成员访问32018.5.3 指针元素访问32118.5.4 address-of 运算符32218.5.5 指针递增和递减32318.5.6 指针算术运算32318.5.7 指针比较32418.5.8 sizeof 运算符32418.6 fixed 语句32518.7 堆栈分配32818.8 动态内存分配
2、3291. 不安全代码如前面几章所定义,核心 C# 语言没有将指针列入它所支持的数据类型,从而与 C 和 C+ 有着显著的区别。作为替代,C# 提供了各种引用类型,并能够创建可由垃圾回收器管理的对象。这种设计结合其他功能,使 C# 成为比 C 或 C+ 安全得多的语言。在核心 C# 语言中,不可能有未初始化的变量、“虚”指针或者超过数组的边界对其进行索引的表达式。这样,以往总是不断地烦扰 C 和 C+ 程序的一系列错误就不会再出现了。尽管实际上对 C 或 C+ 中的每种指针类型构造,C# 都设置了与之对应的引用类型,但仍然会有一些场合需要访问指针类型。例如,当需要与基础操作系统进行交互、访问内
3、存映射设备,或实现一些以时间为关键的算法时,若没有访问指针的手段,就不可能或者至少很难完成。为了满足这样的需求,C# 提供了编写不安全代码 (unsafe code) 的能力。在不安全代码中,可以声明和操作指针,可以在指针和整型之间执行转换,还可以获取变量的地址,等等。在某种意义上,编写不安全代码很像在 C# 程序中编写 C 代码。无论从开发人员还是从用户角度来看,不安全代码事实上都是一种“安全”功能。不安全代码必须用修饰符 unsafe 明确地标记,这样开发人员就不会误用不安全功能,而执行引擎将确保不会在不受信任的环境中执行不安全代码。1.1 不安全上下文C# 的不安全功能仅用于不安全上下文
4、中。不安全上下文是通过在类型或成员的声明中包含一个 unsafe 修饰符或者通过使用 unsafe-statement 引入的: 类、结构、接口或委托的声明可以包含一个 unsafe 修饰符,在这种情况下,该类型声明的整个文本范围(包括类、结构或接口的体)被认为是不安全上下文。 在字段、方法、属性、事件、索引器、运算符、实例构造函数、析构函数或静态构造函数的声明中,也可以包含一个 unsafe 修饰符,在这种情况下,该成员声明的整个文本范围被认为是不安全上下文。 unsafe-statement 使得可以在 block 内使用不安全上下文。该语句关联的 block 的整个文本范围被认为是不安全
5、上下文。下面显示了关联的语法扩展。为简洁起见,用省略号 (.) 表示前几章中出现过的产生式。class-modifier:.unsafestruct-modifier:.unsafeinterface-modifier:.unsafedelegate-modifier:.unsafefield-modifier:.unsafemethod-modifier:.unsafeproperty-modifier:.unsafeevent-modifier:.unsafeindexer-modifier:.unsafeoperator-modifier:.unsafeconstructor-modif
6、ier:.unsafedestructor-declaration:attributesopt externopt unsafeopt identifier ( ) destructor-bodyattributesopt unsafeopt externopt identifier ( ) destructor-bodystatic-constructor-modifiers:externopt unsafeopt staticunsafeopt externopt staticexternopt static unsafeopt externopt static unsafeopt sta
7、tic externopt unsafeoptstatic unsafeopt externoptembedded-statement:.unsafe-statementunsafe-statement:unsafe block在下面的示例中public unsafe struct Nodepublic int Value;public Node* Left;public Node* Right;在结构声明中指定的 unsafe 修饰符导致该结构声明的整个文本范围成为不安全上下文。因此,可以将 Left 和 Right 字段声明为指针类型。上面的示例还可以编写为public struct No
8、depublic int Value;public unsafe Node* Left;public unsafe Node* Right;此处,字段声明中的 unsafe 修饰符导致这些声明被认为是不安全上下文。除了建立不安全上下文从而允许使用指针类型外,unsafe 修饰符对类型或成员没有影响。在下面的示例中public class Apublic unsafe virtual void F() char* p;.public class B: Apublic override void F() base.F();.A 中 F 方法上的 unsafe 修饰符直接导致 F 的文本范围成为不安
9、全上下文并可以在其中使用语言的不安全功能。在 B 中对 F 的重写中,不需要重新指定 unsafe 修饰符,除非 B 中的 F 方法本身需要访问不安全功能。当指针类型是方法签名的一部分时,情况略有不同public unsafe class Apublic virtual void F(char* p) .public class B: Apublic unsafe override void F(char* p) .此处,由于 F 的签名包括指针类型,因此它只能出现在不安全上下文中。然而,为设置此不安全上下文,既可以将整个类设置为不安全的(如 A 中的情况),也可以仅在方法声明中包含一个 un
10、safe 修饰符(如 B 中的情况)。1.2 指针类型在不安全上下文中,type(第 Error! Reference source not found. 章)可以是 pointer-type,也可以是 value-type 或 reference-type。但是,pointer-type 也能在不安全上下文以外的 typeof 表达式(第 Error! Reference source not found.中使用,因为此类使用不是不安全的。type:value-typereference-typepointer-typepointer-type 可表示为 unmanaged-type 后跟一
11、个 * 标记,或者关键字 void 后跟一个 * 标记:pointer-type:unmanaged-type *void *unmanaged-type:type指针类型中,在 * 前面指定的类型称为该指针类型的目标类型 (referent type)。它表示该指针类型的值所指向的变量的类型。与引用(引用类型的值)不同,指针不受垃圾回收器跟踪(垃圾回收器并不知晓指针和它们指向的数据)。出于此原因,不允许指针指向引用或者包含引用的结构,并且指针的目标类型必须是 unmanaged-type。unmanaged-type 是任何不是 reference-type 并且在任何嵌套级别都不包含 re
12、ference-type 字段的类型。换句话说,unmanaged-type 是下列类型之一: sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal 或 bool。 任何 enum-type。 任何 pointer-type。 任何由用户定义的只包含 unmanaged-type 字段的 struct-type。将指针和引用进行混合使用时的基本规则是;引用(对象)的目标可以包含指针,但指针的目标不能包含引用。下表给出了一些指针类型的示例:示例说明byte*指向 byte 的指针char*指向 char 的指针
13、int*指向 int 的指针的指针int*一维数组,它的元素是指向 int 的指针void*指向未知类型的指针对于某个给定实现,所有的指针类型都必须具有相同的大小和表示形式。与 C 和 C+ 不同,在 C# 中,当在同一声明中声明多个指针时,* 只与基础类型写在一起,而不充当每个指针名称的前缀标点符号。例如int* pi, pj;/ NOT as int *pi, *pj;类型为 T* 的一个指针的值表示类型为 T 的一个变量的地址。指针间接寻址运算符 * (第 1.5.1 节) 可用于访问此变量。例如,给定int* 类型的变量 P,则表达式 *P 表示 int 变量,该变量的地址就是 P 的
14、值。 与对象引用类似,指针可以是 null。如果将间接寻址运算符应用于 null 指针,则其行为将由实现自己定义。值为 null 的指针表示为将该指针的所有位都置零。void* 类型表示指向未知类型的指针。因为目标类型是未知的,所以间接寻址运算符不能应用于 void* 类型的指针,也不能对这样的指针执行任何算术运算。但是,void* 类型的指针可以强制转换为任何其他指针类型(反之亦然)。指针类型是一个单独类别的类型。与引用类型和值类型不同,指针类型不从 object 继承,而且不存在指针类型和 object 之间的转换。具体而言,指针不支持装箱和拆箱(第 Error! Reference so
15、urce not found. 节)操作。但是,允许在不同指针类型之间以及指针类型与整型之间进行转换。在第 1.4 节中对此进行了描述。pointer-type 可用作易失字段的类型(第 Error! Reference source not found. 节)。虽然指针可以作为 ref 或 out 参数传递,但这样做可能会导致未定义的行为,例如,指针可能被设置为指向一个局部变量,而当调用方法返回时,该局部变量可能已不存在了;或者指针曾指向一个固定对象,但当调用方法返回时,该对象不再是固定的了。例如:using System;class Teststatic int value = 20;un
16、safe static void F(out int* pi1, ref int* pi2) int i = 10;pi1 = &i;fixed (int* pj = &value) / .pi2 = pj;static void Main() int i = 10;unsafe int* px1;int* px2 = &i;F(out px1, ref px2);Console.WriteLine(*px1 = 0, *px2 = 1,*px1, *px2);/ undefined behavior方法可以返回某一类型的值,而该类型可以是指针。例如,给定一个指向连续的 int 值序列的指针、
17、该序列的元素个数,和另外一个 int 值 (value),下面的方法将在该整数序列中查找与该 value 匹配的值,若找到匹配项,则返回该匹配项的地址;否则,它将返回 null:unsafe static int* Find(int* pi, int size, int value) for (int i = 0; i 运算符可用于通过指针访问结构的成员(第 1.5.2 节)。 运算符可用于索引指针(第 1.5.3 节)。 & 运算符可用于获取变量的地址(第 1.5.4 节)。 + 和 - 运算符可以用于递增和递减指针(第 1.5.5 节)。 + 和 - 运算符可用于执行指针算术运算(第 1.
18、5.6 节)。 =、!=、 运算符可以用于比较指针(第 1.5.7 节)。 stackalloc 运算符可用于从调用堆栈(第 1.7 节)中分配内存。 fixed 语句可用于临时固定一个变量,以便可以获取它的地址(第 1.6 节)。1.3 固定和可移动变量address-of 运算符(第 1.5.4 节)和 fixed 语句(第 18.6 节)将变量划分为两个类别:固定变量 (fixed variable) 和可移动变量 (moveable variable)。固定变量驻留在不受垃圾回收器的操作影响的存储位置中。(固定变量的示例包括局部变量、值参数和由取消指针引用而创建的变量。)另一方面,可移
19、动变量则驻留在会被垃圾回收器重定位或处置的存储位置中。(可移动变量的示例包括对象中的字段和数组的元素。)& 运算符(第 1.5.4 节)允许不受限制地获取固定变量的地址。但是,由于可移动变量会受到垃圾回收器的重定位或处置,因此可移动变量的地址只能使用 fixed 语句(第 1.6 节)获取,而且该地址只在此 fixed 语句的生存期内有效。准确地说,固定变量是下列之一: 用引用局部变量或值参数的 simple-name(第 Error! Reference source not found. 节)表示的变量。 用 V.I 形式的 member-access(第 Error! Reference
20、 source not found. 节)表示的变量,其中 V 是 struct-type 的固定变量。 用 *P 形式的 pointer-indirection-expression(第 1.5.1 节)、P-I 形式的 pointer-member-access(第 18.5.2 节)或 PE 形式的 pointer-element-access(第 18.5.3 节)表示的变量。所有其他变量都属于可移动变量。请注意静态字段属于可移动变量。还请注意即使赋予 ref 或 out 形参的实参是固定变量,它们仍属于可移动变量。最后请注意,由取消指针引用而产生的变量总是属于固定变量。1.4 指针转
21、换在不安全上下文中,可供使用的隐式转换的集合(第 Error! Reference source not found. 节)也扩展为包括以下隐式指针转换: 从任何 pointer-type 到 void* 类型。 从 null 类型到任何 pointer-type。另外,在不安全上下文中,可供使用的显式转换的集合(第 Error! Reference source not found. 节)也扩展为包括以下显式指针转换: 从任何 pointer-type 到任何其他 pointer-type。 从 sbyte、byte、short、ushort、int、uint、long 或 ulong 到任
22、何 pointer-type。 从任何 pointer-type 到 sbyte、byte、short、ushort、int、uint、long 或 ulong。最后,在不安全上下文中,标准隐式转换的集合(第 Error! Reference source not found. 节)包括以下指针转换: 从任何 pointer-type 到 void* 类型。两个指针类型之间的转换永远不会更改实际的指针值。换句话说,从一个指针类型到另一个指针类型的转换不会影响由指针给出的基础地址。当一个指针类型被转换为另一个指针类型时,如果没有将得到的指针正确地对指向的类型对齐,则当结果被取消引用时,该行为将是
23、未定义的。一般情况下,“正确对齐”的概念是可传递的:如果指向类型 A 的指针正确地与指向类型 B 的指针对齐,而此指向类型 B 的指针又正确地与指向类型 C 的指针对齐,则指向类型 A 的指针将正确地与指向类型 C 的指针对齐。请考虑下列情况,其中具有一个类型的变量被通过指向一个不同类型的指针访问:char c = A;char* pc = &c;void* pv = pc;int* pi = (int*)pv;int i = *pi;/ undefined*pi = 123456;/ undefined当一个指针类型被转换为指向字节的指针时,转换后的指针将指向原来所指变量的地址中的最低寻址字
24、节。连续增加该变换后的指针(最大可达到该变量所占内存空间的大小),将产生指向该变量的其他字节的指针。例如,下列方法将 double 型变量中的八个字节的每一个显示为一个十六进制值:using System;class Testunsafe static void Main() double d = 123.456e23;unsafe byte* pb = (byte*)&d;for (int i = 0; i ”标记,最后是一个 identifier。pointer-member-access:primary-expression - identifier在 P-I 形式的指针成员访问中,P
25、必须是除 void* 以外的某个指针类型的表达式,而 I 必须表示 P 所指向的类型的可访问成员。P-I 形式的指针成员访问的计算方式与 (*P).I 完全相同。有关指针间接寻址运算符 (*) 的说明,请参见第 1.5.1 节。有关成员访问运算符 (.) 的说明,请参见第 Error! Reference source not found. 节。在下面的示例中using System;struct Pointpublic int x;public int y;public override string ToString() return ( + x + , + y + );class Tes
26、tstatic void Main() Point point;unsafe Point* p = &point;p-x = 10;p-y = 20;Console.WriteLine(p-ToString();- 运算符用于通过指针访问结构中的字段和调用结构中的方法。由于 P-I 操作完全等效于 (*P).I,因此 Main 方法可以等效地编写为:class Teststatic void Main() Point point;unsafe Point* p = &point;(*p).x = 10;(*p).y = 20;Console.WriteLine(*p).ToString();1
27、.5.3 指针元素访问pointer-element-access 包括一个 primary-no-array-creation-expression,后跟一个用“”和“”括起来的表达式。pointer-element-access:primary-no-array-creation-expression expression 在 PE 形式的指针元素访问中,P 必须是 void* 以外的指针类型的表达式,而 E 则必须是可以隐式转换为 int、uint、long 或 ulong 的类型的表达式。PE 形式的指针元素访问的计算方式与 *(P + E) 完全相同。有关指针间接寻址运算符 (*)
28、的说明,请参见第 1.5.1 节。有关指针加法运算符 (+) 的说明,请参见第 1.5.6 节。在下面的示例中class Teststatic void Main() unsafe char* p = stackalloc char256;for (int i = 0; i 256; i+) pi = (char)i;指针元素访问用于在 for 循环中初始化字符缓冲区。由于 PE 操作完全等效于 *(P + E),因此示例可以等效地编写为:class Teststatic void Main() unsafe char* p = stackalloc char256;for (int i =
29、0; i 256; i+) *(p + i) = (char)i;指针元素访问运算符不能检验是否发生访问越界错误,而且当访问超出边界的元素时行为是未定义的。这与 C 和 C+ 相同。1.5.4 address-of 运算符addressof-expression 包含一个“and”符 (&),后跟一个 unary-expression。addressof-expression:& unary-expression如果给定类型为 T 且属于固定变量(第 1.3 节)的表达式 E,构造 &E 将计算由 E 给出的变量的地址。计算的结果是一个类型为 T* 的值。如果 E 不属于变量,如果 E 属于只
30、读局部变量,或如果 E 表示可移的变量,则将发生编译时错误。在最后一种情况中,可以先利用固定语句(第 1.6 节)临时“固定”该变量,再获取它的地址。如第 Error! Reference source not found. 节中所述,如果在实例构造函数或静态构造函数之外,在结构或类中定义了 readonly 字段,则该字段被认为是一个值,而不是变量。因此,无法获取它的地址。与此类似,无法获取常量的地址。& 运算符不要求它的参数先被明确赋值,但是在执行了 & 操作后,该运算符所应用于的那个变量在此操作发生的执行路径中被“认为是”已经明确赋值的。这意味着,由程序员负责确保在相关的上下文中对该变量
31、实际进行合适的初始化。在下面的示例中using System;class Teststatic void Main() int i;unsafe int* p = &i;*p = 123;Console.WriteLine(i);初始化 p 的代码执行了 &i 操作,此后 i 被认为是明确赋值的。对 *p 的赋值实际上是初始化了 i,但设置此初始化是程序员的责任,而且如果移除此赋值语句,也不会发生编译时错误。上述 & 运算符的明确赋值规则可以避免局部变量的冗余初始化。例如,许多外部 API 要求获取指向结构的指针,而由此 API 来填充该结构。对此类 API 进行的调用通常会传递局部结构变量的
32、地址,而如果没有上述规则,则将需要对此结构变量进行冗余初始化。1.5.5 指针递增和递减在不安全上下文中,+ 和 - 运算符(第 Error! Reference source not found. 节和第 7.6.5 节)可以应用于除 void* 以外的所有类型的指针变量。因此,为每个指针类型 T* 都隐式定义了下列运算符:T* operator +(T* x);T* operator -(T* x);这些运算符分别产生与 x + 1 和 x - 1(第 1.5.6 节)相同的结果。换句话说,对于 T* 类型的指针变量,+ 运算符将该变量的地址加上 sizeof(T),而 - 运算符则将该变
33、量的地址减去 sizeof(T)。如果指针递增或递减运算的结果超过指针类型的域,则结果是由实现定义的,但不会产生异常。1.5.6 指针算术运算在不安全上下文中,+ 和 - 运算符(第 Error! Reference source not found. 节和第 7.7.5 节)可以应用于除 void* 以外的所有指针类型的值。因此,为每个指针类型 T* 都隐式定义了下列运算符:T* operator +(T* x, int y);T* operator +(T* x, uint y);T* operator +(T* x, long y);T* operator +(T* x, ulong y
34、);T* operator +(int x, T* y);T* operator +(uint x, T* y);T* operator +(long x, T* y);T* operator +(ulong x, T* y);T* operator (T* x, int y);T* operator (T* x, uint y);T* operator (T* x, long y);T* operator (T* x, ulong y);long operator (T* x, T* y);给定指针类型 T* 的表达式 P 和类型 int、uint、long 或 ulong 的表达式 N,表
35、达式 P + N 和 N + P 的计算结果是一个属于类型 T* 的指针值,该值等于由 P 给出的地址加上 N * sizeof(T)。与此类似,表达式 P - N 的计算结果也是一个属于类型 T* 的指针值,该值等于由 P 给出的地址减去 N * sizeof(T)。给定指针类型 T* 的两个表达式 P 和 Q,表达式 P Q 将先计算 P 和 Q 给出的地址之间的差,然后用 sizeof(T) 去除该差值。计算结果的类型始终为 long。实际上,P - Q 的计算过程是:(long)(P) - (long)(Q) / sizeof(T)。例如:using System;class Test
36、static void Main() unsafe int* values = stackalloc int20;int* p = &values1;int* q = &values15;Console.WriteLine(p - q = 0, p - q);Console.WriteLine(q - p = 0, q - p);生成以下输出:p - q = -14q - p = 14如果在执行上述指针算法时,计算结果超越该指针类型的域,则将以实现所定义的方式截断结果,但是不会产生异常。1.5.7 指针比较在不安全上下文中,=、!=、 运算符(第 Error! Reference source
37、 not found. 节)可以应用于所有指针类型的值。指针比较运算符有:bool operator =(void* x, void* y);bool operator !=(void* x, void* y);bool operator (void* x, void* y);bool operator =(void* x, void* y);由于存在从任何指针类型到 void* 类型的隐式转换,因此可以使用这些运算符来比较任何指针类型的操作数。比较运算符像比较无符号整数一样比较两个操作数给出的地址。1.5.8 sizeof 运算符sizeof 运算符返回由给定类型的变量占用的字节数。被指定为
38、 sizeof 的操作数的类型必须为 unmanaged-type(第 1.2 节)。sizeof-expression:sizeof ( unmanaged-type )sizeof 运算符的结果是 int 类型的值。对于某些预定义类型,sizeof 运算符将产生如下表所示的常数值。表达式结果sizeof(sbyte)1sizeof(byte)1sizeof(short)2sizeof(ushort)2sizeof(int)4sizeof(uint)4sizeof(long)8sizeof(ulong)8sizeof(char)2sizeof(float)4sizeof(double)8si
39、zeof(bool)1对于所有其他类型,sizeof 运算符的结果是由实现定义的,并且属于值而不是常量。一个结构所属的各个成员以什么顺序被装入该结构中,没有明确规定。出于对齐的目的,在结构的开头、结构内以及结构的结尾处可以插入一些未命名的填充位。这些填充位的内容是不确定的。当 sizeof 应用于具有结构类型的操作数时,结果是该类型变量所占的字节总数(包括所有填充位在内)。1.6 fixed 语句在不安全上下文中,embedded-statement(第 Error! Reference source not found. 章)产生式允许使用一个附加结构即 fixed 语句,该语句用于“固定”可移动变量,从而使该变量的地址在语句的持续时间内保持不变。embedded-statement:.fixed-statementfixed-statement:fixed ( pointer-type fixed-pointer-declarators ) embedded-statementfixed-pointer-declarators:fixed-pointer-declaratorfixed-pointer-declarators , fix
限制150内