Поделиться через


Небезопасный код, типы указателей и указатели функций

Большая часть написанного кода C# — это проверенный безопасный код. Проверенный безопасный код означает, что средства .NET могут убедиться, что код является безопасным. Как правило, безопасный код не обращается непосредственно к памяти с помощью указателей. Он также не выделяет необработанную память. Вместо этого он создает управляемые объекты.

Справочные документы на языке C#, выпущенные последней версией языка C#. Она также содержит начальную документацию по функциям в общедоступных предварительных версиях для предстоящего языкового выпуска.

Документация определяет любую функцию, впервые представленную в последних трех версиях языка или в текущих общедоступных предварительных версиях.

Подсказка

Чтобы узнать, когда функция впервые появилась в C#, ознакомьтесь со статьей по журналу версий языка C#.

C# поддерживает контекст unsafe, в котором можно написать непроверяемый код. В контексте unsafe код может использовать указатели, выделять и освобождать блоки памяти, а также вызывать методы с помощью указателей функций. Небезопасный код в C# не обязательно является опасным; это просто код, безопасность которого не может быть проверена.

Небезопасный код имеет следующие свойства:

  • Методы, типы и блоки кода можно определить как небезопасные.
  • В некоторых случаях небезопасный код может повысить производительность приложения, включив прямой доступ к памяти через указатели, чтобы избежать проверок границ массива.
  • Для вызова собственных функций, требующих указателей, используется небезопасный код.
  • Использование небезопасного кода представляет риски безопасности и стабильности.
  • Необходимо добавить параметр компилятора AllowUnsafeBlocks для компиляции кода, содержащего небезопасные блоки.

Сведения о рекомендациях по небезопасным кодам в C#см. в разделе "Небезопасные рекомендации по коду".

Типы указателей

В небезопасном контексте тип может быть типом указателя, а также типом значения или ссылочным типом. Объявление типа указателя принимает одну из следующих форм:

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

Тип, указанный перед * типом указателя, является ссылочной.

Типы указателей не наследуются от объекта, и преобразования между типами указателей и objectне существуют. Кроме того, упаковка и распаковывание не поддерживают указатели. Однако можно преобразовать между различными типами указателей и между типами указателей и целочисленными типами.

При объявлении нескольких указателей в одном объявлении напишите звездочку (*) вместе только с базовым типом. Он не используется в качестве префикса для каждого имени указателя. Например:

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

Сборщик мусора не отслеживает, указывает ли какой-либо тип указателей на объект. Если ссылка является объектом в управляемой куче (включая локальные переменные, захваченные лямбда-выражениями или анонимными делегатами), необходимо закрепить объект до тех пор, пока используется указатель.

Значение переменной указателя типа MyType* — адрес переменной типа MyType. Ниже приведены примеры объявлений типов указателя:

  • int* p: p — это указатель на целое число.
  • int** p: p — это указатель на целое число.
  • int*[] p: p — это одномерный массив указателей на целые числа.
  • char* p: p — это указатель на символ.
  • void* p: p — это указатель на неизвестный тип.

Оператор косвенного обращения указателя * можно использовать для доступа к содержимому в расположении, на которое указывает переменная указателя. Например, рассмотрим следующее объявление:

int* myVariable;

Выражение *myVariable обозначает переменную int, найденную по адресу, указанному в myVariable.

Существует несколько примеров указателей в статьях по инструкции fixed. В следующем примере используется ключевое слово unsafe и оператор fixed и показано, как увеличить внутренний указатель. Этот код можно вставить в основную функцию консольного приложения, чтобы запустить ее. Эти примеры необходимо скомпилировать с опцией компилятора 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 вызывает поведение, определенное реализацией.

Передача указателей между методами может привести к неопределенному поведению. Рассмотрим метод, который возвращает указатель на локальную переменную через in, outили параметр ref или в качестве результата функции. Если указатель был задан в фиксированном блоке, переменная, к которой она указывает, больше не будет исправлена.

В следующей таблице перечислены операторы и выражения, которые могут работать с указателями в небезопасном контексте.

Оператор/Утверждение Использование
* Выполняет непрямление указателя.
-> Обращается к члену структуры с помощью указателя.
[] Индексирует указатель.
& Получает адрес переменной.
++ и -- Приращения и уменьшения указателей.
+ и - Выполняет арифметику указателя.
==, !=, <, >, <=и >= Сравнивает указатели.
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: {number}");

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

Буферы фиксированного размера

Используйте ключевое fixed слово для создания буфера с массивом фиксированного размера в структуре данных. Буферы фиксированного размера полезны при написании методов, взаимодействующих с источниками данных из других языков или платформ. Буфер фиксированного размера может принимать любые атрибуты или модификаторы, разрешенные для обычных элементов структуры. Единственным ограничением является то, что тип массива должен быть bool, bytechar, char, int, long, sbyte, ushort, uint, ulong, float, doubleили .

