Kode tidak aman, jenis pointer, dan pointer fungsi

Sebagian besar kode C# yang Anda tulis adalah "kode aman yang dapat diverifikasi." Kode aman yang dapat diverifikasi berarti alat .NET dapat memverifikasi bahwa kode aman. Secara umum, kode aman tidak langsung mengakses memori menggunakan pointer. Ini juga tidak mengalokasikan memori mentah. Ini membuat objek terkelola sebagai gantinya.

C# mendukung konteks unsafe, tempat Anda dapat menulis kode yang tidak dapat diverifikasi. Dalam konteks unsafe, kode dapat menggunakan pointer, mengalokasikan dan membebaskan blok memori, dan memanggil metode menggunakan pointer fungsi. Kode tidak aman dalam C# tidak selalu berbahaya; itu hanya kode yang keamanannya tidak dapat diverifikasi.

Kode tidak aman memiliki properti berikut:

  • Metode, jenis, dan blok kode dapat didefinisikan sebagai tidak aman.
  • Dalam beberapa kasus, kode yang tidak aman dapat meningkatkan performa aplikasi dengan menghapus pemeriksaan batas array.
  • Kode tidak aman diperlukan saat Anda memanggil fungsi asli yang memerlukan pointer.
  • Menggunakan kode tidak aman menimbulkan risiko keamanan dan stabilitas.
  • Kode yang berisi blok tidak aman harus dikompilasi dengan opsi compiler AllowUnsafeBlocks.

Jenis pointer

Dalam konteks yang tidak aman, jenis mungkin merupakan jenis pointer, selain jenis nilai, atau jenis referensi. Deklarasi jenis pointer mengambil salah satu formulir berikut:

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

Jenis yang ditentukan sebelum * dalam jenis pointer disebut jenis referensi. Hanya jenis yang tidak terkelola yang dapat menjadi jenis referensi.

Jenis pointer tidak mewarisi dari objek dan tidak ada konversi antara jenis pointer dan object. Selain itu, boxing dan unboxing tidak mendukung pointer. Namun, Anda dapat mengonversi antara jenis pointer yang berbeda dan antara jenis pointer dan jenis integral.

Saat Anda mendeklarasikan beberapa pointer dalam deklarasi yang sama, Anda menulis tanda bintang (*) bersama dengan jenis yang mendasar saja. Ini tidak digunakan sebagai awalan untuk setiap nama pointer. Misalnya:

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

Pointer tidak dapat menunjuk ke referensi atau ke struct yang berisi referensi, karena referensi objek dapat menjadi sampah yang dikumpulkan bahkan jika pointer menunjuk ke referensi tersebut. Pengumpul sampah tidak melacak apakah objek sedang diarahkan oleh jenis pointer apa pun.

Nilai variabel pointer jenis MyType* adalah alamat variabel jenis MyType. Berikut ini adalah contoh deklarasi jenis pointer:

  • int* p: p adalah pointer ke bilangan bulat.
  • int** p: p adalah pointer ke pointer ke bilangan bulat.
  • int*[] p: p adalah array pointer satu dimensi ke bilangan bulat.
  • char* p: p adalah pointer ke char.
  • void* p: p adalah pointer ke jenis yang tidak diketahui.

Operator * tidak langsung pointer dapat digunakan untuk mengakses konten di lokasi yang diarahkan oleh variabel pointer. Misalnya, pertimbangkan dua jenis deklarasi berikut:

int* myVariable;

Ekspresi *myVariable menunjukkan variabel int yang ditemukan di alamat yang terkandung dalam myVariable.

Ada beberapa contoh pointer dalam artikel tentang fixed pernyataan. Contoh berikut menggunakan kata kunci unsafe dan pernyataan fixed, dan menunjukkan cara menaikkan pointer interior. Anda dapat menempelkan kode ini ke fungsi Utama aplikasi konsol untuk menjalankannya. Contoh-contoh ini harus dikompilasi dengan kumpulan opsi compiler 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
*/

Anda tidak dapat menerapkan operator tidak langsung ke pointer jenis void*. Namun, Anda dapat menggunakan transmisi untuk mengonversi pointer ke jenis pointer lainnya, dan sebaliknya.

Pointer dapat berupa null. Menerapkan operator tidak langsung ke pointer null menyebabkan perilaku yang ditentukan implementasi.

