Bagikan melalui


Kode tidak aman, jenis penunjuk, dan penunjuk fungsi

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

Referensi bahasa C# mendanai versi bahasa C# yang terbaru dirilis. Ini juga berisi dokumentasi awal untuk fitur dalam pratinjau publik untuk rilis bahasa yang akan datang.

Dokumentasi mengidentifikasi fitur apa pun yang pertama kali diperkenalkan dalam tiga versi terakhir bahasa atau dalam pratinjau publik saat ini.

Tip

Untuk menemukan kapan fitur pertama kali diperkenalkan di C#, lihat artikel tentang riwayat versi bahasa C#.

C# mendukung konteks unsafe, di mana Anda dapat menulis kode yang tidak dapat diverifikasi. unsafe Dalam konteks, kode dapat menggunakan pointer, mengalokasikan dan membebaskan blok memori, dan memanggil metode dengan menggunakan penunjuk fungsi. Kode tidak aman di C# belum tentu berbahaya; itu hanya kode yang keamanannya tidak dapat diverifikasi.

Kode tidak aman memiliki properti berikut:

  • Anda dapat menentukan metode, jenis, dan blok kode sebagai tidak aman.
  • Dalam beberapa kasus, kode yang tidak aman dapat meningkatkan performa aplikasi dengan mengaktifkan akses memori langsung melalui penunjuk untuk menghindari pemeriksaan batas array.
  • Anda menggunakan kode tidak aman untuk memanggil fungsi asli yang memerlukan penunjuk.
  • Menggunakan kode yang tidak aman menimbulkan risiko keamanan dan stabilitas.
  • Anda harus menambahkan opsi pengkompilasi AllowUnsafeBlocks untuk mengkompilasi kode yang berisi blok yang tidak aman.

Untuk informasi tentang praktik terbaik untuk kode tidak aman di C#, lihat Praktik terbaik kode tidak aman.

Jenis penunjuk

Dalam konteks tidak aman, jenis bisa menjadi jenis penunjuk, selain jenis nilai atau jenis referensi. Deklarasi jenis pointer mengambil salah satu formulir berikut:

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

Jenis yang * Anda tentukan sebelum dalam jenis penunjuk adalah jenis referensi.

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

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

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

Pengumpul sampah tidak melacak apakah objek sedang diarahkan oleh jenis pointer apa pun. Jika referensi adalah objek dalam tumpukan terkelola (termasuk variabel lokal yang diambil oleh ekspresi lambda atau delegasi anonim), Anda harus menyematkan objek selama penunjuk digunakan.

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

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

Anda dapat menggunakan operator * tidak langsung penunjuk untuk mengakses konten di lokasi yang diarahkan oleh variabel penunjuk. Misalnya, pertimbangkan deklarasi berikut:

int* myVariable;

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

Ada beberapa contoh pointer dalam artikel tentang pernyataan fixed. Contoh berikut menggunakan kata kunci unsafe dan pernyataan fixed, serta menunjukkan cara meningkatkan pointer internal. Anda dapat menempelkan kode ini ke fungsi Utama aplikasi konsol untuk menjalankannya. Contoh-contoh ini harus dikompilasi dengan pengaturan 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 penunjuk jenis void*. Meskipun demikian, Anda dapat menggunakan cast untuk mengonversi void pointer ke jenis pointer lainnya, dan sebaliknya.

Pointer bisa null. Menerapkan operator penginderaan ke penunjuk null menyebabkan perilaku yang ditentukan oleh implementasi.

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

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

Operator/Pernyataan Pakai
* Melakukan pengindeksan pointer.
-> Mengakses anggota struct melalui pointer.
[] Mengindeks penunjuk.
& Mendapatkan alamat variabel.
++ dan -- Kenaikan dan penunjuk penurunan.
+ 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 yang terkait dengan pointer, lihat Operator terkait pointer.

Jenis penunjuk apa pun dapat dikonversi secara implisit ke jenis void*. Jenis penunjuk apa pun dapat ditetapkan nilainya null. Anda dapat secara eksplisit mengonversi jenis penunjuk apa pun ke jenis penunjuk lain menggunakan ekspresi transmisi. Anda juga dapat mengonversi jenis integral apa pun ke jenis penunjuk, atau jenis penunjuk apa pun ke jenis integral. Konversi ini memerlukan pemeran eksplisit.

