Программирование: Работа с небезопасным кодом в C#

» Раздел: C#
Язык C# поддерживает указатели, однако несколько ограниченно. Ограниченность заключается в том, что применение указателей не поощряют, поскольку справедливо считается, что это может повлиять на надежность как кода, так и среды выполнения в целом.
Указатель - это переменная, содержащая в себе адрес памяти, в которой содержится переменная какого-либо типа. Другое ограничение C# - указатели могут быть объявлены только для удержания адреса переменной и массива. В отличие от ссылочных типов, типы указателей не отслеживаются механизмом сбора мусора по умолчанию. По той же самой причине указателям не разрешено указывать на ссылочный тип или даже на тип структуры, которая содержит в себе ссылочный тип. Можно сказать, что указатели могут указывать только на неуправляемые типы, которые включают в себя все базовые типы данных, типы перечисления, другие типы указателей и структуры, которые содержат только неуправляемые типы.
Чтобы работать с неуправляемыми конструкциями, код должен компилироваться с ключом /unsafe.
Пример:
Правый клик по проекту (не решению) -> Свойства -> Сборка -> Разрешить небезопасный код
Затем сохраняемся, чтобы применить изменения к проекту.

Объявление указателя

Основная форма объявления переменной указателя следующая:
type *variable_name;
Здесь звездочка * обозначает оператор разыменования. Например, строка
int *ptr;
объявляет переменную указателя ptr, которая может держать в себе адрес переменной типа int. Обратный оператор генерирования ссылки (reference operator, &) может использоваться для получения адреса переменной. Например, у нас есть переменная
int x = 100;
Оператор &x даст нам адрес переменной x, который мы можем присвоить переменной указателя.
int *ptr = &x;
Console.WriteLine((int)ptr); // Отобразится адрес памяти переменной.
Console.WriteLine(*ptr);     // Отобразится значение переменной.
Здесь мы рассмотрели обычное простое использование указателей, применяемое на языках C и C++. На языке C# все происходит похожим образом, однако есть некоторые отличия.

Небезопасный код (Unsafe Code)

Правилами языка C# определено, что операторы могут выполняться либо в безопасном, либо в небезопасном контексте. Операторы, процедуры и функции, помеченные как небезопасные, запускаются вне области управления памятью с помощью сборщика мусора. Помните, что любой код C#, использующий указатели, требует для выполнения небезопасного контекста.
Для того, чтобы пометить небезопасный контекст (т. е. код), используется ключевое слово unsafe. Мы можем использовать unsafe двумя различными способами. Ключевое слово unsafe может использоваться как модификатор метода, свойства, конструктора, и т. д. Например:
using System;
 
class MyClass
{
   public unsafe void Method()
   {
      int x = 10;
      int y = 20;
      int *ptr1 = &x;
      int *ptr2 = &y;
      Console.WriteLine((int)ptr1);
      Console.WriteLine((int)ptr2);
      Console.WriteLine(*ptr1);
      Console.WriteLine(*ptr2);
   }
}
 
class MyClient
{
   public static void Main()
   {
      MyClass mc = new MyClass();
      mc.Method();
   }
}
Кроме того, ключевое слово unsafe может также использоваться, чтобы пометить группу операторов как небезопасную:
using System;
 
class MyClass
{
   public void Method()
   {
      unsafe
      {
         int x = 10;
         int y = 20;
         int *ptr1 = &x;
         int *ptr2 = &y;
         Console.WriteLine((int)ptr1);
         Console.WriteLine((int)ptr2);
         Console.WriteLine(*ptr1);
         Console.WriteLine(*ptr2);
      }
   }
}
 
class MyClient
{
   public static void Main()
   {
      MyClass mc = new MyClass();
      mc.Method();
   }
}

Прикрепление объекта