Meneruskan pointer antar metode dapat menyebabkan perilaku yang tidak terdefinisi. Pertimbangkan metode yang mengembalikan pointer ke variabel lokal melalui parameter in, out, atau ref atau sebagai hasil fungsi. Jika pointer diatur dalam blok tetap, variabel yang titiknya mungkin tidak lagi diperbaiki.

Tabel berikut mencantumkan operator dan pernyataan yang dapat beroperasi pada pointer dalam konteks yang tidak aman:

Operator/Pernyataan Menggunakan
* Melakukan indirection pointer.
-> Mengakses anggota struct melalui pointer.
[] Mengindeks pointer.
& Mendapatkan alamat variabel.
++ dan -- Kenaikan dan penurunan pointer.
+ dan - Melakukan aritmatika pointer.
==, !=, <, >, <=, dan >= Membandingkan pointer.
stackalloc Mengalokasikan memori pada tumpukan.
fixed pernyataan Memperbaiki variabel untuk sementara waktu sehingga alamatnya dapat ditemukan.

Untuk informasi selengkapnya tentang operator terkait pointer, lihat Operator terkait pointer.

Jenis pointer apa pun dapat dikonversi secara implisit menjadi jenis void*. Jenis pointer apa pun dapat diberi nilai null. Jenis pointer apa pun dapat dikonversi secara eksplisit ke jenis pointer lainnya menggunakan ekspresi transmisi. Anda juga dapat mengonversi jenis integral apa pun menjadi jenis pointer, atau jenis pointer apa pun ke jenis integral. Konversi ini memerlukan cast eksplisit.

Contoh berikut mengonversi int* menjadi byte*. Perhatikan bahwa pointer menunjuk ke byte terendah yang ditangani dari variabel. Saat Anda secara berturut-turut menambah hasil, hingga ukuran int (4 byte), Anda dapat menampilkan byte variabel yang tersisa.

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
    */
}

Buffer ukuran tetap

Anda dapat menggunakan fixed kata kunci untuk membuat buffer dengan array ukuran tetap dalam struktur data. Buffer ukuran tetap berguna saat Anda menulis metode yang beroperasi dengan sumber data dari bahasa atau platform lain. Buffer ukuran tetap dapat mengambil atribut atau pengubah apa pun yang diizinkan untuk anggota struct reguler. Satu-satunya batasan adalah bahwa jenis array harus bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, floatatau double.

private fixed char name[30];

Dalam kode aman, struct C# yang berisi array tidak berisi elemen array. Struct berisi referensi ke elemen sebagai gantinya. Anda dapat menyematkan array berukuran tetap dalam struct saat digunakan dalam blok kode yang tidak aman.

Ukuran berikut struct ini tidak bergantung pada jumlah elemen dalam array, karena pathName merupakan referensi:

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

Struktur dapat berisi array yang disematkan dalam kode yang tidak aman. Dalam contoh berikut, array fixedBuffer memiliki ukuran tetap. Anda menggunakan fixed pernyataan untuk mendapatkan penunjuk ke elemen pertama. Anda mengakses elemen array melalui pointer ini. Pernyataan fixed menyematkan fixedBuffer bidang instans ke lokasi tertentu dalam memori.

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]);
    }
}

Ukuran array elemen char 128 adalah 256 byte. Buffer karakter ukuran tetap selalu mengambil 2 byte per karakter, terlepas dari pengodeannya. Ukuran array ini sama bahkan saat buffer karakter digalang ke metode API atau struktur dengan CharSet = CharSet.Auto atau CharSet = CharSet.Ansi. Untuk informasi selengkapnya, lihat CharSet .

Contoh sebelumnya menunjukkan mengakses fixed bidang tanpa menyematkan. Array ukuran tetap umum lainnya adalah array bool. Elemen dalam array bool selalu berukuran 1 byte. Array bool tidak sesuai untuk membuat array bit atau buffer

Buffer ukuran tetap dikompilasi dengan System.Runtime.CompilerServices.UnsafeValueTypeAttribute, yang menginstruksikan runtime bahasa umum (CLR) bahwa jenis berisi array yang tidak dikelola yang berpotensi meluap. Memori yang dialokasikan menggunakan stackalloc juga secara otomatis memungkinkan fitur deteksi overrun buffer di CLR. Contoh sebelumnya menunjukkan bagaimana buffer ukuran tetap bisa ada di unsafe struct.

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

C# yang dihasilkan compiler untuk Buffer diatributkan sebagai berikut:

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;
}

