使用 C# 编写不安全代码






4.77/5 (49投票s)
一个简单的教程,展示了如何使用 C# 编写不安全代码
引言
在不安全代码(或称非托管代码)中,可以声明和使用指针。但问题是我们为什么要编写非托管代码?如果我们想编写与操作系统交互的代码,或者想访问内存映射设备,或者想实现一个对时间要求很高的算法,那么使用指针可以带来很多优势。
但是,使用指针也有一些缺点。如果在编译时将指针选择为 32 位量,那么代码将仅限于 4GB 的地址空间,即使它运行在 64 位机器上。如果在编译时将指针选择为 64 位,那么代码将无法在 32 位机器上运行。
在 C# 中,我们可以使用 `unsafe` 修饰符来编写不安全代码。所有不安全代码都必须用 `unsafe` 修饰符明确标记。编写不安全代码就像在 C# 程序中编写 C 代码一样。
现在让我们看看第一个程序
程序 1
using System; class MyClass { public static void Main() { int iData = 10; int* pData = &iData; Console.WriteLine("Data is " + iData); Console.WriteLine("Address is " + (int)pData ); } }
在这个程序中,我使用了一个指针。现在编译这个程序。编译器会给出错误
Microsoft (R) Visual C# Compiler Version 7.00.9030 [CLR version 1.00.2204.21] Copyright (C) Microsoft Corp 2000. All rights reserved. um1.cs(6,8): error CS0214: Pointers may only be used in an unsafe context um1.cs(8,27): error CS0214: Pointers may only be used in an unsafe context
现在让我们稍微修改一下程序,并在函数中添加 `unsafe` 修饰符。
程序 2
using System; class MyClass { public unsafe static void Main() { int iData = 10; int* pData = &iData; Console.WriteLine("Data is " + iData); Console.WriteLine("Address is " + (int)pData ); } }
在这个程序中,`Main()` 函数被定义为 `unsafe`,所以我们可以在这个函数中使用指针。程序的输出是
Data is 10 Address is 1244316
不一定非要把 `unsafe` 修饰符定义在函数上。我们可以定义一个不安全的代码块。让我们再稍微修改一下程序。
程序 3
using System; class MyClass { public static void Main() { unsafe { int iData = 10; int* pData = &iData; Console.WriteLine("Data is " + iData); Console.WriteLine("Address is " + (int)pData ); } } }
在这个程序中,一个代码块是用 `unsafe` 修饰符定义的。所以我们可以在该代码中使用指针。这个程序的输出与上一个程序相同。
现在让我们稍微修改一下程序,以便从指针中获取一个值。
程序 4
using System; class MyClass { public static void Main() { unsafe { int iData = 10; int* pData = &iData; Console.WriteLine("Data is " + iData); Console.WriteLine("Data is " + pData->ToString() ); Console.WriteLine("Address is " + (int)pData ); } } }
我们可以使用 `ToString()` 成员函数从指针中获取数据。让我们稍微修改一下程序,看看它的行为。
程序 5
using System; class MyClass { public static void Main() { testFun(); } public static unsafe void testFun() { int iData = 10; int* pData = &iData; Console.WriteLine("Data is " + iData); Console.WriteLine("Address is " + (int)pData ); } }
在这个程序中,一个带有 `unsafe` 修饰符的函数从一个普通函数中被调用。这个程序表明托管代码可以调用非托管函数。程序的输出与上一个程序相同。
现在稍微修改一下程序,在另一个类中创建一个不安全函数。
程序 6
using System; class MyClass { public static void Main() { TestClass Obj = new TestClass(); Obj.testFun(); } } class TestClass { public unsafe void testFun() { int iData = 10; int* pData = &iData; Console.WriteLine("Data is " + iData); Console.WriteLine("Address is " + (int)pData ); } }
程序的输出与上一个相同。
现在尝试将指针作为参数传递。我们来看看这个程序。
程序 7
using System; class MyClass { public static void Main() { TestClass Obj = new TestClass(); Obj.testFun(); } } class TestClass { public unsafe void testFun() { int x = 10; int y = 20; Console.WriteLine("Before swap x = " + x + " y= " + y); swap(&x, &y); Console.WriteLine("After swap x = " + x + " y= " + y); } public unsafe void swap(int* p_x, int *p_y) { int temp = *p_x; *p_x = *p_y; *p_y = temp; } }
在这个程序中,不安全函数 `testFun()` 调用经典的 `swap()` 函数来交换两个按引用传递的变量的值。现在稍微修改一下程序。
程序 8
using System; class MyClass { public static void Main() { TestClass Obj = new TestClass(); unsafe { int x = 10; int y = 20; Console.WriteLine("Before swap x = " + x + " y= " + y); Obj.swap(&x, &y); Console.WriteLine("After swap x = " + x + " y= " + y); } } } class TestClass { public unsafe void swap(int* p_x, int* p_y) { int temp = *p_x; *p_x = *p_y; *p_y = temp; } }
这个程序与上一个程序做了同样的事情。但是在这个程序中,我们只编写了一个不安全函数,并从 `Main` 中的不安全块调用该函数。
现在让我们看看另一个展示 C# 中数组用法的程序
程序 9
using System; class MyClass { public static void Main() { TestClass Obj = new TestClass(); Obj.fun(); } } class TestClass { public unsafe void fun() { int [] iArray = new int[10]; // store value in array for (int iIndex = 0; iIndex < 10; iIndex++) { iArray[iIndex] = iIndex * iIndex; } // get value from array for (int iIndex = 0; iIndex < 10; iIndex++) { Console.WriteLine(iArray[iIndex]); } } }
这个程序显示了从零到九的数字的平方。
让我们稍微修改一下程序,并将数组作为参数传递给一个函数。
程序 10
using System; class MyClass { public static void Main() { TestClass Obj = new TestClass(); Obj.fun(); } } class TestClass { public unsafe void fun() { int [] iArray = new int[10]; // store value in array for (int iIndex = 0; iIndex < 10; iIndex++) { iArray[iIndex] = iIndex * iIndex; } testFun(iArray); } public unsafe void testFun(int [] p_iArray) { // get value from array for (int iIndex = 0; iIndex < 10; iIndex++) { Console.WriteLine(p_iArray[iIndex]); } } }
程序的输出与上一个相同。
现在让我们稍微修改一下程序,并尝试通过指针而不是索引来获取数组的值。
程序 11
using System; class MyClass { public static void Main() { TestClass Obj = new TestClass(); Obj.fun(); } } class TestClass { public unsafe void fun() { int [] iArray = new int[10]; // store value in array for (int iIndex = 0; iIndex < 10; iIndex++) { iArray[iIndex] = iIndex * iIndex; } // get value from array for (int iIndex = 0; iIndex < 10; iIndex++) { Console.WriteLine(*(iArray + iIndex) ); } } }
在这个程序中,我们尝试通过 `*(iArray + iIndex)` 而不是 `iArray[iIndex]` 来访问数组元素。但是程序会给出以下错误。
Microsoft (R) Visual C# Compiler Version 7.00.9030 [CLR version 1.00.2204.21] Copyright (C) Microsoft Corp 2000. All rights reserved. um11.cs(21,24): error CS0019: Operator '+' cannot be applied to operands of type 'int[]' and 'int'
在 C# 中,`int*` 和 `in[]` 的处理方式不同。为了更清楚地理解这一点,让我们再看一个程序。
程序 12
using System; class MyClass { public static void Main() { TestClass Obj = new TestClass(); Obj.fun(); } } class TestClass { public unsafe void fun() { int [] iArray = new int[10]; iArray++; int* iPointer = (int*)0; iPointer++; } }
这个程序中有两种不同类型的变量。首先,变量 `iArray` 被声明为一个数组,第二个变量 `iPointer` 是一个指针变量。现在我将递增两者。我们可以递增指针变量,因为它没有固定在内存中,但我们不能递增 `iArray`,因为数组的起始地址存储在 `iArray` 中,如果我们允许递增它,我们将丢失数组的起始地址。
程序的输出是一个错误。
Microsoft (R) Visual C# Compiler Version 7.00.9030 [CLR version 1.00.2204.21] Copyright (C) Microsoft Corp 2000. All rights reserved. um12.cs(13,3): error CS0187: No such operator '++' defined for type 'int[]'
要通过指针访问数组的元素,我们必须固定指针,使其不能被递增。C# 使用 `fixed` 保留字来做到这一点。
程序 13
using System; class MyClass { public static void Main() { TestClass Obj = new TestClass(); Obj.fun(); } } class TestClass { public unsafe void fun() { int [] iArray = new int[10]; // store value in array for (int iIndex = 0; iIndex < 10; iIndex++) { iArray[iIndex] = iIndex * iIndex; } // get value from array fixed(int* pInt = iArray) for (int iIndex = 0; iIndex < 10; iIndex++) { Console.WriteLine(*(pInt + iIndex) ); } } }
我们可以使用相同的技术将数组传递给接收指针作为参数的函数。
程序 14
using System; class MyClass { public static void Main() { TestClass Obj = new TestClass(); Obj.fun(); } } class TestClass { public unsafe void fun() { int [] iArray = new int[10]; // store value in array for (int iIndex = 0; iIndex < 10; iIndex++) { iArray[iIndex] = iIndex * iIndex; } // get value from array fixed(int* pInt = iArray) testFun(pInt); } public unsafe void testFun(int* p_pInt) { for (int iIndex = 0; iIndex < 10; iIndex++) { Console.WriteLine(*(p_pInt + iIndex) ); } } }
程序的输出与上一个相同。如果我们尝试访问数组范围之外的元素,它将打印出垃圾数据。
程序 15
using System; class MyClass { public static void Main() { TestClass Obj = new TestClass(); Obj.fun(); } } class TestClass { public unsafe void fun() { int [] iArray = new int[10]; // store value in array for (int iIndex = 0; iIndex < 10; iIndex++) { iArray[iIndex] = iIndex * iIndex; } // get value from array fixed(int* pInt = iArray) testFun(pInt); } public unsafe void testFun(int* p_pInt) { for (int iIndex = 0; iIndex < 20; iIndex++) { Console.WriteLine(*(p_pInt + iIndex) ); } } }
这里我们试图从数组中读取 20 个元素,但数组中只有 10 个元素,所以它将在打印完数组元素后打印出垃圾数据。
程序 16
using System; struct Point { public int iX; public int iY; } class MyClass { public unsafe static void Main() { // reference of point Point refPoint = new Point(); refPoint.iX = 10; refPoint.iY = 20; // Pointer of point Point* pPoint = &refPoint; Console.WriteLine("X = " + pPoint->iX); Console.WriteLine("Y = " + pPoint->iY); Console.WriteLine("X = " + (*pPoint).iX); Console.WriteLine("Y = " + (*pPoint).iY); } }
这里 `pPoint` 是 `Point` 类实例的指针。我们可以使用 `->` 运算符来访问它的元素。
Beta 2 中的更改
当您想使用命令行开关编译程序时,您会在编译器名称后输入程序名称;例如,如果您的程序名为 prog1.cs,那么您将这样编译它
scs prog1.cs
在使用 Beta 1 编写不安全代码时,这工作正常。在 Beta 2 中,Microsoft 为 C# 的命令行编译器添加了一个额外的开关用于编写不安全代码。现在,如果您想编写不安全代码,则必须在命令行编译器中指定 `/unsafe` 命令开关,否则编译器会报错。在 Beta 2 中,如果您想在程序中编写不安全代码,则像这样编译您的程序
csc /unsafe prog1.cs
这里 `prog1.cs` 是程序名称。如果您在不使用 `/unsafe` 开关的情况下编译包含不安全代码的程序,编译器会报错。