Сборщик мусора C# может переместить объекты в памяти в соответствии с алгоритмом процесса уборки мусора. Язык C# предоставляет специальное ключевое слово fixed, чтобы указать сборщику мусора не перемещать объект. Это означает, что позиция переменной в памяти фиксируется, чтобы на нее мог ссылаться указатель. В C# это называется прикреплением.
Функционал оператора fixed обычно реализован путем генерации таблиц, описывающих для сборщика мусора, какие объекты в каких областях выполняемого кода должны оставаться фиксированными. Таким образом, пока процесс сбора мусора не встречает во время выполнения операторов fixed, потери ресурсов на них оказываются весьма незначительными. Однако, когда сборщик мусора встречает fixed, то фиксированные объекты могут привести к образованию фрагментации кучи. Т. е. в куче могут появиться неиспользуемые "дыры". Следовательно, объекты должны использовать fixed только тогда, когда это абсолютно необходимо, и только на самый малый, насколько это возможно, промежуток времени выполнения кода.

Указатели и методы

Указатели могут быть переданы в метод как аргументы. Методы также могут возвратить указатель. Пример:
using System;
 
class MyClass
{
   public unsafe void Method()
   {
      int x = 10;
      int y = 20;
      int *sum = swap(&x,&y);
      Console.WriteLine(*sum);
   }
   public unsafe int* swap(int *x, int *y)
   {
      int sum; 
      sum = *x + *y;
      return ∑
   }
}
 
class MyClient
{
   public static void Main()
   {
      MyClass mc = new MyClass();
      mc.Method();
   }
}

Указатели и преобразования типа

Типы указателей в C# не наследуются от объекта, и нет существующих преобразований между типами указателя и объектами. Это означает, что boxing и un-boxing не поддерживается указателями. Однако C# поддерживает преобразования между различными типами указателей, типами указателей и целочисленными типами.
C# поддерживает и неявные, и явные преобразования указателя в небезопасном контексте. Имеются неявные преобразования типа:
  • Из типа указателя на любой тип к типу указателя на тип void *.
  • Из типа null к любому другому типу указателя.
Оператор преобразования типа cast operator () необходим для любых явных преобразований типа. Имеются явные преобразования типа:
  • Из любого типа указателя на любой другой тип указателя.
  • Из типов sbyte, byte, short, ushort, int, uint, long, ulong к любому другому типу указателя.
  • Из любого типа указателя к типам sbyte, byte, short, ushort, int, uint, long, ulong.
Пример:
char c = 'R';
char *pc = &c;
void *pv = pc;        // неявное преобразование
int *pi = (int *) pv; // явное преобразование оператором кастинга

Арифметика указателей

В небезопасном контексте, операторы ++ и -- могут быть приложены к переменной указателя всех типов, за исключением типа void *. Таким образом, для каждого типа указателя T* следующие операторы будут неявно перегружены.
T* operator ++ (T *x);
T* operator -- (T *x);
Оператор ++ добавляет sizeof(T) к адресу, содержащемуся в переменной указателя, и оператор -- вычитает sizeof(T) из этого адреса для переменной указателя на тип T*.
In an un-safe context a constant can be added or subtracted from a pointer variable. Similarly a pointer variable can be subtracted from another pointer variable. But it is not possible to add two pointer variables in C#.
В небезопасном контексте операторы ==, !=, <, >, <=, >= могут также быть использоваться со значениями указателей на все типы. Умножение и деление переменной указателя на константу или другую переменную-указатель не поддерживается в C#.

Выделение памяти стека

В небезопасном контексте локальные определения переменных могут включать инициализатор выделения стека, который выделяет память из стека вызовов.
Оператор stackalloc T[E] требует T как необрабатываемый тип и E как выражение типа int. Вышеуказанная конструкция выделяет E * sizeof(T) байт из стека и генерирует указатель типа T* на новый выделенный блок. Если E отрицательно, то выбрасывается исключение System.OverFlowException. Если недостаточно памяти, то срабатывает исключение System.StackOverflowException.
Содержимое только что выделенной памяти является неопределенным. Нет способа неявного освобождения памяти, выделенной через stackalloc. Вместо этого весь блок памяти стека автоматически освобождается после возврата из функции.

