C#编写不安全代码 和应用程序域

来源:互联网 发布:电信iptv机顶盒装软件 编辑:程序博客网 时间:2024/04/28 14:08

这篇文章的原文大家可以在 http://www.codeproject.com/csharp/unsafe_prog.asp 找到,作者是Kumar Gaurav Khanna,我仅仅对原文进行翻译,以下是文章的正文

在运行库的控制下执行的代码称作托管代码。相反,在运行库之外运行的代码称作非托管代码。COM 组件、ActiveX 接口和 Win32 API 函数都是非托管代码的示例。

 

有一个特别的主题始终吸引着最多的C/C++程序员,并且它总被认为太过复杂和困难而难以理解:指针!

然而,当讨论C#时,我遇见的大多数人都持有一种观点,认为在C#中没有指针的概念(如果允许我补充的话,他们的这种观点还非常牢固)。事实上,C#是“抹掉”了这个概念。尽管如此,C#编程中的不安全代码讨论的全是关于编程中指针方面的内容。并非如它字面上的意思那样,在编程中使用指针并没有什么不安全。

它之所以被这样称作不安全代码,是因为它不同于常规的.NET开发,不安全代码编程对程序员有特别的要求。在这篇文章中,我将先从两个很容易混淆的概念开始讨论,不安全代码及托管代码。然后将讨论如何编写不安全代码,也就是如何在C#中使用指针。

不安全代码 还是 非托管代码?这是一个问题

托管代码是指在CLR运行环境的监控下运行的代码。CLR运行环境将负责处理各种“家务”工作,比如:

#管理对象的内存

#执行类型检查

#进行内存垃圾回收

以上只提到了一部分。用户不用自己处理上面提到的工作。用户不用自己去直接操作内存,因为CLR运行环境将处理这些问题。

另一方面,所谓非托管代码是指在CLR环境以外运行的代码。这个概念最好的例子就是我们传统的WIN 32 DLL例如Kernel32.dll , user32.dll  和 安装在我们系统上的COM组件。如何为其分配内存空间,如何释放内存,怎么(如果需要)进行类型检测这些工作由自己来做。典型的C++编程中内存分配指针指向也是非托管代码的另一例子,因为你作为程序员需要自己来负责处理这些工作:

#调用内存分配函数

#确保生成的正确性

#确保当任务结束时候内存被释放

 如果你留意的话,你会发现正如上面所说的那样,这些"家务"工作都由CLR运行环境处理,从而将程序员从这些繁重的工作中解放出来.

 不安全代码是托管代码与非托管代码之间的纽带

不安全代码在CLR托管环境的监管下运行,就像托管代码那样,但允许你通过使用指针直接访问内存,就像非托管代码中的做法那样.这样,你同时获得了两个世界里最好的东西.你也许要写的程序需要使用传统WIN 32 DLL中的函数,这些函数需要使用指针.这个时候就轮到不安全代码来帮助你了.

现在,我们已经讨论了区别,让我们开始编码吧...毫无疑问,这是最棒的部分,你认为呢?

不安全代码内部

编写不安全代码需要使用两个特殊的关键字:unsafefixed .我们回忆会知道,总共由3种指针的操作运算:

*

&

->

在任意的语句,代码段,或函数中使用上面的的指针操作运算符,通过使用unsafe 关键字标识为不安全代码,如下面的例子所示:


public unsafe void Triple(int *pInt)

{

  *pInt=(*pInt)*3;

}

上面的函数做的事情是将变量的值乘以3然后再对其赋值.值得注意的是包含需要乘以3的变量的地址的在函数中的使用.函数完成其工作.因为函数使用了"*"指针运算符,因此函数被标记为不安全代码,内存被直接的操作.

但是,还有一个问题.回忆我们之前的讨论,不安全代码是托管代码,因此将在CLR托管环境的监管下运行.现在,CLR运行环境可以有权利移动内存中的对象.这是一个可以减少内存碎片的原因.但这样的操作,对程序员来说是不知道的,是对程序员透明的,被指针指向的变量的内存可能被重现安排倒另外的内存位置.(这是由CLR完成的)

因此,如果 *pInt指向的变量的原始地址是1001 , CLR执行了一些内存重新安排以便减少内存碎片后,该变量的地址之前为1001 , 在重新进行内存安排后可能存储在内存中地址为2003的位置.这会是一个大灾难 , 因为指针指向的1001地址什么都没有了,指针变成了无效的.可能这是在.NET下指针使用被弱化的一个原因.你怎么认为呢?

使用固定指针

