Небезопасный код, типы указателей и указатели функций
Большая часть написанного кода C# — это "проверенный безопасный код". Проверяемо безопасный код означает, что средства .NET могут убедиться, что код является безопасным. Как правило, безопасный код не обращается непосредственно к памяти с помощью указателей. Он также не выделяет необработанную память. Вместо этого он создает управляемые объекты.
C# поддерживает контекст unsafe
, в котором можно написать непроверяемый код. В контексте unsafe
код может использовать указатели, выделять и освобождать блоки памяти, а также вызывать методы с помощью указателей функций. Небезопасный код в C# не обязательно является опасным; это просто код, безопасность которого не может быть проверена.
Небезопасный код имеет следующие свойства:
- Методы, типы и блоки кода можно определить как небезопасные.
- В некоторых случаях небезопасный код может повысить производительность приложения, удалив проверки границ массива.
- Небезопасный код требуется при вызове собственных функций, требующих указателей.
- Использование небезопасного кода представляет риски безопасности и стабильности.
- Код, содержащий небезопасные блоки, необходимо скомпилировать с помощью параметра компилятораAllowUnsafeBlocks.
Типы указателей
В небезопасном контексте тип может быть типом указателя, а также типом значения или ссылочным типом. Объявление типа указателя принимает одну из следующих форм:
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: {0}", number);
/* Output:
The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
*/
}
Буферы фиксированного размера
Ключевое слово fixed
можно использовать для создания буфера с массивом фиксированного размера в структуре данных. Буферы фиксированного размера полезны при написании методов, взаимодействующих с источниками данных из других языков или платформ. Буфер фиксированного размера может принимать любые атрибуты или модификаторы, разрешенные для обычных элементов структуры. Единственным ограничением является то, что тип массива должен быть bool
, byte
char
, char
, short
, 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
закрепляет расположение исходных и целевых массивов в памяти, чтобы сборка мусора не перемещала массивы. Блоки памяти для массивов открепляются при завершении блока 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*
(или возвращаютdelegate*
) можно вызывать только в контекстеunsafe
. - Оператор
&
для получения адреса функции разрешен только дляstatic
функций. (Это правило применяется как к функциям-членам, так и к локальным функциям).
Синтаксис имеет параллели с объявлением типов delegate
и использованием указателей. Суффикс *
на delegate
указывает, что объявление является указателем функции .
&
при назначении группы методов указателю функции указывает, что операция принимает адрес метода.
Вы можете указать соглашение о вызове для delegate*
с помощью ключевых слов managed
и unmanaged
. Кроме того, для указателей функций 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#.