不安全的程式碼、指標型別和函式指標

您撰寫的大部分 C# 程式代碼都是「可驗證的安全程式碼」。可驗證的安全程式碼 表示 .NET 工具可以驗證程式碼是否安全。 一般而言,安全程式碼不會使用指標直接存取記憶體。 也不會配置原始記憶體。 它會改為建立受控物件。

C# 支援 unsafe 內容,您可以在其中寫入無法驗證的程式碼。 在 unsafe 內容中,程式碼可能會使用指標、配置和釋放記憶體區塊,以及使用函式指標呼叫方法。 C# 中不安全的程式碼不一定具有危險性,這不過是其安全性無法驗證的程式碼。

不安全的程式碼具有下列屬性:

  • 方法、類型和程式碼區塊可以定義為不安全。
  • 在某些情況下,不安全的程式碼可能會藉由移除陣列界限檢查,來提升應用程式的效能。
  • 當您呼叫需要指標的原生函式時,需要不安全的程式碼。
  • 使用不安全的程式碼會帶來安全性和穩定性風險。
  • 包含不安全之區塊的程式碼必須使用 AllowUnsafeBlocks 編譯器選項編譯。

指標型別

在不安全內容中,除了實值型別或參考型別,型別也有可能是指標型別。 指標類型宣告會使用下列其中一種格式:

type* identifier;
void* identifier; //allowed but not recommended

在指標型別中的 * 之前指定的型別稱為參考型別。 只有 unmanaged 型別才可以是參考型別。

指標型別不會從 object 繼承,而且指標型別與 object 之間無法進行轉換。 此外,boxing 和 unboxing 不支援指標。 不過,不同的指標類型之間以及指標類型與整數類資料類型之間可以進行轉換。

當您在同一個宣告中宣告多個指標時,只能將星號 (*) 與基礎類型一起寫入。 它不會作為每個指標名稱的前置詞。 例如:

int* p1, p2, p3;   // Ok
int *p1, *p2, *p3;   // Invalid in C#

指標無法指向參考或包含參考的 struct,因為即使指標指向物件參考,依然可以對物件參考進行記憶體回收。 記憶體回收行程不會持續追蹤是否有任何指標型別指向物件。

MyType* 類型的指標變數值是 MyType 類型變數的位址。 以下是指標類型宣告的範例:

  • int* pp 為整數的指標。
  • int** pp 為整數指標的指標。
  • int*[] pp 為整數的一維陣列指標。
  • char* pp 為字元的指標。
  • void* pp 為未知型別的指標。

您可以使用指標間接運算子 * 存取指標變數所指向位置的內容。 例如,請參考下列宣告:

int* myVariable;

這個運算式 *myVariable 表示位於 int 所包含之位址的 myVariable 變數。

fixed陳述式文章中有數個指標範例。 下列範例使用 unsafe 關鍵字和 fixed 陳述式,並示範如何讓內部指標遞增。 您可以將這個程式碼貼入主控台應用程式的 Main 函式中來執行它 這些範例必須使用 AllowUnsafeBlocks 編譯器選項集合來編譯。

// Normal pointer to an object.
int[] a = [10, 20, 30, 40, 50];
// Must be in unsafe code to use interior pointers.
unsafe
{
    // Must pin object on heap so that it doesn't move while using interior pointers.
    fixed (int* p = &a[0])
    {
        // p is pinned as well as object, so create another pointer to show incrementing it.
        int* p2 = p;
        Console.WriteLine(*p2);
        // Incrementing p2 bumps the pointer by four bytes due to its type ...
        p2 += 1;
        Console.WriteLine(*p2);
        p2 += 1;
        Console.WriteLine(*p2);
        Console.WriteLine("--------");
        Console.WriteLine(*p);
        // Dereferencing p and incrementing changes the value of a[0] ...
        *p += 1;
        Console.WriteLine(*p);
        *p += 1;
        Console.WriteLine(*p);
    }
}

Console.WriteLine("--------");
Console.WriteLine(a[0]);

/*
Output:
10
20
30
--------
10
11
12
--------
12
*/

您無法將間接運算子套用至 void* 型別的指標。 不過,您可以使用轉型,將 Void 指標轉換成任何其他指標類型,反之亦然。

指標可以是 null。 將間接運算子套用至 null 指標會產生實作定義的行為。

在方法之間傳遞指標時,可能會導致未定義的行為。 請考慮使用透過 inoutref 參數或是以函式結果的方式,將指標傳至區域變數的方法。 如果已在固定區塊中設定指標,則該指標指向的變數就可能不再是固定的。

下表所列出的運算子和陳述式可以用於 unsafe 內容中的指標:

運算子/陳述式 使用
* 執行指標間接取值。
-> 透過指標存取結構的成員。
[] 索引指標。
& 取得變數位址。
++-- 遞增和遞減指標。
+- 執行指標算術。
==!=<><=>= 比較指標。
stackalloc 在堆疊上配置記憶體。
fixed 陳述式 暫時固定變數以便找到其位址。