Contoh berikut mengonversi int* menjadi byte*. Perhatikan bahwa pointer menunjuk ke byte terendah yang diatasi dari variabel. Ketika Anda secara berturut-turut meningkatkan hasilnya, 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: {number}");

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

Buffer ukuran tetap

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

private fixed char name[30];

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

Ukuran struct berikut tidak bergantung pada jumlah elemen dalam array, karena pathName adalah 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 pernyataan fixed untuk mendapatkan penunjuk ke elemen pertama. Anda mengakses elemen array melalui penunjuk ini. Pernyataan fixed menyematkan bidang instans fixedBuffer 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 char elemen 128 adalah 256 byte. Buffer berukuran tetap char selalu menggunakan 2 byte per karakter, terlepas dari pengodean. Ukuran array ini tetap sama bahkan ketika buffer karakter di-marshalkan ke metode API atau struktur dengan CharSet = CharSet.Auto atau CharSet = CharSet.Ansi. Untuk informasi selengkapnya, lihat CharSet.

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

Buffer ukuran tetap dikompilasi dengan System.Runtime.CompilerServices.UnsafeValueTypeAttribute, yang memberi instruksi kepada common language runtime (CLR) bahwa tipe tersebut mengandung array yang tidak dikelola yang berpotensi meluap. Memori yang dialokasikan dengan 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 oleh kompilator untuk Buffer ditetapkan 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 berukuran tetap berbeda dari array biasa dengan cara berikut:

  • Anda hanya dapat menggunakannya dalam unsafe konteks.
  • Mereka hanya dapat menjadi bidang instans struktur.
  • Mereka 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 larik 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 tetap mendeklarasikan penunjuk ke array sumber dan tujuan. Pernyataan fixedmenyematkan lokasi array sumber dan array tujuan dalam memori sehingga proses pengumpulan sampah tidak memindahkan array. Blok fixed menyematkan blok memori untuk array dalam cakupan blok. Copy Karena metode dalam contoh ini menggunakan unsafe kata kunci, Anda harus mengkompilasinya dengan menggunakan opsi pengkompilasi AllowUnsafeBlocks.

Contoh ini mengakses elemen kedua array dengan menggunakan indeks daripada pointer kedua yang tidak dikelola. Deklarasi penunjuk pSource dan pTarget memfiksasi posisi 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
    */
}

Penunjuk fungsi

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

Anda dapat menentukan penunjuk fungsi dengan menggunakan delegate* sintaks. Pengkompilasi memanggil fungsi dengan menggunakan calli instruksi daripada membuat delegate instans objek 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 yang sama dan jenis pengembalian:

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

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

Kode berikut menunjukkan cara Anda mendeklarasikan fungsi lokal statis dan memanggil metode dengan menggunakan penunjuk ke fungsi lokal tersebut UnsafeCombine :

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

Kode sebelumnya menggambarkan beberapa aturan pada fungsi yang diakses sebagai penunjuk fungsi:

  • Anda hanya dapat mendeklarasikan penunjuk fungsi dalam unsafe konteks.
  • Anda hanya dapat memanggil metode yang mengambil delegate* (atau mengembalikan delegate*) dalam unsafe konteks.
  • Operator & untuk mendapatkan alamat fungsi hanya diizinkan pada fungsi static. Aturan ini berlaku untuk fungsi anggota dan fungsi lokal.

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

Anda dapat menentukan konvensi panggilan untuk dengan delegate* menggunakan kata kunci managed dan unmanaged. Selain itu, Anda dapat menentukan konvensi panggilan untuk penunjuk fungsi unmanaged. Deklarasi berikut menunjukkan contoh masing-masing. 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 memilih konvensi pemanggilan pada waktu runtime.

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

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

Spesifikasi bahasa C#

Untuk informasi selengkapnya, lihat bab kode Tidak Aman dari spesifikasi bahasa C#.

Lihat juga