我们马上来讨论fixed关键字.当在一段代码中使用该关键字时,就告诉了CLR不用去动内存中的那些"问题"对象,它就不会去动它们了.这样,当在C#中使用指针时,使用fixed关键字就很好的避免了在运行时指针无效的问题.让我们来看看它是怎么做的:


using System;

class CData

{

    public int x;

}

class CProgram

{

   unsafe static void SetVal(int *pInt)

 {

       *pInt=1979;

    }

public unsafe static void Main()

    {

        CData d = new CData();      

        Console.WriteLine("Previous value: {0}", d.x);   

        fixed(int *p=&d.x)

        {

            SetVal(p);

        }

        Console.WriteLine("New value: {0}", d.x);

    }

}

我们在这里做了这些事情,在"固定"代码段,将CData类的x域的地址赋值给了整形变量指针p.现在,因为CLR被告知当"固定"代码段执行过程中不允许CLR移动其位置,当"固定"代码段的语句在执行时,指针所指向的变量在内存中的位置将不会改变,内存中的变量将不会被CLR重新部署内存位置.

这就是在C#中指针的使用.确保该函数是用unsafe标注的,确保被指向的对象用fixed标注,你也就已经有了在C#中使用指针的能力!

 

源文档 <http://blog.csdn.net/gamer_gerald/archive/2007/02/19/1511866.aspx>

编写不安全代码

MSDN:"尽管实际上对 C 或 C++ 中的每种指针类型构造,C# 都设置了与之对应的引用类型,但仍然会有一些场合需要访问指针类型。例如,当需要与基础操作系统进行交互、访问内存映射设备,或实现一些以时间为关键的算法时,若没有访问指针的手段,就不可能或者至少很难完成。为了满足这样的需求,C# 提供了编写不安全代码的能力。

在不安全代码中,可以声明和操作指针,可以在指针和整型之间执行转换,还可以获取变量的地址,等等。在某种意义上,编写不安全代码很像在 C# 程序中编写 C 代码。"

不安全代码必须用修饰符 unsafe 明确地标记。

这里所谓的编写"不安全代码",就是要跳出.net CLR的限制,自己进行地址分配和垃圾回收.在C++里面,我们定义一个简单的指针,至少要做三件事,1.给他分配内存,2.保证类型转换正确3将内存空间释放,而在.net环境里,CLR将程序员从这些事情里解放出来,用户不再需要直接手工地进行内存操作。但是有时候,比如调用windows底层函数,或是效率上的原因使我们需要自己去操作地址空间,本文主要是说明在c#里面指针的用法.

指针的使用、操作内存

1.& 和 *

    c++里面,我们很熟悉这两个东西.在c#里面他们也一样可以用,只不过含有他们的代码如果不在unsafe 标记下,编译器将会将它拒之门外.当然如果条件编译参数没有/unsafe 也是无法编译通过的(如果用vs.net集成编译环境,则在项目属性页-代码生成节将"允许不安全代码块"设置成true).

    &可以取得变量的地址,但是并不是所有的变量,托管类型,将无法取得其地址.c#里面,普通的值类型都是可以取得地址的,比如struct,int,long等,而class是无法取得其地址的,另外string是比较特殊的类型,虽然是值类型,但它也是受管的.这里插一下另一个运算符,sizeof,它也是仅可用于unsafe模式下.

看下面这段代码,里面简单的用到了*,&,sizeof,还有c#里很少见但c++里大家很熟的->:

    class Class1    {        struct Point        {            public int x;            public int y;        }        public static unsafe void Main()         {            Point pt = new Point();            Point* pt1 = &pt;            int* px = &(pt1->x);            int* py = &(pt1->y);            Console.WriteLine("Address of pt is :0x{0:X} ",(uint)&pt);            Console.WriteLine("size of the struct :{0} ",sizeof(Point));            Console.WriteLine("Address of pt.x is :0x{0:X} ",(uint)&(pt.x));            Console.WriteLine("Address of pt.y is :0x{0:X} ",(uint)&(pt.y));            Console.WriteLine("Address of px is :0x{0:X} ",(uint)&(*px));            Console.WriteLine("Address of py is :0x{0:X} ",(uint)&(*py));            Console.ReadLine();        }    }

 

我这里运行的输出结果是:

Address of pt is :0x12F698size of the struct :8Address of pt.x is :0x12F698Address of pt.y is :0x12F69CAddress of px is :0x12F698Address of py is :0x12F69C

 

可以看出struct的首地址与第一个成员变量的地址相同,而这个struct的长度是8个字节(=4+4).

2.fixed

虽然在unsafe模式下可以使用指针,但是unsafe的代码仍然是受管代码.CLR会对它的对象进行管理以及垃圾回收,CLR在这个过程中就会对内存进行重定位,可能过一段时间后,根据指针指向的地址就找不到原来的对象了,岂不是说指针在c#里没有什么实际的作用?别急,还有fixed.