Указатели и массивы

В C# может быть получен доступ к элементам массива при помощи использованием нотаций указателя.
using System;
 
class MyClass
{
   public unsafe void Method()
   {
      int []iArray = new int[10];
      for(int count=0; count < 10; count++)
      {
         iArray[count] = count*count;
      }
      fixed(int *ptr = iArray)
      Display(ptr);
      //Console.WriteLine(*(ptr+2));
      //Console.WriteLine((int)ptr); 
   }
   public unsafe void Display(int *pt)
   {
      for(int i=0; i < 14;i++)
      {
         Console.WriteLine(*(pt+i));
      }
   }
}
 
class MyClient
{
   public static void Main()
   {
      MyClass mc = new MyClass();
      mc.Method();
   }
}

Указатели и структуры

Все структуры C# имеют значимый тип. На структуру тоже может быть задан указатель только в том случае, если структура в качестве своих полей содержит только типы в виде значения. Пример:
using System;
 
struct MyStruct
{ 
   public int x;
   public int y;
   public void SetXY(int i, int j)
   {
      x = i;
      y = j;
   }
   public void ShowXY()
   {
      Console.WriteLine(x);
      Console.WriteLine(y);
   }
}
 
class MyClient
{
   public unsafe static void Main()
   {
      MyStruct ms = new MyStruct();
      MyStruct *ms1 = &ms;
      ms1->SetXY(10,20);
      ms1->ShowXY(); 
   }
}


Views: 3 717

» Лучшие комментарии


BrEd Pitt #1 - 3 years ago 0
Голосов: +0 / -0
Msey, это "записки по своему опыту" или из книги какой? Читается легко и плавно, словом, тутор классный.
Msey #2 - 3 years ago 3
Голосов: +3 / -0
BrEd Pitt: по-разному бывает. Часть напишу сам, часть возьму из одного источника, часть из мсдн.
Первые статьи вообще полностью сам писал, опираясь изредка на оф. документацию, чтобы лишний раз не сморозить дезинформирующую отсебятину.
Hanabishi #4 - 3 years ago (изм. ) 1
Голосов: +1 / -0
Забыл написать для чего это вообще надо. Для начала использования небезопасного кода нужно стараться избегать (помните что 95% дырок безопасности в истории это вина программиста, прозевавшего проверки в неуправляемом коде). Реально же практическое применение я вижу в двух вариантах:
  1. Если в проекте используется неуправляемая библиотека. И то очень специфически, если у вас структуры данных прибиты гвоздями. В большинстве же случаев можно обойтись маршалингом в безопасным контексте.
  2. Вы кулхацкер, который по каким-то причинам захотел использовать именно шарп. Тогда думаю вы в этой статье и не нуждаетесь. Хотя кстати я могу привести личные примеры такого применения )