如需指標相關運算子的詳細資訊,請參閱指標相關運算子

任何指標型別都可以隱含轉換成 void* 型別。 任何指標型別都可以指派值 null。 任何指標型別都可以使用轉換運算式明確轉換成任何其他指標型別。 您也可以將任何整數型別轉換成指標型別,或將任何指標型別轉換成整數型別。 這些轉換需要明確的轉換。

下列範例會將 int* 轉換為 byte*。 請注意,指標會指向變數的最低定址位元組。 當您連續遞增結果時 (大小最多為 int (4 個位元組)),可以顯示變數的剩餘位元組。

int number = 1024;

unsafe
{
    // Convert to byte:
    byte* p = (byte*)&number;

    System.Console.Write("The 4 bytes of the integer:");

    // Display the 4 bytes of the int variable:
    for (int i = 0 ; i < sizeof(int) ; ++i)
    {
        System.Console.Write(" {0:X2}", *p);
        // Increment the pointer:
        p++;
    }
    System.Console.WriteLine();
    System.Console.WriteLine("The value of the integer: {0}", number);

    /* Output:
        The 4 bytes of the integer: 00 04 00 00
        The value of the integer: 1024
    */
}

固定大小的緩衝區

您可以使用 fixed 關鍵字,在資料結構中建立具有固定大小陣列的緩衝區。 當您寫入的方法會與其他語言或平台的資料來源交互操作時,固定大小緩衝區就很有用。 固定大小緩衝區可接受一般結構成員所允許的任何屬性或修飾詞。 唯一的限制是陣列類型必須為 boolbytecharshortintlongsbyteushortuintulongfloatdouble

private fixed char name[30];

在安全的程式碼中,包含陣列的 C# 結構不包含陣列元素。 相反地,該結構會包含元素的參考。 您可以將固定大小的陣列嵌入用於不安全程式碼區塊的 struct

下列 struct 的大小不取決於陣列中的元素數目,因為 pathName 是參考:

public struct PathArray
{
    public char[] pathName;
    private int reserved;
}

stuct 可以在不安全的程式碼中包含內嵌陣列。 在下列範例中,fixedBuffer 陣列具有固定大小。 您可以使用 fixed 陳述式建立第一個元素的指標。 透過這個指標即可存取陣列的項目。 fixed 陳述式會將 fixedBuffer 執行個體欄位釘選到記憶體中的特定位置。

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

internal unsafe class Example
{
    public Buffer buffer = default;
}

private static void AccessEmbeddedArray()
{
    var example = new Example();

    unsafe
    {
        // Pin the buffer to a fixed location in memory.
        fixed (char* charPtr = example.buffer.fixedBuffer)
        {
            *charPtr = 'A';
        }
        // Access safely through the index:
        char c = example.buffer.fixedBuffer[0];
        Console.WriteLine(c);

        // Modify through the index:
        example.buffer.fixedBuffer[0] = 'B';
        Console.WriteLine(example.buffer.fixedBuffer[0]);
    }
}

128 個元素的 char 陣列大小為 256 個位元組。 不論編碼為何,在固定大小的 char 緩衝區中,每個字元一律會有 2 個位元組。 即使 char 緩衝區封送為具有 CharSet = CharSet.AutoCharSet = CharSet.Ansi 的 API 方法或結構,此陣列大小也相同。 如需詳細資訊,請參閱CharSet

上述範例示範在沒有釘選的情況下存取 fixed 欄位。 另一個常見的固定大小陣列是 bool 陣列。 bool 陣列中的元素大小一律為 1 個位元組。 bool 陣列不適用於建立位元陣列或緩衝區。

固定大小的緩衝區會使用 System.Runtime.CompilerServices.UnsafeValueTypeAttribute 編譯,其會指示通用語言執行平台 (CLR) 型別包含可能會溢位的非受控陣列。 使用 stackalloc 配置的記憶體也會自動啟用 CLR 中的緩衝區溢位偵測功能。 上一個範例顯示固定大小緩衝區如何存在於 unsafe struct 中。

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

Buffer 的編譯器產生的 C# 屬性如下:

internal struct Buffer
{
    [StructLayout(LayoutKind.Sequential, Size = 256)]
    [CompilerGenerated]
    [UnsafeValueType]
    public struct <fixedBuffer>e__FixedBuffer
    {
        public char FixedElementField;
    }

    [FixedBuffer(typeof(char), 128)]
    public <fixedBuffer>e__FixedBuffer fixedBuffer;
}

固定大小緩衝區與一般陣列的差異如下:

  • 可能只能在 unsafe 內容中使用。
  • 可能只能是結構的執行個體欄位。
  • 它們一律是向量或一維陣列。
  • 宣告應該包含長度,例如 fixed char id[8]。 您無法使用 fixed char id[]

如何使用指標複製位元組陣列

下列範例會使用指標,以將位元組從某個陣列複製到另一個陣列。

