C#锐利体验 第十八讲 非安全代码

来源:互联网 发布:卡玛d1c知乎 编辑:程序博客网 时间:2024/04/19 17:44
C#锐利体验

南京邮电学院 李建忠(lijianzhong@263.net.cn

第十八讲 非安全代码

.NET通用语言运行时为C#引入了一种托管的安全编程方式。指针存取,变量地址计算,对象销毁等等操作在托管编程环境下都是C#所不允许的,这大大改进了传统C/C++的安全问题。但事物往往是多面性的,在我们摈除指针等内存的直接存取方式的同时,我们也丧失了它在某些问题上的便利性,比如某些和操作系统底层的交互,内存映射设备的存取等等。在某些特殊的任务上,我们甚至不希望引入自动垃圾收集这种“不确定的系统消耗”。C#通过引入非安全(unsafe)代码来迎合这些特殊需要。

非安全代码
???????在非安全代码中,C#允许我们直接操作指针,获取变量地址,进行指针类型与整数类型之间的转换等等在C/C++中经常出现的操作。非安全代码通过关键字“unsafe”来标志。编译非安全代码时必须加编译选项“/unsafe”,否则编译器报错。“unsafe”关键字可以加之于类型(类,结构,接口,委派)声明,成员(构造器,析构器,域,方法,属性,事件,索引器,操作符)声明,以及用大括号“{}”括起来的语句块。标志了unsafe的代码块又称为unsafe上下文。看下面的例程:

using System;
unsafe struct Point//unsafe类型
{
?????public int* x,y;

?????public Point(int* a,int* b)
?????{
?????????x=a;
?????????y=b;
?????}
}
class Test
{
?????public static void Swap(ref int x, ref int y)
?????{
?????????int a;
?????????a=x; x=y; y=a;
?????}

?????public static unsafe void Swap(ref int *x,ref int *y)//unsafe成员
?????{
?????????int* a;
?????????a=x; x=y; y=a;
?????}

?????public static void Main()
?????{
?????????int a=1,b=2;
?????????unsafe//unsafe语句块
?????????{
??????????????Point pt=new Point(&a,&b);//初始化
??????????????Console.WriteLine("初始值 :");
??????????????Console.WriteLine("Values:??*pt.x= {0}, *pt.y= {1}", *pt.x, *pt.y);
??????????????Console.WriteLine("Address: pt.x= {0}, pt.y= {1}",(int)pt.x, (int)pt.y);
??????????????
??????????????Swap(ref *pt.x,ref *pt.y);//交换值
??????????????Console.WriteLine("换值之后 :");
??????????????Console.WriteLine("Values:??*pt.x= {0}, *pt.y= {1}", *pt.x, *pt.y);
??????????????Console.WriteLine("Address: pt.x= {0}, pt.y= {1}",(int)pt.x, (int)pt.y);
????????????????????????????
??????????????Swap(ref pt.x, ref pt.y);//交换地址
??????????????Console.WriteLine("换址之后 :");
??????????????Console.WriteLine("Values:??*pt.x= {0}, *pt.y= {1}", *pt.x, *pt.y);
??????????????Console.WriteLine("Address: pt.x= {0}, pt.y= {1}",(int)pt.x, (int)pt.y);
?????????}
?????}
}

程序输出:
初始值 :
Values:??*pt.x= 1, *pt.y= 2
Address: pt.x= 1243332, pt.y= 1243328
换值之后 :
Values:??*pt.x= 2, *pt.y= 1
Address: pt.x= 1243332, pt.y= 1243328
换址之后 :
Values:??*pt.x= 1, *pt.y= 2
Address: pt.x= 1243328, pt.y= 1243332

在上面的程序中,我们演示了典型的三种unsafe上下文。我们用unsafe 来修饰Point结构类型,从而可以在该结构内任意地方操作指针。
注意其中的成员声明语句“public int* x,y;”声明x和y都为指向整数的指针,这在传统C/C++中需要同时在x,y前面加星号(*),如“public int* x,*y;”。
在Test类中的Swap方法声明中加上unsafe修饰后,便可以传入指针类型的参数,并在方法体内进行指针操作。
在Main函数中,我们则采用了unsafe语句块的处理方式,这使得语句块内可以进行指针存取操作。两个Swap函数,一个交换指针指向的数据,一个交换指针地址,我们从程序的输出也可以看到这一点。
需要指出的是,“非安全代码并非不安全”!它仅仅是指示其中的内存不受自动垃圾收集器管理,而需要我们象以前在C/C++中那样自己负责分配和释放。

指针类型
???????指针类型和我们前面的托管环境下的引用类型有点相似,类型本身都不包含数据,数据包含在他们指向的内存区块。但指针类型和托管环境的引用类型有着本质的区别——指针指向的数据区块不受自动垃圾收集器追踪,而引用句柄指向的数据对象却受自动垃圾收集器追踪、管理。实际上自动垃圾收集器根本不知道指针及其数据的存在!
???????指针类型包含其指向的内存块的数据类型,这个数据类型在C#中被限制为“非托管类型”和“void类型”。其中“非托管类型”定义为下列类型之一:
1.??sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal和bool;
2.??枚举类型;
3.??自定义结构类型,其成员变量只能为“非托管类型”;
4.??指针类型。
可以看到,“非托管类型”不能是引用类型。实际上,指针类型指向的内存区块根本不能是引用类型,也不能是包含引用类型的自定义结构。但引用类型可以包含指针类型,这是为什么呢?理解这一点并不困难,因为引用类型会被自动垃圾收集器管理,而声明为指针指向的“非托管类型”不被自动垃圾收集器管理!
???????虽然指针在传递参数的时候,也可以用out和ref来表达传址(传引用),从而使得我们可以在函数内改变指针变量本身(也就是变量的地址),本文第一个例子中的Swap函数就这样做的。但和C/C++中的传递指针参数一样,如果我们在这样的函数中将指针的值改变为函数内局部变量的地址,在我们退出函数时,由于系统往往会回收这些地址空间,程序便会发生未定义的行为。看下面的例子:

using System;
class Test
{
?????unsafe static void F(out int* pi1, ref int* pi2)
?????{
?????????int i=100;
?????????int j=200;
?????????pi1 = &i;
?????????pi2 = &j;
?????????Console.WriteLine((int)pi1+","+(int)pi2);
?????}
?????unsafe static void Main()
?????{
?????????int i = 10;????????
?????????int* px1 = &i;
?????????int* px2 = &i;
?????????Console.WriteLine((int)px1+","+(int)px2);
?????????
?????????F(out px1, ref px2);

?????????Console.WriteLine((int)px1+","+(int)px2);
?????????Console.WriteLine("*px1 = {0}, *px2 = {1}",*px1, *px2);
?????}
}

程序输出:
1243332,1243332
1243304,1243308
1243304,1243308
*px1 = 13249636, *px2 = 13253284
???????注意前三行的结果依赖于特定的执行环境,最后一行结果就属于未定义的程序行为。可以看到,函数F内的变量i,j的存储空间在退出函数栈后被回收,指针变量px1,px2指向的数值将不确定。鉴于此,我们一般不要用out修饰指针类型参数,只有在确定改变的地址空间在退出函数后不会被回收,才可以使用ref来修饰指针类型参数。
???????指针类型可以和8种C#的整数类型之间进行转化,这种转化必须用括号“()”的形式进行明晰转化。空类型“null”也被可以作为指针类型,表示地址为0,不指向有效数据。指向不同的数据类型(不包括void类型)的指针之间也存在明晰转化。void*类型转化为其他托管类型的值针类型时为明晰转化。反之有其他指针类型转化为void*类型时为隐含转化,可以自动进行。C#不保证指针类型之间的转化是安全的,可以通过C#的异常捕捉机制来处理这种可能的情况。
???????和C/C++类似,指针类型可以参与相当多的表达式运算。除了前面已经接触到的如指针提领运算(*p=value),取地址运算(p=&value),还有结构成员获取运算(p->value),指针元素获取运算(p[0]),指针增量与减量运算(p++,p--,++p,--p),sizeof运算(sizeof(unmanaged-type)),指针之间的比较运算,以及指针和整数(int,uint,long,ulong)之间,指针之间的加减运算。这些表达式运算都需放置在unsafe标志的代码中,否则引起编译时错误。

fixed语句
???????从内存管理的角度可以把C#中的变量分为两类,一类是固定变量,另一类是可动变量。固定变量一般寄宿于方法调用的栈中,它们的内存不受自动垃圾收集器管理。
固定变量的例子有非托管类型的局部变量及其实例域成员,非托管类型的传值参数,以及提领指针所创建的变量等。一个判别固定变量的方法是看它是否可以无任何限制地用符号&来获取它的地址。看下面的例程:

using System;
struct Point
{
?????public int x;
?????public int y;
?????public int z;
}
unsafe class Test
{
?????static void Main()
?????{
?????????int integer=100;???
?????????PrintAddress(integer);

?????????int* ptInteger=&integer;
?????????Console.WriteLine((int)&(*ptInteger));//提领指针所创建的变量

?????????Point point=new Point();
?????????Console.WriteLine((int)&point);//非托管类型局部变量
?????????Console.WriteLine((int)&point.x);//非托管类型局部变量的实例成员
?????????Console.WriteLine((int)&point.y);//非托管类型局部变量的实例成员
?????????Console.WriteLine((int)&point.z);//非托管类型局部变量的实例成员
?????}

?????static void PrintAddress(int i)
?????{
?????????Console.WriteLine((int)&i);//非托管类型的传值参数
?????}
}


???????可动变量寄宿于托管堆中,它们的内存全权交由自动垃圾收集器管理。可动变量的例子有引用类型对象的实例域成员,任何类型的静态域成员,引用或者输出参数,数组元素等。不可以直接用符号&获取可动变量的地址,C#支持用fixed语句来暂时性地获取可动变量的地址。该地址只有在紧跟在fixed后面的语句块内才有效。看下面的例程:

using System;
class MyClass
{????
?????public static int StaticField;
?????public int InstanceField;
}
unsafe class Test
{
?????static void PrintAddress(int* point)
?????{
?????????Console.WriteLine((int)point);
?????}
?????public static void Main()
?????{
?????????MyClass myObject = new MyClass();
?????????int[] arr = new int[10];
?????????fixed (int* p = &MyClass.StaticField) PrintAddress(p);
?????????fixed (int* p = &myObject.InstanceField) PrintAddress(p);
?????????fixed (int* p = &arr[0]) PrintAddress(p);
?????????fixed (int* p = arr) PrintAddress(p);
?????}
}


sizeof操作符
???????sizeof操作符可以用来计算非托管类型的在栈分配时的存储尺寸,它的结果是以字节为单位的整数。sizeof操作符的操作对象是托管类型,变量不可以做sizeof操作符的参数。
???????对于C#中的简单类型,由于他们的存储空间已被系统确定,sizeof返回他们的所占字节数为常数。对于自定义枚举类型,sizeof返回该枚举类型指定的基类型的存储尺寸,如果没有指定枚举的基类型,C#默认为32位的整数类型。对于指针类型,它的存储尺寸和32位的整数类型相同,自然sizeof会返回4个字节。看下面的例程:

using System;
enum MyEnum:byte
{
?????北京,
?????上海,
?????南京
}
class Test
{
?????public unsafe static void Main()
?????{
?????????Console.Write("sizeof(sbyte) :{0}??/t",sizeof(sbyte));
?????????Console.WriteLine("sizeof(byte) "+sizeof(byte));
?????????Console.Write("sizeof(short) :{0}??/t",sizeof(short));
?????????Console.WriteLine("sizeof(ushort) :"+sizeof(ushort));
?????????Console.Write("sizeof(int) :{0}???/t",sizeof(int));
?????????Console.WriteLine("sizeof(uint) :"+sizeof(uint));
?????????Console.Write("sizeof(long) :{0}???/t",sizeof(long));
?????????Console.WriteLine("sizeof(ulong) :"+sizeof(ulong));

?????????Console.Write("sizeof(char) :{0}???/t",sizeof(char));
?????????Console.WriteLine("sizeof(bool) :"+sizeof(bool));
?????????Console.Write("sizeof(float) :{0}???/t",sizeof(float));
?????????Console.WriteLine("sizeof(double) :"+sizeof(double));

?????????Console.Write("sizeof(MyEnum) :{0}???/t",sizeof(MyEnum));
?????????Console.WriteLine("sizeof(int*) :{0}???/t",sizeof(int*));
?????}
}

程序输出:
sizeof(sbyte) :1?????????sizeof(byte) 1
sizeof(short) :2?????????sizeof(ushort) :2
sizeof(int) :4??????????sizeof(uint) :4
sizeof(long) :8??????????sizeof(ulong) :8
sizeof(char) :2??????????sizeof(bool) :1
sizeof(float) :4??????????sizeof(double) :8
sizeof(MyEnum) :1??????????sizeof(int*) :4?????????

对于托管的自定义结构类型,sizeof在计算它的存储尺寸时,要考虑各域成员变量的存储空间在排列时的情况。如果没有用StructLayout特征来明确指定,托管自定义结构类型的域成员变量的存储排列方式将由CLR运行时负责,这时往往会为了排列对齐而对结构的存储空间做一定的填充,也就是说。值得指出的是,成员变量声明的顺序不同,也会导致CLR运行时对它们作不同的排列,进而会有不同的填充行为,也可能会导致sizeof返回值的不同。对于用StructLayout特征明确指定的托管自定义结构,sizeof在要考虑按指定的要求排列的同时,也要考虑对齐填充的问题。看下面的例程:

using System;
using System.Runtime.InteropServices;

struct MyStruct1
{
?????public byte MyByte;// 1 byte
?????public short MyShort;// 2 bytes
?????public int MyInt;// 4 bytes
?????public long MyLong;// 8 bytes
}
struct MyStruct2
{
?????public short MyShort;// 2 bytes
?????public int MyInt;// 4 bytes
?????public long MyLong;// 8 bytes????
?????public byte MyByte;// 1 byte
}
[StructLayout(LayoutKind.Explicit)]
struct MyUnion
{
?????[FieldOffset(0)]
?????public byte MyByte;//从零位开始的byte
?????[FieldOffset(0)]
?????public short MyShort;//从零位开始的short
?????[FieldOffset(0)]
?????public int MyInt;//从零位开始的int
?????[FieldOffset(0)]
?????public long MyLong;//从零位开始的long
}
class Test
{
?????public unsafe static void Main()
?????{
?????????Console.WriteLine("sizeof(MyStruct1) :{0}",sizeof(MyStruct1));
?????????Console.WriteLine("sizeof(MyStruct2) :{0}",sizeof(MyStruct2));
?????????Console.WriteLine("sizeof(MyUnion) :{0}",sizeof(MyUnion));
?????}
}

程序输出:
sizeof(MyStruct1) :16
sizeof(MyStruct2) :24
sizeof(MyUnion) :8
???????我们看到对于所有的域变量都相同的非托管自定义结构MyStruct1和MyStruct2,仅仅由于我们将MyByte域的声明位置放在不同的地方,它们的sizeof的返回值便不同。MyUnion结构由于我们用StructLayout特征指定了它的域成员的存储布局,它的sizeof返回值和其中占位最长的域变量MyLong的返回值相同,这和它作为“联合”的行为是一致的。

内存分配
???????C#没有象C/C++那样的内存动态分配语法(分配于堆上),只提供了应用于局部非托管类型变量的栈分配语句:stackalloc unmanaged-type [ expression ] ,其中expression为整数的表达式或者常量。栈分配空间不需要我们自己清除,函数退出后自动回收。看下面的例子:

using System;
class Test
{
?????unsafe static string IntToString(int value)
?????{
?????????char* buffer = stackalloc char[16];
?????????char* p = buffer + 16;
?????????int n = value >= 0? value: -value;
?????????do
?????????{
??????????????*(--p) = (char)(n % 10 + '0');
??????????????n /= 10;
?????????} while (n != 0);

?????????if (value < 0) *--p = '-';
?????????return new string(p, 0, (int)(buffer + 16 - p));
?????}

?????public static void Main()
?????{
?????????Console.WriteLine(IntToString(12345));
?????????Console.WriteLine(IntToString(-999));
?????}
}


函数IntToString实现整数到字符串的转换,其中我们分配了buffer缓冲字符数组,用来暂时存放转换的字符变量。如果系统内存不够,栈分配语句会抛出System.StackOverflowException异常。这可以用try语句来捕捉。值得指出的是C#规定栈分配语句不可以放在catch或finally语句块内。
???????C#没有提供动态分配语法,但我们如果需要该怎么办?答案是通过引入外部方法来调用特定平台的动态分配服务.看下面的例子:

using System.Runtime.InteropServices;
using System;
public unsafe class Memory
{
?????static int ph = GetProcessHeap();//获得进程堆的句柄
?????private Memory() {}
?????public static void* Alloc(int size) //内存分配
?????{
?????????void* result = HeapAlloc(ph, HEAP_ZERO_MEMORY, size);
?????????if (result == null) throw new OutOfMemoryException();
?????????return result;
?????}
?????public static void Free(void* block) //内存释放
?????{
?????????if (!HeapFree(ph, 0, block)) throw new InvalidOperationException();
?????}

?????const int HEAP_ZERO_MEMORY = 0x00000008;//内存起始地址
?????[DllImport("kernel32")]
?????static extern int GetProcessHeap();
?????[DllImport("kernel32")]
?????static extern void* HeapAlloc(int hHeap, int flags, int size);
?????[DllImport("kernel32")]
?????static extern bool HeapFree(int hHeap, int flags, void* block);
}
class Test
{
?????unsafe static void Main()
?????{
?????????byte* buffer = (byte*)Memory.Alloc(256);
?????????for (int i = 0; i < 256; i++)
??????????????buffer[i] = (byte)i;
?????????for (int i = 0; i < 256; i++)
??????????????Console.WriteLine(buffer[i]);
?????????Memory.Free(buffer);
?????}
}


我们通过调用Win32平台上的kernel32.dll库内的GetProcessHeap, HeapAlloc和HeapFree实现了Memory类的动态内存分配和释放功能.注意这里是在堆上进行内存动态分配,我们必须自己负责释放!
原创粉丝点击