Msey #5 - 3 years ago 0
Голосов: +0 / -0
Самый частый случай, который встречался мне на практике - это более шустрые операции с массивами и строками за счет отсутствия проверки их границ.
Hanabishi #7 - 3 years ago 2
Голосов: +2 / -0
Msey, плохой случай. Надо писать так, чтобы проверки границ в сейф коде убирались оптимизатором.
То есть допустим в случае
//a - некоторый массив
for(int i = 0; i < a.Length; i++)
проверки границ не будет. И разницы производительности с небезопасным вариантом тоже. Потому что оптимизатор умный.
Лезть ради этого в небезопасный контекст - крайне сомнительное занятие, нужно просто правильно писать в безопасном.
ScorpioT1000 #8 - 3 years ago 2
Голосов: +2 / -0
Или foreach, вообще внутри всё запаковано
uranus #9 - 3 years ago 0
Голосов: +0 / -0
Реально же практическое применение я вижу в двух вариантах:
Если не ошибаюсь, на хабре доказывали, что таким способом можно добиться большей скорости, чем у аналогичных функций стандартной библиотеки. Только не кидайте камнями, может, я не так понял просто.
Hanabishi #10 - 3 years ago 0
Голосов: +0 / -0
uranus, быстрее чем функции стандартной библиотеки можно добиться и не прибегая к небезопасному коду (написать собственную реализацию), тут о другом речь.
Ярг Восьмой #11 - 3 years ago 0
Голосов: +0 / -0
Мне кажется или это похоже на C++ в C#?
Не знаю, я пользуюсь mem alloc'ом в некоторых своих подопытных детишках по C++ только...
У меня свои шаманства с бубном, короче.
Hanabishi #12 - 3 years ago 0
Голосов: +0 / -0
KingMaximax, какой-то странный вопрос. Синтаксически C# происходит от C, что вроде очевидно. Соответсвенно и форма записи указателей взята оттуда.
Но несмотря на внешнюю схожесть, внутри C# работает абсолютно по-другому. Даже в небезопасном контексте с прямым доступом к памяти, код все равно выполняется на виртуальной машине.
Ярг Восьмой #13 - 3 years ago 0
Голосов: +0 / -0
Hanabishi, т.е. он пытался обойти виртуальную машину, если я правильно понял. Это как внутренняя система безопасности проверки жизненности цикла программы, что-то наподобие в этом роде. Она добавляет 100+ лишних килобайт. Но её вроде отключать можно, но лучше не надо, иначе есть риск поменять ОЗУ. Мой продвинутый друг в программировании занимался подобным, но потом перешёл на ассемблер из-за паранойности C++\C#, врать не буду, но это не совсем точные сведения. Или я снова туплю?
Hanabishi #14 - 3 years ago 0
Голосов: +0 / -0
KingMaximax, неправильно, нельзя "обойти виртуальную машину", код в любом случае на ней выполняется. Это главное отличие C# от нативных C\C++, но речь в статье вообще не об этом.
Есть понятие управляемого и неуправляемого кода. Так вот шарп по-умолчанию использует управляемый код, это когда все данные жестко контролируются средой, ты не управляешь памятью напрямую и не можешь критически накосячить, а ненужные данные удаляются автоматически (то есть утечки памяти практически невозможны).
Но существует и возможность работать с неуправляемым кодом, использовать прямой доуступ к памяти, о чем собственно статья. Это считается небезопасным (отсюда и ключевое слово unsafe), потому что работоспособность, стабильность и защищенность программы начинает на 100% зависеть от уровня криворукости программиста.
Ярг Восьмой #15 - 3 years ago 0
Голосов: +0 / -0
Hanabishi, теперь понял. Теперь мне придётся пересмотреть некоторые темы программирование, а то уже совсем что-то забываю. Уже начал путать эмулятор со строгим оптимизатором, грубо говоря.
И ещё, извиняюсь за свою неграмотность в области ЯП.
Doc #16 - 3 years ago 0
Голосов: +0 / -0
Все структуры C# имеют тип переменной
Это так переведено all C# structs are value types?
Msey #17 - 3 years ago 0
Голосов: +0 / -0
Doc, "Все структуры C# имеют значимый тип". Пофикшено.
alexprey #18 - 3 years ago 0
Голосов: +0 / -0
Msey:
Самый частый случай, который встречался мне на практике - это более шустрые операции с массивами и строками за счет отсутствия проверки их границ.
оптимизация работы с большими массивами данных, когда надо тоскать между функциями все эти данные и манипулировать небольшими участками массива без выделения в небольшой кусочек.
Но с недавних пор это все не актуально стало. В новом стандарте есть новые структуры данных, которые оборачивают такие небезопасные операции и непосредственное использование unsafe становится ненужным
Это сообщение удалено