Buffer ukuran tetap berbeda dari array reguler dengan cara berikut:

  • Hanya dapat digunakan dalam konteks unsafe.
  • Mungkin hanya bidang instans struct.
  • Bidang selalu vektor, atau array satu dimensi.
  • Deklarasi harus mencakup panjangnya, seperti fixed char id[8]. Anda tidak dapat menggunakan fixed char id[].

Cara menggunakan pointer untuk menyalin array byte

Contoh berikut menggunakan pointer untuk menyalin byte dari satu array ke array lainnya.

Contoh ini menggunakan kata kunci tidak aman, yang memungkinkan Anda menggunakan pointer dalam metode Copy. Pernyataan fixed (tetap) digunakan untuk mendeklarasikan pointer ke array sumber dan tujuan. Pernyataan fixedmenyematkan lokasi array sumber dan tujuan dalam memori sehingga tidak akan dipindahkan oleh pengumpulan sampah. Blok memori untuk array tidak disematkan saat blok fixed selesai. Karena metode Copy dalam contoh ini menggunakan kata kunci unsafe, metode harus dikompilasi dengan opsi compiler AllowUnsafeBlocks.

Contoh ini mengakses elemen kedua array menggunakan indeks daripada pointer kedua yang tidak dikelola. Deklarasi pSource dan pTarget pointer menyematkan array.

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
    */
}

Pointer fungsi

C# menyediakan jenis delegate untuk menentukan objek pointer fungsi yang aman. Memanggil delegasi melibatkan pembuatan instans jenis yang berasal dari System.Delegate dan melakukan panggilan metode virtual ke metode Invoke. Panggilan virtual ini menggunakan instruksi IL callvirt. Dalam jalur kode kritikal performa, menggunakan instruksi IL calli lebih efisien.

Anda dapat menentukan pointer fungsi menggunakan sintaks delegate*. Compiler akan memanggil fungsi menggunakan instruksi calli daripada membuat instans objek delegate dan memanggil Invoke. Kode berikut mendeklarasikan dua metode yang menggunakan delegate atau delegate* untuk menggabungkan dua objek dengan jenis yang sama. Metode pertama menggunakan jenis delegasi System.Func<T1,T2,TResult>. Metode kedua menggunakan deklarasi delegate* dengan parameter dan jenis pengembalian yang sama:

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);

Kode berikut menunjukkan bagaimana Anda akan mendeklarasikan fungsi lokal statis dan memanggil metode UnsafeCombine menggunakan pointer ke fungsi lokal tersebut:

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

Kode sebelumnya mengilustrasikan beberapa aturan pada fungsi yang diakses sebagai pointer fungsi:

  • pointer fungsi hanya dapat dideklarasikan dalam konteks unsafe.
  • Metode yang mengambil delegate* (atau mengembalikan delegate*) hanya dapat dipanggil dalam konteks unsafe.
  • Operator & untuk mendapatkan alamat fungsi hanya diperbolehkan pada fungsi static. (Aturan ini berlaku untuk fungsi anggota dan fungsi lokal).

Sintaks memiliki paralel dengan mendeklarasikan jenis delegate dan menggunakan pointer. Akhiran * pada delegate menunjukkan deklarasi adalah pointer fungsi. & saat menetapkan grup metode ke pointer fungsi menunjukkan operasi mengambil alamat metode.

Anda dapat menentukan konvensi panggilan untuk delegate* yang menggunakan kata kunci managed dan unmanaged. Selain itu, untuk pointer fungsi unmanaged, Anda dapat menentukan konvensi panggilan. Sebagai contoh, deklarasi berikut adalah sah: Deklarasi pertama menggunakan konvensi panggilan managed, yang merupakan default. Empat berikutnya menggunakan konvensi panggilan unmanaged. Masing-masing menentukan salah satu konvensi panggilan ECMA 335: Cdecl, Stdcall, Fastcall, atau Thiscall. Deklarasi terakhir menggunakan konvensi panggilan unmanaged, menginstruksikan CLR untuk memilih konvensi panggilan default untuk platform. CLR akan memilih konvensi panggilan pada waktu proses.

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);

Anda dapat mempelajari selengkapnya tentang penunjuk fungsi dalam spesifikasi fitur penunjuk Fungsi.

Spesifikasi bahasa C#

Untuk informasi selengkapnya, lihat Kode tidak Aman dalam Spesifikasi Bahasa C#.