fixed 语句设置指向托管变量的指针并在fixed里的语句块执行期间“锁定”该变量(或者是几个变量)。如果没有 fixed 语句,则指向托管变量的指针将作用很小,因为垃圾回收可能不可预知地重定位变量。(实际上,除非在 fixed 语句中,否则 C# 不允许设置指向托管变量的指针。)

看一段与刚才类似的代码,不同的地方是这里的输出地址的语句都在fixed块里.为什么不直接像第一个例子那样直接输出呢?这是因为我们对Point进行了一个小小的改动,它不再是struct了,它现在是class!它是托管类型了,它的成员都没有固定的地址.但是在fixed块里,它的地址是固定的.

        class Point        {                public static int x;            public int y;        }        public static unsafe void Main()         {            Point pt = new Point();            int[] arr = new int[10];            //如果不用fixed语句,无论是静态成员还是实例成员,都将无法取得其地址。            //int* ps = &CPoint.StaticField;            //PrintAddress(ps);            fixed (int* p = &Point.x)                Console.WriteLine("Address is 0x{0:X}",(int)p);            fixed (int* p = &pt.y)                 Console.WriteLine("Address is 0x{0:X}",(int)p);            fixed (int* p1 = &arr[0],p2 = arr)            {                Console.WriteLine("Address is 0x{0:X}",(int)p1);                Console.WriteLine("Address is 0x{0:X}",(int)p2);            }            Console.ReadLine();        }

 

我这里运行的输出结果是:

Address is 0x3D5404Address is 0x4BF1968Address is 0x4BF1978Address is 0x4BF1978

 

3.分配内存

在堆栈上分配内存

c#提供stackalloc ,在堆栈上而不是在堆上分配一个内存块,语句为 type * ptr = stackalloc type [ expr ];它的大小足以包含 type 类型的 expr 元素;该块的地址存储在 ptr 指针中。此内存不受垃圾回收的制约,因此不必使用fixed将其固定。此内存块的生存期仅限于定义该内存块的方法的生存期。如果内存空间不足,将会抛出System.StackOverflowException异常.

以下是一段示例程序(form msdn),

public static unsafe void Main() {    int* fib = stackalloc int[100];    int* p = fib;    *p++ = *p++ = 1;    //fib[0]=fib[1]=1    for (int i=2; i<100; ++i, ++p)        *p = p[-1] + p[-2];//fib[i]=fib[i-1]+fib[i-2];    for (int i=0; i<10; ++i)        Console.WriteLine (fib[i]);    Console.ReadLine();}

 

在堆上分配内存

既然有stackalloc,有没有heapalloc呢?答案是没有,c#没有提供这样的语法.想在堆上动态分配内存,只能靠自己想办法了.通过Kernel32.dll里的HeapAlloc()和HeapFree()可以达到这个目的.看下面的代码:

using System;using System.Runtime.InteropServices;public unsafe class Memory{    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);    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();    }}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);        Console.ReadLine();

 

}

C#中调用非托管DLL的API详解:http://hi.baidu.com/%DA%E4%C4%B0%B0%EB%D4%B5%BE%FD/blog/item/54b9b63dcce956e83d6d97ad.html

C#非托管代码怎么用(以关机程序为例):http://hi.baidu.com/%DA%E4%C4%B0%B0%EB%D4%B5%BE%FD/blog/item/dbd743c2f76add1d0ef477da.html

将C++托管扩展项目从纯粹的中间语言转换成混合模式:http://www.vckbase.com/document/viewdoc/?id=1403

细致解说C#里使用指针:http://www.va1314.com/bcsl/?viewthread-9508.html

 

应用程序域

 

在.NET平台下,程序集并没有直接承载在进程中(而传统的win32程序是直接承载的)。实际上.NET可执行程序承载在进程的一个逻辑分区中,术语称为应用程序域(也称AppDomain)。可见,一个进程可以包含多个应用程序域,每一个应用程序域中承载一个.NET可执行程序,这样的好处如下:

    应用程序域是.NET平台操作系统独立性的关键特性。这种逻辑分区将不同操作系统加载可执行程序的差异抽象化了。

    和一个完整的进程相比,应用程序域的CPU和内存占用要小的多。因此CLR加载和卸载应用程序域比起完整的进程来说也快的多。

    应用程序域为承载的应用程序提供了深度隔离。如果进程中一个应用程序域失败了,其他的应用程序域也能保持正常。

 

源文档 <http://www.cnblogs.com/longer/archive/2009/02/21/1395276.html>


原创粉丝点击