不安全代码和指针(C# 编程指南)
为了保持类型安全,默认情况下,C# 不支持指针运算。不过,通过使用 unsafe 关键字,可以定义可使用指针的不安全上下文。有关指针的更多信息,请参见主题指针类型。
注意 |
在公共语言运行库 (CLR) 中,不安全代码是指无法验证的代码。C# 中的不安全代码不一定是危险的,只是其安全性无法由 CLR 进行验证的代码。因此,CLR 只对在完全受信任的程序集中的不安全代码执行操作。如果使用不安全代码,由您负责确保您的代码不会引起安全风险或指针错误。有关更多信息,请参见安全性与 C#。 |
不安全代码概述
不安全代码具有下列属性:
· 方法、类型和可被定义为不安全的代码块。
· 在某些情况下,通过移除数组界限检查,不安全代码可提高应用程序的性能。
· 当调用需要指针的本机函数时,需要使用不安全代码。
· 使用不安全代码将引起安全风险和稳定性风险。
· 在 C# 中,为了编译不安全代码,必须用 /unsafe 编译应用程序。
指针类型(C# 编程指南)
在不安全的上下文中,类型可以是指针类型以及值类型或引用类型。指针类型声明具有下列形式之一:
type* identifier;
void* identifier; //allowed but not recommended
下列类型都可以是指针类型:
· sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal 或 bool。
· 任何枚举类型。
· 任何指针类型。
· 仅包含非托管类型的字段的任何用户定义的结构类型。
指针类型不继承 object,并且指针类型与 object 之间不存在转换。此外,装箱和取消装箱不支持指针。但是,允许在不同指针类型之间以及指针类型与整型之间进行转换。
当在同一个声明中声明多个指针时,* 仅与基础类型一起使用,而不是作为每个指针名称的前缀。例如:
int* p1, p2, p3; // Ok
int *p1, *p2, *p3; // Invalid in C#
指针不能指向引用或包含引用的结构,因为即使有指针指向对象引用,该对象引用也可能会被执行垃圾回收。GC 并不注意是否有任何类型的指针指向对象。
myType* 类型的指针变量的值是 myType 类型的变量的地址。下面是指针类型声明的示例:
示例 | 说明 |
int* p | p 是指向整数的指针 |
int** p | p 是指向整数的指针的指针 |
int*[] p | p 是指向整数的指针的一维数组 |
char* p | p 是指向字符的指针 |
void* p | p 是指向未知类型的指针 |
指针间接寻址运算符 * 可用于访问位于指针变量所指向的位置的内容。例如,对于下面的声明,
int* myVariable;
表达式 *myVariable 表示在 myVariable 中包含的地址处找到的 int 变量。
不能对 void* 类型的指针应用间接寻址运算符。但是,可以使用强制转换将 void 指针转换为其他指针类型,反之亦然。
指针可以为 null。如果将间接寻址运算符应用于 null 指针,则会导致由实现定义的行为。
注意,在方法之间传递指针会导致未定义的行为。示例包括通过 Out 或 Ref 参数向局部变量返回指针或作为函数结果向局部变量返回指针。如果将指针设置在固定的块中,它所指向的变量可能不再是固定的。
下表列出可在不安全的上下文中针对指针执行的运算符和语句:
运算符/语句 | 用途 |
* | 执行指针间接寻址。 |
-> | 通过指针访问结构的成员。 |
[] | 对指针建立索引。 |
& | 获取变量的地址。 |
++ 和 -- | 递增或递减指针。 |
加、减 | 执行指针算法。 |
==、!=、<、>、<= 和 >= | 比较指针。 |
stackalloc | 在堆栈上分配内存。 |
fixed 语句 | 临时固定变量以便可以找到其地址。 |
固定大小的缓冲区(C# 编程指南)
在 C# 2.0 中,可以使用 fixed 语句在数据结构中创建固定大小的数组。使用现有代码(如:使用其他语言、预先存在的 DLL 或 COM 项目编写的代码)时这种方法非常有用。固定数组可采用允许普通结构成员使用的任何属性或修饰符。唯一的限制是,数组类型必须是 bool、byte、 char、 short、int、long、sbyte、ushort、uint、ulong、float 或 double。
复制
private fixed char name[30];
备注
在以前版本的 C# 中,声明 C++ 样式的固定大小结构是很困难的,因为包含数组的 C# 结构不包含数组元素,而是包含对元素的引用。
C# 2.0 添加了在结构(当用在不安全的代码块中时)中嵌入固定大小的数组的功能。
例如,在 C# 2.0 之前,下面的 struct 的大小为 8 字节,其中 pathName 数组是对堆分配的数组的引用:
复制
public struct MyArray
{
public char[] pathName;
private int reserved;
}
在 C# 2.0 中,struct 可使用嵌入数组进行声明:
复制
public struct MyArray // This code must appear in an unsafe block
{
public fixed char pathName[128];
}
在此结构中,pathName 数组具有固定的大小和位置,因此可用在其他不安全的代码中。
128 个元素的 char 数组的大小为 256 字节。在固定大小的 char 缓冲区中,每个字符始终占用两个字节,而与编码无关。即使将 char 缓冲区封送到具有 CharSet = CharSet.Auto 或 CharSet = CharSet.Ansi 的 API 方法或结构,也是如此。有关更多信息,请参见 CharSet。
另一种常见的固定大小的数组是 bool 数组。bool 数组中的元素的大小始终为一个字节。bool 数组不适合用于创建位数组或位缓冲区。
注意 |
除了用 stackalloc 创建的内存,C# 编译器和公共语言运行库 (CLR) 不执行任何安全缓冲区溢出检查。与所有不安全代码一样,请谨慎使用。 |
不安全缓冲区与常规数组有如下差异:
· 不安全缓冲区只能用在不安全上下文中。
· 不安全缓冲区始终是向量(或一维数组)。
· 数组的声明应包括计数,如 char id[8]。而不能使用 char id[]。
· 不安全缓冲区只能是不安全上下文中的结构的实例字段。
如何:使用指针复制字节数组(C# 编程指南)
下面的示例使用指针将字节从一个数组复制到另一个使用指针的数组。
此示例使用 unsafe 关键字,它允许在 Copy 方法内使用指针。fixed 语句用于声明指向源数组和目标数组的指针。这将锁定源数组和目标数组在内存中的位置,使其不会因为垃圾回收操作而移动。这些内存块将在 fixed 块结束时取消锁定。因为本示例中 Copy 函数使用了 unsafe 关键字,它必须使用 /unsafe 编译器选项进行编译。
示例
复制
// compile with: /unsafe
复制
class TestCopy
{
// The unsafe keyword allows pointers to be used within the following method:
static unsafe void Copy(byte[] src, int srcIndex, byte[] dst, int dstIndex, int count)
{
if (src == null || srcIndex < 0 ||
dst == null || dstIndex < 0 || count < 0)
{
throw new System.ArgumentException();
}
int srcLen = src.Length;
int dstLen = dst.Length;
if (srcLen - srcIndex < count || dstLen - dstIndex < count)
{
throw new System.ArgumentException();
}
// The following fixed statement pins the location of the src and dst objects
// in memory so that they will not be moved by garbage collection.
fixed (byte* pSrc = src, pDst = dst)
{
byte* ps = pSrc;
byte* pd = pDst;
// Loop over the count in blocks of 4 bytes, copying an integer (4 bytes) at a time:
for (int i = 0 ; i < count / 4 ; i++)
{
*((int*)pd) = *((int*)ps);
pd += 4;
ps += 4;
}
// Complete the copy by moving any bytes that weren't moved in blocks of 4:
for (int i = 0; i < count % 4 ; i++)
{
*pd = *ps;
pd++;
ps++;
}
}
}
static void Main()
{
byte[] a = new byte[100];
byte[] b = new byte[100];
for (int i = 0; i < 100; ++i)
{
a[i] = (byte)i;
}
Copy(a, 0, b, 0, 100);
System.Console.WriteLine("The first 10 elements are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(b[i] + " ");
}
System.Console.WriteLine("\n");
}
}
输出
The first 10 elements are:
0 1 2 3 4 5 6 7 8 9
如何:使用 Windows ReadFile 函数(C# 编程指南)
下面的示例通过读取并显示一个文本文件来演示 Windows ReadFile 函数。ReadFile 函数需要使用 unsafe 代码,因为它需要一个作为参数的指针。
传递到 Read 函数的字节数组是托管类型。这意味着公共语言运行库 (CLR) 垃圾回收器可能会随意地对数组使用的内存进行重新定位。为了防止出现这种情况,使用 fixed 来获取指向内存的指针并对它进行标记,以便垃圾回收器不会移动它。在 fixed 块的末尾,内存将自动返回,以便能够通过垃圾回收移动。
此功能称为“声明式锁定”。锁定的好处是系统开销非常小,除非在 fixed 块中发生垃圾回收(但此情况不太可能发生)。
示例
复制
class FileReader
{
const uint GENERIC_READ = 0x80000000;
const uint OPEN_EXISTING = 3;
System.IntPtr handle;
[System.Runtime.InteropServices.DllImport("kernel32", SetLastError = true)]
static extern unsafe System.IntPtr CreateFile
(
string FileName, // file name
uint DesiredAccess, // access mode
uint ShareMode, // share mode
uint SecurityAttributes, // Security Attributes
uint CreationDisposition, // how to create
uint FlagsAndAttributes, // file attributes
int hTemplateFile // handle to template file
);
[System.Runtime.InteropServices.DllImport("kernel32", SetLastError = true)]
static extern unsafe bool ReadFile
(
System.IntPtr hFile, // handle to file
void* pBuffer, // data buffer
int NumberOfBytesToRead, // number of bytes to read
int* pNumberOfBytesRead, // number of bytes read
int Overlapped // overlapped buffer
);
[System.Runtime.InteropServices.DllImport("kernel32", SetLastError = true)]
static extern unsafe bool CloseHandle
(
System.IntPtr hObject // handle to object
);
public bool Open(string FileName)
{
// open the existing file for reading
handle = CreateFile
(
FileName,
GENERIC_READ,
0,
0,
OPEN_EXISTING,
0,
0
);
if (handle != System.IntPtr.Zero)
{
return true;
}
else
{
return false;
}
}
public unsafe int Read(byte[] buffer, int index, int count)
{
int n = 0;
fixed (byte* p = buffer)
{
if (!ReadFile(handle, p + index, count, &n, 0))
{
return 0;
}
}
return n;
}
public bool Close()
{
return CloseHandle(handle);
}
}
class Test
{
static int Main(string[] args)
{
if (args.Length != 1)
{
System.Console.WriteLine("Usage : ReadFile <FileName>");
return 1;
}
if (!System.IO.File.Exists(args[0]))
{
System.Console.WriteLine("File " + args[0] + " not found.");
return 1;
}
byte[] buffer = new byte[128];
FileReader fr = new FileReader();
if (fr.Open(args[0]))
{
// Assume that an ASCII file is being read.
System.Text.ASCIIEncoding Encoding = new System.Text.ASCIIEncoding();
int bytesRead;
do
{
bytesRead = fr.Read(buffer, 0, buffer.Length);
string content = Encoding.GetString(buffer, 0, bytesRead);
System.Console.Write("{0}", content);
}
while (bytesRead > 0);
fr.Close();
return 0;
}
else
{
System.Console.WriteLine("Failed to open requested file");
return 1;
}
}
}
unsafe(C# 参考)
unsafe 关键字表示不安全上下文,该上下文是任何涉及指针的操作所必需的。有关更多信息,请参见不安全代码和指针(C# 编程指南)。
可以在类型或成员的声明中使用 unsafe 修饰符。因此,类型或成员的整个正文范围均被视为不安全上下文。例如,以下是用 unsafe 修饰符声明的方法:
复制
unsafe static void FastCopy(byte[] src, byte[] dst, int count)
{
// Unsafe context: can use pointers here.
}
不安全上下文的范围从参数列表扩展到方法的结尾,因此指针在以下参数列表中也可以使用:
复制
unsafe static void FastCopy ( byte* ps, byte* pd, int count ) {...}
还可以使用不安全块从而能够使用该块内的不安全代码。例如:
复制
unsafe
{
// Unsafe context: can use pointers here.
}
若要编译不安全代码,必须指定 /unsafe 编译器选项。无法通过公共语言运行库验证不安全代码。
示例
复制
// cs_unsafe_keyword.cs
// compile with: /unsafe
using System;
class UnsafeTest
{
// Unsafe method: takes pointer to int:
unsafe static void SquarePtrParam(int* p)
{
*p *= *p;
}
unsafe static void Main()
{
int i = 5;
// Unsafe method: uses address-of operator (&):
SquarePtrParam(&i);
Console.WriteLine(i);
}
}
输出
25
fixed 语句(C# 参考)
fixed 语句禁止垃圾回收器重定位可移动的变量。fixed 语句只能出现在不安全的上下文中。Fixed 还可用于创建固定大小的缓冲区。
备注
fixed 语句设置指向托管变量的指针并在 statement 执行期间“钉住”该变量。如果没有 fixed 语句,则指向可移动托管变量的指针的作用很小,因为垃圾回收可能不可预知地重定位变量。C# 编译器只允许在 fixed 语句中分配指向托管变量的指针。
复制
// assume class Point { public int x, y; }
// pt is a managed variable, subject to garbage collection.
Point pt = new Point();
// Using fixed allows the address of pt members to be
// taken, and "pins" pt so it isn't relocated.
fixed ( int* p = &pt.x )
{
*p = 1;
}
可以用数组或字符串的地址初始化指针:
复制
fixed (int* p = arr) ... // equivalent to p = &arr[0]
fixed (char* p = str) ... // equivalent to p = &str[0]
只要指针的类型相同,就可以初始化多个指针:
复制
fixed (byte* ps = srcarray, pd = dstarray) {...}
要初始化不同类型的指针,只需嵌套 fixed 语句:
fixed (int* p1 = &p.x)
{
fixed (double* p2 = &array[5])
{
// Do something with p1 and p2.
}
}
执行完语句中的代码后,任何固定变量都被解除固定并受垃圾回收的制约。因此,不要指向 fixed 语句之外的那些变量。
在不安全模式中,可以在堆栈上分配内存。堆栈不受垃圾回收的制约,因此不需要被锁定。有关更多信息,请参见 stackalloc。
示例
// statements_fixed.cs
// compile with: /unsafe
using System;
class Point
{
public int x, y;
}
class FixedTest
{
// Unsafe method: takes a pointer to an int.
unsafe static void SquarePtrParam (int* p)
{
*p *= *p;
}
unsafe static void Main()
{
Point pt = new Point();
pt.x = 5;
pt.y = 6;
// Pin pt in place:
fixed (int* p = &pt.x)
{
SquarePtrParam (p);
}
// pt now unpinned
Console.WriteLine ("{0} {1}", pt.x, pt.y);
}
}
输出
25 6
stackalloc(C# 参考)
在不安全的代码上下文中使用,可以在堆栈上分配内存块。
复制
int* fib = stackalloc int[100];
备注
上面的示例在堆栈而不是堆上分配了一个内存块,它的大小足以包含 100 个 int 类型的元素;该块的地址存储在 fib 指针中。此内存不受垃圾回收的制约,因此不必将其钉住(通过 fixed)。内存块的生存期受定义它的方法的生存期的限制(没有在方法返回之前释放内存的途径)。
stackalloc 仅在局部变量的初始值设定项中有效。
由于涉及指针类型,stackalloc 要求不安全上下文。请参见不安全代码和指针。
stackalloc 类似于 C 运行时库中的 _alloca。
安全性
不安全代码是天生比非不安全替代代码安全性更低的代码。但是,通过使用 stackalloc 可以自动启用公共语言运行库 (CLR) 中的缓冲区溢出检测功能。如果检测到缓冲区溢出,进程将尽快终止,以最大限度地减小执行恶意代码的机会。
示例
复制
// cs_keyword_stackalloc.cs
// compile with: /unsafe
using System;
class Test
{
static unsafe void Main()
{
int* fib = stackalloc int[100];
int* p = fib;
*p++ = *p++ = 1;
for (int i = 2; i < 100; ++i, ++p)
{
*p = p[-1] + p[-2];
}
for (int i = 0; i < 10; ++i)
{
Console.WriteLine(fib[i]);
}
}
}
输出
1
1
2
3
5
8
13
21
34
55