Небезопасный код может использоваться только при компиляции с параметром unsafe
Небезопасный код может использоваться только при компиляции с параметром unsafe
Использование указателей (небезопасный контекст) требуется в C# не часто, только в некоторых ситуациях. Например, в следующих случаях:
• Работа с существующими структурами на диске.
• Сценарии с Advanced COM или Platform Invoke (PInvoke), когда вовлекаются структуры с указателями на них.
• Критичный к производительности код.
Использование небезопасного контекста в других случаях не рекомендуется. Особенно не нужно использовать unsafe context, чтобы писать код C средствами языка C#.
Внимание: код, написанный в unsafe-контексте не может быть проверен на безопасность, так что этот код должен выполняться только тогда, когда к нему есть полное доверие. Другими словами, unsafe-код не может выполняться в не доверяемом окружении. Например, Вы не можете запустить unsafe-код напрямую из Интернет.
В руководстве рассмотрено 3 примера:
• Пример 1: использование указателей, чтобы копировать массив байт.
• Пример 2: показывает, как вызвать функцию ReadFile из Windows API.
• Пример 3: показывает, как напечатать Win32-версию исполняемого файла.
[Пример 1]
Здесь используются указатели для копирования массива байт из src в dst. Пример следует компилировать с опцией /unsafe.
Пример выведет следующее:
Обратите внимание на следующие моменты в примере 1:
• В коде используется ключевое слово unsafe, которое позволяет использовать указатели в методе Copy.
• Оператор fixed используется для декларирования указателей на массивы источника данных (откуда данные копируются) и получателя данных (куда копируются). Он привязывает места расположения в памяти для объектов src и dst, чтобы они не были случайно перемещены сборщиком мусора. Объекты отвязываются, когда завершается блок оператора fixed.
• Небезопасный код работает быстрее, потому что пропускаются проверки границ массива.
[Пример 2]
Здесь показан вызов функции ReadFile из Windows API (Platform SDK), которая требует использования небезопасного контекста, потому что требует в параметрах указатель на буфер для чтения. Демонстрируется утилита командной строки, которая открывает указанный в командной строке файл, и отображает в консоли его содержимое.
Массив байт, передаваемый в функцию Read, является управляемым типом (managed-тип. Что такое managed и unmanaged, см. в [2]). Managed означает, что сборщик мусора мог бы перенести место в памяти, где находится указанный массив. Оператор fixed позволяет Вам как получить указатель на память, где находится массив, так и привязать его к памяти, пометив для сборщика мусора, что нельзя манипулировать областью памяти массива во время действия оператора fixed.
По окончании блока fixed экземпляр массива помечается, что теперь его можно переносить. Эта возможность известна как декларативное прикрепления (declarative pinning). Хорошее свойство этой технологии в том, что она не затратная за исключением случая, когда сборщик мусора попытается обработать фиксированный блок, что довольно редкое явление.
[Пример 3]
Код читает и отображает номер версии Win32 исполняемого файла, который у примера тот же самый, что и номер версии сборки. Исполняемый файл примера printversion.exe. Используются функции VerQueryValue, GetFileVersionInfoSize и GetFileVersionInfo из Platform SDK. В примере применяются указатели, потому что это упрощает использование методов с сигнатурами, где есть указатели. Указатели обычная техника в Win32 API.
Пример вывода программы:
[Небезопасный код и язык C#. Ключевое слово unsafe]
Принцип работы языка C# отличается от языков C и C++ тем, что в нем отсутствуют указатели как тип данных. Вместо указателей C# предоставляет ссылки (references) и возможность создавать объекты, размещение в памяти которых (автоматически) обслуживается сборщиком мусора (garbage collector). Считается, что сборщик мусора вместе с другими технологическими возможностями делает язык C# безопаснее, чем C или C++. Т. е. в базовом языке C# просто невозможно иметь не инициализированную переменную, указатель «в никуда», или выражение, которое индексирует массив за его границы. Таким образом исключается возможность большой категории ошибок, которыми страдают программы на C и C++.
Примечание переводчика: за все надо платить. Теперь придется мириться с тормозами программ и с диким геморроем при использовании Windows API.
В unsafe-коде можно декларировать указатели и работать с ними, выполнять преобразования между указателями и целочисленными типами, получить адрес переменной и т. д. В некотором смысле написание unsafe-кода на C# во многом похожа на традиционное (native) программирование на языке C.
Unsafe-код фактически «безопасная» возможность с точки зрения разработчиков и пользователей, как бы странно ни звучало. Unsafe-код должен быть четко помечен модификатором unsafe, чтобы разработчики не могли использовать эту возможность случайно. Это гарантирует, что и система выполнения кода не будет выполнять небезопасный код в недоверяемом окружении.
Ключевое слово unsafe помечает небезопасный контекст, который требуется для использования указателей.
Вы можете использовать модификатор unsafe в декларировании типа или члена класса. Далее все текстовое расширение типа или члена класса считается небезопасным контекстом. Например, как в следующем методе, объявленном с модификатором unsafe:
Область unsafe-контекста находится от списка параметров до окончания метода, так что указатели можно использовать в списке параметров:
Вы также можете использовать unsafe-блок кода, в котором допускается небезопасный код. Например:
Чтобы можно было использовать unsafe-код, Вы должны указать для компилятора опцию /unsafe. Unsafe-код не проверяется подсистемой CLR (common language runtime). Простой пример:
Пример вывода программы:
[Оператор fixed]
Оператор fixed запрещает сборщику мусора (garbage collector, GC) физическое перемещение переменной в памяти, что происходит в следующей форме:
type не обслуживаемый тип (unmanaged type) или void.
ptr имя указателя.
expr выражение, которое неявно конвертируется в type*.
statement выполняемый оператор или блок кода (код за фигурными скобками <>).
Оператор fixed разрешено использовать только в unsafe-контексте (код, ограниченный действием ключевого слова unsafe).
Оператор fixed устанавливает указатель на адрес managed-переменной, и одновременно «прикалывает» переменную к её физическому размещению в памяти на время выполнения оператора fixed. Без fixed указатели на managed-переменые были бы мало полезны, поскольку сборщик мусора мог бы непредсказуемо в любой момент переместить переменную в памяти.
Примечание: фактически компилятор C# не позволит Вам установить значение указателя на managed-переменную, если это не делается под управлением оператора fixed.
Вы можете инициализировать указатель адресом массива или строки:
Вы можете инициализировать несколько указателей, пока они имеют один и тот же тип:
Чтобы инициализировать указатели разных типов, просто используйте вложение для операторов fixed:
Нельзя модифицировать указатели внутри операторов fixed.
После завершения действия statement (т. е. оператора или блока кода fixed) любые «привязанные» к физической памяти переменные «отвязываются», и эти переменные снова попадают под действие сборщика мусора. Таким образом, не указывайте на эти переменные вне действия оператора fixed.
В unsafe-режиме Вы можете выделять память в стеке, что не является областью действия сборщика мусора, и таким образом не нужно «привязывать» оператором fixed такие unmanaged-переменные. Для дополнительной информации см. stackalloc. Пример:
[stackalloc]
Ключевое слово stackalloc применяется для выделения блока памяти в стеке.
type unmanaged-тип.
ptr имя указателя.
expr интегральное выражение.
В результате будет в стеке (не в куче) будет выделен блок памяти достаточного размера, чтобы в нем поместилось expr элементов типа type. Адрес блока будет сохранен в указателе ptr. Эта память не попадает в область действия сборщика мусора, так что её не нужно привязывать к памяти оператором fixed. Время жизни выделенного таким способом блока памяти ограничено методом, где применен stackalloc.
Можно применять stackalloc только в инициализаторах локальной переменной.
Поскольку привлекается использование типов указателей, stackalloc требует unsafe-контекста.
Оператор stackalloc аналогичен функции _alloca в run-time библиотеке языка C.
Параметры компилятора C# для правил языковых функций
CheckForOverflowUnderflow
Параметр CheckForOverflowUnderflow указывает, будет ли использование целочисленного арифметического оператора, в результате выполнения которого получено значение, выходящее за установленный для определенного типа данных диапазон значений, приводить к возникновению исключения во время выполнения.
AllowUnsafeBlocks
Дополнительные сведения см. в разделе Небезопасный код и указатели.
DefineConstants
Параметр DefineConstants определяет символы в файлах исходного кода вашей программы.
LangVersion
Инструктирует компилятор принимать только синтаксис, включенный в заданную спецификацию языка C#.
Допустимы следующие значения.
Значение | Значение |
---|---|
preview | Компилятор допускает использование любого допустимого синтаксиса языка из последней предварительной версии. |
latest | Компилятор принимает синтаксис из последней выпущенной версии компилятора (включая дополнительный номер версии). |
latestMajor ( default ) | Компилятор принимает синтаксис из последней основной версии компилятора. |
10.0 | Компилятор принимает только синтаксис, включенный в спецификацию C# 10.0 или более ранних версий. |
9.0 | Компилятор принимает только синтаксис, включенный в спецификацию C# 9.0 или более ранних версий. |
8.0 | Компилятор принимает только синтаксис, включенный в спецификацию C# 8.0 или более ранней версии. |
7.3 | Компилятор принимает только синтаксис, включенный в спецификацию C# 7.3 или более ранней версии. |
7.2 | Компилятор принимает только синтаксис, включенный в спецификацию C# 7.2 или более ранней версии. |
7.1 | Компилятор принимает только синтаксис, включенный в спецификацию C# 7.1 или более ранней версии. |
7 | Компилятор принимает только синтаксис, включенный в спецификацию C# 7.0 или более ранней версии. |
6 | Компилятор принимает только синтаксис, включенный в спецификацию C# 6.0 или более ранней версии. |
5 | Компилятор принимает только синтаксис, включенный в спецификацию C# 5.0 или более ранней версии. |
4 | Компилятор принимает только синтаксис, включенный в спецификацию C# 4.0 или более ранней версии. |
3 | Компилятор принимает только синтаксис, включенный в спецификацию C# 3.0 или более ранней версии. |
ISO-2 (или 2 ) | Компилятор принимает только синтаксис, включенный в спецификацию ISO/IEC 23270:2006 C# (2.0). |
ISO-1 (или 1 ) | Компилятор принимает только синтаксис, включенный в спецификацию ISO/IEC 23270:2003 C# (1.0/1.2). |
Версия языка по умолчанию зависит от целевой платформы приложения, а также от установленной версии пакета SDK или Visual Studio. Такие правила определены в статье Управление версиями языка C#.
Параметр компилятора LangVersion не влияет на метаданные, на которые ссылается ваше приложение C#.
Так как каждая версия компилятора C# содержит расширения для спецификации языка, параметр LangVersion не позволяет использовать возможности, аналогичные возможностям более ранней версии компилятора.
Другие способы указания версии языка C# см. в статье Управление версиями языка C#.
Дополнительные сведения об установке этого параметра компилятора программным путем см. в разделе LanguageVersion.
Спецификация языка C#
Минимальная версия пакета SDK, необходимая для поддержки всех возможностей языка
В следующей таблице перечислены минимальные версии пакета SDK с компилятором C#, поддерживающим соответствующую версию языка:
Допускает значения NULL
Параметр Nullable позволяет указать контекст, допускающий значение NULL.
Анализ потока используется для определения допустимости значений NULL в переменных в исполняемом коде. Выводимая допустимость переменной значения NULL не зависит от объявленной в переменной допустимости значения NULL. Вызовы методов анализируются, даже если они условно опущены. Например, Debug.Assert в режиме выпуска.
Вызов методов, снабженных следующими атрибутами, также повлияет на анализ потока:
Глобальный контекст, допускающий значения NULL, не применяется для созданных файлов кода. Независимо от этого параметра, контекст, допускающий значение NULL, отключен для любого исходного файла, помеченного как созданный. Существует четыре способа пометки файла как созданного:
Ненадежный код, типы указателей и указатели функций
Небезопасный код имеет следующие свойства:
типы указателей
В небезопасном контексте тип может быть не только типом значения или ссылочным типом, но и типом указателя. Объявления типа указателя выполняется одним из следующих способов:
Тип, указанный до * в типе указателя, называется ссылочным типом. Ссылочным типом может быть только неуправляемый тип.
При объявлении нескольких указателей в одном объявлении знак ( * ) указывается только с базовым типом. Он не используется в качестве префикса для каждого имени указателя. Пример:
Указатель не может указывать на ссылку или на структуру, содержащую ссылки, поскольку ссылка на объект может быть подвергнута сбору мусора, даже если на нее указывает указатель. Сборщик мусора не отслеживает наличие указателей любых типов, указывающих на объекты.
Оператор косвенного обращения указателя * можно использовать для доступа к содержимому, на которое указывает переменная-указатель. В качестве примера рассмотрим следующее объявление:
Для указателя типа void* использовать оператор косвенного обращения нельзя. Однако можно использовать приведение для преобразования указателя типа void в любой другой тип и наоборот.
В следующей таблице перечислены операторы, которые можно использовать для указателей в небезопасном контексте.
Дополнительные сведения об операторах, связанных с указателем, см. в разделе Операторы, связанные с указателем.
Буферы фиксированного размера
В безопасном коде структура C#, содержащая массив, не содержит элементы массива. Вместо этого в ней присутствуют ссылки на элементы. Вы можете внедрить массив фиксированного размера в структуру, если он используется в блоке небезопасного кода.
Размер следующего объекта struct не зависит от количества элементов в массиве, поскольку pathName представляет собой ссылку:
В предыдущем примере демонстрировался доступ к полям fixed без закрепления в памяти, доступный в C#, начиная с версии 7.3.
Еще одним распространенным массивом фиксированного размера является массив bool. Элементы в массиве bool всегда имеют размер в 1 байт. Массивы bool не подходят для создания битовых массивов или буферов.
Созданный компилятором код C# для Buffer помечен с помощью атрибутов, как показано далее.
Буферы фиксированного размера отличаются от обычных массивов указанными ниже особенностями.
Практическое руководство. Использование указателей для копирования массива байтов
В следующем примере указатели используются для копирования байт из одного массива в другой.
В этом примере доступ к элементам обоих массивов выполняется с помощью индексов, а не второго неуправляемого указателя. Объявление указателей pSource и pTarget закрепляет массивы. Эта возможность доступна начиная с C# 7.3.
Указатели функций
В приведенном выше коде иллюстрируется ряд правил работы с функциями, доступ к которым осуществляется по указателю:
Синтаксис имеет сходства с объявлением типов delegate и использованием указателей. Суффикс * в служебном слове delegate указывает на то, что данное объявление является указателем функции. Знак & при назначении группы методов указателю функции указывает, что операция использует адрес метода.
Дополнительные сведения об указателях функций см. в предложении Указатель функции для C# 9.0.
Класс Marshal, использование PInvoke, небезопасный код (unsafe)
Использовать функции предоставляемые данным классом нужно в тех случаях, когда есть необходимость в применении неуправляемого кода или PInvoke. В данной статье будут рассмотрены наиболее важные (по моему мнению) и часто используемые функции.
3.1. Функции выделения и освобождения неуправляемой памяти
3.1.4. Пример использования функций:
3.2. Копирование управляемой памяти в неуправляемую и наоборот
Marshal.Copy предоставляет множество перегрузок для копирования массивов всех стандартных типов из неуправляемой памяти в управляемую и наоборот.
Пример обработки изображения ускоренным методом (относительно GetPixel/SetPixel):
3.3. Определение размера занимаемой объектом (или типом) неуправляемой памяти
Внимание! Если в примере заменить struct на class, то во время выполнения кода будет выдано исключение ArgumentException, в котором будет сказано что невозможно представить объект в виде неуправляемого, т.к. невозможного рассчитать его размер и смещение полей. Чтобы исправить эту ситуацию достаточно добавить атрибут LayoutKind.Sequential при объявлении класса (Об использовании атрибута StructLayout речь пойдет в разделе (4.3)). |
3.4. Определение смещения поля в типе данных
Функция Marshal.OffsetOf позволяет рассчитать смещение поля относительно начала размещения структуры в неуправляемой памяти. Это может понадобиться когда размер структуры меняется в зависимости от ОС и/или её разрядности. Функция позволяет автоматизировать расчет смещения, а не высчитывать его каждый раз самостоятельно.
3.5. Чтение/запись из/в неуправляемую память
3.6. Получение адреса объекта в памяти
3.6.1. Marshal.StructureToPtr и Marshal.PtrToStructure
3.6.2. GCHandle, краткое описание
3.7. Копирование строк из управляемой памяти в неуправляемую и наоборот
Множество алгоритмов и всевозможных решений в настоящее время существуют в виде DLL библиотек, что позволяет их использовать в приложениях путем вызова экспортируемых функций из них. В CLR для этого встроен специальный «сервис»: Platform Invocation Service который позволяет легко вызывать находящиеся в DLL функции, а также выполнять необходимые преобразования параметров для передачи их из управляемого кода в неуправляемый. Для взаимодействия с этим сервисом существуют специальные атрибуты: DllImport, MarshalAs и StructLayout. Они позволяют настроить сервис необходимым образом, чтобы при вызове неуправляемой функции все параметры передавались нужным образом.
Если немного изменить данный пример, раскомментировав параметр ExactSpelling и запустив приложение, выполнение программы прервётся на месте вызова функции gwt с ошибкой EntryPointNotFoundException: «Unable to find an entry point named ‘GetWindowText’ in DLL ‘USER32.DLL’.».
Закомментировав параметр CallingConvention, при вызове функции будет выдано исключение PInvokeStackImbalance, что говорит о неверном способе передачи параметров в неуправляемую функцию.
Запустив скомпилированное приложение с закомментированным параметром CharSet и потом с раскомментированным, будет видно как параметр CharSet влияет на вызов функций.
Атрибут StructLayout позволяет указать способ расположения полей объекта в памяти. Данный атрибут применим как для структур, так и для классов.
Данные атрибуты можно сочетать, тогда преобразование будет выполнятся до вызова метода и после.
Небезопасный код
P/Invoke
Обеспечивает возможность взаимодействий с функциями, экспортируемыми библиотеками DLL.
COM Interop
Язык C++/CLI
Поддерживает взаимодействия с кодом на C и C++ посредством использования гибридного языка программирования.
Эти механизмы имеют большое значение, поэтому очень важно понять, какие проблемы производительности влечет их применение, и как уменьшить их влияние.
Управляемый код обеспечивает безопасность хранения данных в памяти и дает гарантии их защищенности, устраняя некоторые сложные в диагностике проблемы и уязвимости, распространенные в неуправляемом коде, такие как повреждение данных в динамической памяти и переполнение буфера. Эти преимущества достигаются за счет запрета прямого доступа к памяти по указателям, использования строго типизированных ссылок, проверки границ массивов и допустимости приведения типов объектов.
К счастью, C# и среда выполнения CLR поддерживают небезопасный доступ к памяти с помощью указателей и допускают возможность приведения типов указателей. В числе других небезопасных особенностей можно назвать выделение памяти в стеке и встраивание массивов в структуры. Недостатком небезопасного кода является снижение безопасности, что может стать причиной повреждения данных в памяти и появления уязвимостей, поэтому будьте очень осторожны при разработке небезопасного кода.
Чтобы получить возможность использовать небезопасный код, необходимо сначала включить поддержку компиляции небезопасного кода в настройках проекта C#, в результате чего компилятору C# автоматически будет передаваться параметр /unsafe командной строки:
Затем следует выделить области, где допускается использование небезопасного кода или переменных. Такими областями могут быть целые классы или структуры, отдельные методы или фрагменты методов.
Закрепление объектов в памяти и дескрипторы сборщика мусора
Так как управляемые объекты, размещаемые в динамической памяти, могут перемещаться в процессе сборки мусора, их необходимо закреплять в памяти, прежде чем пытаться получить их адреса.
Указатель, полученный в области видимости инструкции fixed, нельзя использовать за ее пределами, потому что закрепленный объект открепляется после выхода из этой области. Ключевое слово fixed может применяться к массивам типов значений, к строкам и к полям управляемого класса, имеющим тип значения. Не забывайте указывать организацию памяти в структурах.
Всего существует четыре разновидности дескрипторов сборщика мусора, определяемые значениями перечисления GCHandleType: Weak, WeakTrackRessurection, Normal и Pinned. Типы Normal и Pinned предотвращают утилизацию объекта сборщиком мусора, даже если на него не осталось ни одной ссылки. Кроме того, тип Pinned дает возможность не только закрепить объект, но и получить его адрес в памяти. Типы Weak и WeakTrackResurrection не препятствуют утилизации объекта, но позволяют получить обычную (сильную) ссылку на него, если объект еще не был утилизирован. Дескрипторы этих типов используются типом WeakReference.
Закрепление объектов может вызывать фрагментацию динамической памяти в ходе сборки мусора. Фрагментация приводит к напрасному расходованию памяти и снижает эффективность алгоритма сборки мусора. Чтобы уменьшить фрагментацию памяти, не удерживайте объекты закрепленными дольше, чем это необходимо.
Управление жизненным циклом
Во многих случаях неуправляемый код продолжает удерживать неуправляемые ресурсы между вызовами функции и требует явного их освобождения. В этом случае, в дополнение к методу-финализатору, реализуйте в обертывающем управляемом классе интерфейс IDisposable. Это сохранит за клиентами возможность явно освобождать неуправляемые ресурсы, и обеспечит запасной вариант освобождения памяти с помощью финализатора, если вы забудете выполнить освобождение явно.
Выделение неуправляемой памяти
Управляемые объекты, занимающие в памяти более 85 000 байт (обычно массивы байтов и строки), помещаются в кучу больших объектов (Large Object Heap, LOH), которая обслуживается сборщиком мусора вместе с областью поколения 2 и требует значительных вычислительных затрат. Куча больших объектов также часто оказывается фрагментированной из-за того, что она никогда не сжимается, а свободное пространство между объектами используется, только когда это возможно.
Обе эти проблемы увеличивают расход памяти и вычислительной мощности процессора. Поэтому гораздо эффективнее использовать пулы памяти или выделять подобные буферы в неуправляемой памяти (например, вызовом Marshal.AllocHGlobal()). Если позднее потребуется получить доступ к неуправляемому буферу из управляемого кода, используйте прием на основе «потоков», копируя небольшие фрагменты из неуправляемого буфера в управляемую память и обрабатывая их по одному. Чтобы упростить работу, используйте System.UnmanagedMemoryStream и System.UnmanagedMemoryAccessor.
Использование пулов памяти
При интенсивном использовании буферов для взаимодействий с неуправляемым кодом, их можно выделять в динамической памяти сборщика мусора или в неуправляемой памяти. Первый подход недостаточно эффективен из-за высоких накладных расходов операции выделения памяти, когда буферы имеют маленький размер. Кроме того, управляемые буферы необходимо закреплять, что увеличивает фрагментацию памяти. Второй подход также имеет свои недостатки, потому что в большинстве случаев управляемый код работает с буферами, размещенными в управляемой памяти (byte[]), а не с указателями. Нельзя преобразовать указатель на управляемый массив, минуя копирование, а это отрицательно сказывается на производительности.
Мы предлагаем использовать решение, показанное на рисунке ниже, обеспечивающее доступ к буферу без копирования, как из управляемого, так и из неуправляемого кода, и не оказывающее отрицательного влияния на сборщик мусора. Идея состоит в том, чтобы выделить большие буферы в управляемой памяти (сегменты) в куче больших объектов. В этом случае отпадает необходимость закреплять сегменты, потому что они уже являются неперемещаемыми.
Простой диспетчер пула, в котором указатель на сегмент (фактически индекс) может перемещаться только вперед с каждым запросом, выделяет буферы различных размеров (вплоть до размера сегмента) и возвращает объект, обертывающий выделенные буферы. Когда указатель приблизится к концу сегмента, и очередная попытка выделить буфер в этом сегменте потерпит неудачу, из пула сегментов выделяется новый сегмент, и попытка выделить память повторяется.
Каждый сегмент имеет счетчик ссылок, увеличивающийся с каждой операцией выделения и уменьшающийся с каждой операцией освобождения буфера. Когда счетчик ссылок достигнет нуля, сегмент может быть приведен в исходное состояние (установкой указателя в его начало, с возможным заполнением памяти нулевыми байтами), и возвращен в пул сегментов.