這個範例會使用 unsafe 關鍵字,讓您可以使用 Copy 方法中的指標。 fixed 陳述式用來宣告來源和目的陣列的指標。 fixed 陳述式會將來源和目的陣列的位置「釘選」到記憶體中,讓記憶體回收不會移動它們。 fixed 區塊完成時,會取消釘選陣列的記憶體區塊。 因為這個範例中的 Copy 方法使用 unsafe 關鍵字,所以必須使用 AllowUnsafeBlocks 編譯器選項編譯。

這個範例會使用索引 (而不是第二個非受控指標) 來存取這兩個陣列的項目。 pSourcepTarget 指標的宣告會釘選到陣列。

static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
    int targetOffset, int count)
{
    // If either array is not instantiated, you cannot complete the copy.
    if ((source == null) || (target == null))
    {
        throw new System.ArgumentException("source or target is null");
    }

    // If either offset, or the number of bytes to copy, is negative, you
    // cannot complete the copy.
    if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
    {
        throw new System.ArgumentException("offset or bytes to copy is negative");
    }

    // If the number of bytes from the offset to the end of the array is
    // less than the number of bytes you want to copy, you cannot complete
    // the copy.
    if ((source.Length - sourceOffset < count) ||
        (target.Length - targetOffset < count))
    {
        throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
    }

    // The following fixed statement pins the location of the source and
    // target objects in memory so that they will not be moved by garbage
    // collection.
    fixed (byte* pSource = source, pTarget = target)
    {
        // Copy the specified number of bytes from source to target.
        for (int i = 0; i < count; i++)
        {
            pTarget[targetOffset + i] = pSource[sourceOffset + i];
        }
    }
}

static void UnsafeCopyArrays()
{
    // Create two arrays of the same length.
    int length = 100;
    byte[] byteArray1 = new byte[length];
    byte[] byteArray2 = new byte[length];

    // Fill byteArray1 with 0 - 99.
    for (int i = 0; i < length; ++i)
    {
        byteArray1[i] = (byte)i;
    }

    // Display the first 10 elements in byteArray1.
    System.Console.WriteLine("The first 10 elements of the original are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray1[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of byteArray1 to byteArray2.
    Copy(byteArray1, 0, byteArray2, 0, length);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of the last 10 elements of byteArray1 to the
    // beginning of byteArray2.
    // The offset specifies where the copying begins in the source array.
    int offset = length - 10;
    Copy(byteArray1, offset, byteArray2, 0, length - offset);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");
    /* Output:
        The first 10 elements of the original are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        90 91 92 93 94 95 96 97 98 99
    */
}

函式指標

C# 提供 delegate 型別定義安全函式指標物件。 叫用委派牽涉到具現化衍生自 System.Delegate 的型別,並對其 Invoke 方法進行虛擬方法呼叫。 此虛擬呼叫會使用 callvirt 中繼語言 (IL) 指令。 在效能關鍵程式碼路徑中,使用 calli 中繼語言 (IL) 指令會更有效率。

您可以使用 delegate* 語法定義函式指標。 編譯器會使用 calli 指令來呼叫函式,而不是具現化 delegate 物件並呼叫 Invoke。 下列程式碼會宣告兩個方法,這些方法會使用 delegatedelegate* 合併兩個相同型別的物件。 第一個方法會使用System.Func<T1,T2,TResult>委派型別。 第二個方法會使用相同的參數和傳回型別的 delegate* 宣告:

public static T Combine<T>(Func<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

public static T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

下列程式碼示範如何宣告靜態區域函式,並使用該區域函式的指標叫用 UnsafeCombine 方法:

static int localMultiply(int x, int y) => x * y;
int product = UnsafeCombine(&localMultiply, 3, 4);

上述程式碼說明以函式指標方式存取的函式上的數個規則:

  • 函式指標只能在 unsafe 內容中宣告。
  • 接受 delegate* (或傳回 delegate*) 的方法只能在 unsafe 內容中呼叫。
  • 只有在 static 函式上才允許取得函式位址的 & 運算子。 (此規則同時適用於成員函式和區域函式)。

語法與宣告 delegate 型別和使用指標平行。 delegate 上的 * 尾碼表示宣告是 函式指標& 將方法群組指派給函式指標時,表示作業會接受方法的位址。

您可以使用關鍵字 managedunmanaged 指定 delegate* 的呼叫慣例。 此外,針對 unmanaged 函式指標,您可以指定呼叫慣例。 下列宣告顯示每個宣告的範例。 第一個宣告會使用 managed 呼叫慣例,此為預設值。 接下來四個宣告會使用 unmanaged 呼叫慣例。 每個都會指定其中一個 ECMA 335 呼叫慣例:CdeclStdcallFastcallThiscall。 最後一個宣告會使用 unmanaged 呼叫慣例,指示通用語言執行平台 (CLR) 挑選平台的預設呼叫慣例。 CLR 會在執行時間選擇呼叫慣例。

public static T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
    combinator(left, right);

您可以在函式指標功能規格中深入瞭解函式指標。

C# 語言規格

如需詳細資訊,請參閱 C# 語言規格中的不安全的程式碼一節。