private fixed char name[30];

В безопасном коде структуру C#, содержащую массив, не содержит элементов массива. Структура содержит ссылку на элементы. Массив фиксированного размера можно внедрить в структуру при использовании в блоке кода небезопасного кода.

Размер следующего struct не зависит от количества элементов в массиве, так как pathName является ссылкой:

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

Структура может содержать встроенный массив в небезопасном коде. В следующем примере массив 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]);
    }
}

Размер массива char из 128 элементов составляет 256 байт. Фиксированный размер буферов char всегда принимает 2 байта на символ независимо от кодировки. Этот размер массива остается одинаковым даже при маршалировании буферов char в методы или структуры API с CharSet = CharSet.Auto или CharSet = CharSet.Ansi. Дополнительные сведения см. в CharSet.

В предыдущем примере показано, как осуществить обращение к полям fixed без закрепления. Другим общим массивом фиксированного размера является массив логических значений . Элементы в массиве bool всегда равны 1 байтам. bool массивы не подходят для создания битовых массивов или буферов.

Буферы фиксированного размера компилируются с помощью System.Runtime.CompilerServices.UnsafeValueTypeAttribute, который указывает общей языковой среде выполнения (CLR), что тип содержит неуправляемый массив, способный к потенциальному переполнению. Память, выделенная с помощью stackalloc , также автоматически включает функции обнаружения переполнения буфера в среде CLR. В предыдущем примере показано, как буфер фиксированного размера может существовать в объекте unsafe struct.

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

С# код, сгенерированный компилятором для Buffer, имеет следующую атрибуцию:

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[].

Использование указателей для копирования массива байтов

В следующем примере указатели используются для копирования байтов из одного массива в другой.

В этом примере используется небезопасное ключевое слово, которое позволяет использовать указатели в методе Copy. Исправленная инструкция объявляет указатели на исходные и целевые массивы. Инструкция fixedзакрепляет расположение исходных и целевых массивов в памяти, чтобы сборка мусора не перемещала массивы. Блок fixed закрепляет блоки памяти для массивов в области блока. Copy Так как метод в этом примере использует ключевое unsafe слово, его необходимо скомпилировать с помощью параметра компилятора AllowUnsafeBlocks.

Этот пример обращается к элементам обоих массивов с помощью индексов, а не второго неуправляемого указателя. Объявление указателей pSource и pTarget закрепляет массивы.

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. В критически важных участках кода с точки зрения производительности использование инструкции IL calli более эффективно.

Указатель функции можно определить с помощью синтаксиса delegate* . Компилятор вызывает функцию с помощью calli инструкции, а не создания экземпляра delegate объекта и вызова Invoke. Следующий код объявляет два метода, которые используют delegate или delegate* для объединения двух объектов одного типа. Первый метод использует тип делегата 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 unsafe T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

В следующем коде показано, как объявить статическую локальную функцию и вызвать UnsafeCombine метод с помощью указателя на эту локальную функцию:

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

Приведенный выше код иллюстрирует несколько правил для функции, доступной через указатель функции.

  • В контексте unsafe можно объявлять только указатели функций.
  • Методы, которые принимают delegate* (или возвращают) в контекстеunsafe, можно вызывать delegate*только методы.
  • Оператор & для получения адреса функции разрешен только для static функций. Это правило применяется как к функциям-членам, так и к локальным функциям.

Синтаксис имеет параллели с объявлением типов delegate и использованием указателей. Суффикс * на delegate указывает, что объявление является указателем функции . & при назначении группы методов указателю функции указывает, что операция принимает адрес метода.

С помощью ключевых слов и unmanagedключевых слов можно указать соглашение delegate* о вызовахmanaged. Кроме того, для указателей функций unmanaged можно указать соглашение о вызовах. В следующих объявлениях показаны примеры каждого из них. В первой декларации используется соглашение о вызовах managed, которое является стандартным. Следующие четыре используют конвенцию вызова unmanaged. Каждый определяет одно из соглашений о вызовах ECMA 335: Cdecl, Stdcall, Fastcallили Thiscall. Последнее объявление использует соглашение о вызовах unmanaged, которое указывает среде CLR выбрать соглашение о вызовах по умолчанию для данной платформы. Общая среда выполнения (CLR) выбирает соглашение о вызовах во время выполнения.

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

Вы можете узнать больше об указателях функций в спецификации функции .

Спецификация языка C#

Дополнительные сведения см. в разделе небезопасный код главе спецификации языкаC#.

См. также