Common Intermediate Language и системное программирование в Microsoft .NET

Язык CIL: обработка исключений. Синтаксис ILASM

Разбить на страницы
Показывать лекцию целиком

Существует два основных способа перехвата ошибок, возникающих в процессе работы программы:

  • Обработка кодов возврата.

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

    Этот способ хорошо работает, если глубина стека вызовов функций в программе относительно невелика. В противном случае код программы из-за постоянных проверок становится громоздким и трудночитаемым.

  • Обработка исключений.

    Этот способ заключается в том, что в случае возникновения ошибки генерируется так называемая исключительная ситуация (исключение), которая описывается некоторым объектом. Генерация исключения приводит к передаче управления на фрагмент кода программы, называемый обработчиком исключения. Преимуществом такого подхода является то, что перехват ошибок локализован в отдельной части программы, а не распределен по всему коду, как в случае с обработкой кодов возврата.

  • Основная трудность для понимания деталей реализации обработки исключений в CLI заключается в том, что обработка исключений частично закодирована в телах методов (в виде специальных инструкций), а частично - в заголовках методов. Скорее всего, такая смешанная схема была выбрана разработчиками CLI для обеспечения компактности сборок. Поэтому мы в данном разделе сначала рассмотрим ту часть информации об обработке исключений, которая расположена в заголовках методов, затем перейдем к инструкциям CIL и, в конце концов, свяжем все воедино, приведя семантику обработки исключений виртуальной системой выполнения.

    Предложения обработки исключений в заголовках методов

    Для дальнейшего изложения нам понадобится ввести понятия области в коде метода и координат области.

    Будем называть областью непрерывную последовательность инструкций в коде метода. При этом область будет определяться своими координатами, а именно парой чисел ( offset, length ), где offset - это смещение первой инструкции области относительно начала тела метода, а length - длина области. Как смещение, так и длину будем измерять в байтах.

    Заголовок каждого метода содержит специальный массив, элементы которого называются предложениями обработки исключений (exception handling clause).

    Каждое предложение обработки исключений представляет собой структуру, состоящую из нескольких полей. В этих полях записаны координаты двух или трех областей, а именно: в любом предложении присутствуют координаты защищенной области (protected block) и области обработчика (exception handler), а в некоторых предложениях дополнительно описана область фильтра (filter block).

    Если говорить в терминах языка C#, то защищенная область - это try-блок, а область обработчика - это либо catch-блок, либо finally-блок. Аналог для области фильтра в языке C# отсутствует, но зато он есть в Visual Basic .NET и в Visual C++ with Managed Extensions. Область фильтра содержит код, принимающий решение о том, может ли данное исключение быть обработано обработчиком. Естественно, такое представление о назначении областей в предложении обработки исключений несколько примитивно и понадобится нам лишь на начальном этапе.

    Давайте рассмотрим два возможных формата, в которых кодируется массив предложений обработки исключений. В каждом из двух форматов содержатся одни и те же поля, и различаются они только размерами полей. Первый формат называется коротким форматом (см. таблицу 3.43) и используется тогда, когда смещения областей не превышают 65535 байт, а длины областей не превышают 255 байт. Во втором, длинном формате (см. таблицу 3.44) допускаются любые смещения и длины. Строго говоря, не любые, а укладывающиеся в 32 бита. Но на практике этого более чем достаточно.

    Поля предложения обработки исключений в случае короткого формата
    Смещение Размер Поле Описание
    0 2 Flags Флаги
    2 2 TryOffset Координаты защищенной области
    4 1 TryLength
    5 2 HandlerOffset Координаты области обработчика
    7 1 HandlerLengh
    8 4 ClassToken Токен метаданных
    8 4 FilterOffset Смещение области фильтра

    Поля предложения обработки исключений в случае длинного формата
    Смещение Размер Поле Описание
    0 4 Flags Флаги
    4 4 TryOffset Координаты защищенной области
    8 4 TryLength
    12 4 HandlerOffset Координаты области обработчика
    16 4 HandlerLengh
    20 4 ClassToken Токен метаданных
    20 4 FilterOffset Смещение области фильтра

    Итак, координаты защищенной области задаются парой ( TryOffset, TryLength ), а координаты области обработчика - парой ( HandlerOffset, HandlerLength ). Для области фильтра указывается только ее смещение, потому что подразумевается, что она непосредственно предшествует области обработчика (длину области фильтра можно вычислить: она равна HandlerOffset - FilterOffset ).

    Обратите внимание, что смещения полей ClassToken и FilterOffset совпадают. Это означает, что фактически они представляют собой одно поле. Просто иногда оно интерпретируется как токен метаданных, а иногда - как смещение области фильтра.

    Поле Flags, возможные значения которого перечислены в таблице 3.45, задает тип обработчика.

    Допустимые значения поля Flags предложения обработки исключений
    Значение Описание
    0 Обработчик исключений с фильтрацией по типу
    1 Обработчик исключений с пользовательской фильтрацией
    2 Обработчик finally
    4 Обработчик fault

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

  • Обработчик с фильтрацией по типу.

    Получает управление, если тип исключения совместим по присваиванию с типом, указанным в поле ClassToken предложения обработки исключений.

  • Обработчик с пользовательской фильтрацией.

    Решение о том, получит или не получит управление обработчик, принимает код, содержащийся в области фильтра.

  • Обработчик finally.

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

  • Обработчик fault.

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

  • Первые два типа обработчиков мы будем относить к категории обработчиков с фильтрацией, а последние два - к категории обработчиков без фильтрации.

    Инструкции CIL для обработки исключений

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

    Инструкции для генерации исключений

    Инструкция throw (см. таблицу 3.46) генерирует исключение, включая тем самым механизм обработки исключений.

    Инструкция throw
    Код Инструкция Встроенный операнд Описание
    0x7A throw - Генерирует исключение

    Диаграмма стека для инструкции throw:

    ... , obj -> ...

    Объектная ссылка, которую инструкция throw потребляет со стека вычислений, должна указывать на объект в куче, описывающий исключение. Вообще говоря, в качестве такого объекта может выступать объект любого типа, в том числе упакованный тип-значение, но спецификация CLS требует, чтобы базовым классом для типа объекта-исключения являлся класс System.Exception.

    Если при выполнении инструкции throw на стеке вычислений лежит нулевая ссылка, то генерируется исключение System.NullReferenceException.

    Инструкция rethrow (см. таблицу 3.47) разрешена только внутри обработчика исключений с фильтрацией по типу и предназначена для генерации того же самого исключения, которое было поймано обработчиком.

    Инструкция rethrow
    Код Инструкция Встроенный операнд Описание
    0xFE 0x1A rethrow - Генерирует то же самое исключение, что было поймано обработчиком

    Диаграмма стека для инструкции rethrow:

    ... -> ...

    Инструкции передачи управления между блоками

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

    Инструкция leave
    Код Инструкция Встроенный операнд Описание
    0xDD leave int32 Выход из области
    0xDE leave.s int8 Выход из области (короткий период)

    Диаграмма стека для инструкции leave:

    ... ->

    Как видно из диаграммы, побочным эффектом при выполнении инструкции leave является очистка стека вычислений.

    Инструкция endfinally (см. таблицу 3.49) используется для выхода из областей обработчиков без фильтрации. У нее есть псевдоним - endfault.

    Инструкция endfinally
    Код Инструкция Встроенный операнд Описание
    0xDC endfinally (endfault) - Выход из обработчиков finally и fault

    Диаграмма стека для инструкции endfinally:

    ... -> ...

    Инструкция endfilter (см. таблицу 3.50) завершает область фильтра. Ее основная задача состоит в том, чтобы вернуть целое число (0 или 1). Значение 0 означает, что данное исключение не может быть обработано и нужно поискать другой обработчик. Значение 1 говорит о том, что нужно передать управление на обработчик.

    Инструкция endfilter
    Код Инструкция Встроенный операнд Описание
    0xFE 0x11 endfilter - Завершение области фильтра

    Диаграмма стека для инструкции endfilter:

    ... , value -> ...

    Правила размещения областей

    Итак, в общем случае, предложение обработки исключений определяет три области в коде метода: область защищенного блока, область фильтра и область обработчика (фильтр может отсутствовать). Эти области должны быть расположены в соответствии с определенными правилами:

  • Области, определяемые в предложении обработки исключений, не могут перекрываться.
  • Область фильтра всегда расположена непосредственно перед областью обработчика и завершается инструкцией endfilter.
  • Для любой пары предложений обработки исключений A и B должно быть справедливо следующее:
  • если защищенная область предложения A находится внутри защищенной области предложения B, то области фильтра и обработчика предложения A также должны располагаться внутри защищенной области предложения B;
  • если защищенная область предложения A не пересекается с защищенной областью предложения B, то области фильтров и обработчиков этих предложений тоже не должны пересекаться;
  • если защищенная область предложения A совпадает с защищенной областью предложения B, то области фильтров и обработчиков этих предложений не должны пересекаться.
  • Ограничения на передачу управления

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

  • Передача управления на обработчики осуществляется только через механизм обработки исключений.
  • Существует только два способа передать управление извне на защищенную область:
  • передача управления на первую инструкцию защищенной области;
  • использование инструкции leave из области обработчика с фильтрацией, связанной с данной защищенной областью (область обработки связана с защищенной областью, если их координаты указаны в одном и том же предложении обработки исключений).
  • Перед входом в защищенную область стек вычислений должен быть пустым.
  • Для выхода из защищенной области, из области фильтра или из области обработчика существуют только следующие возможности:
  • порождение исключения инструкцией throw ;
  • использование инструкции leave из защищенной области или области с фильтрацией;
  • использование инструкции endfilter из области фильтра;
  • использование инструкции endfinally из области без фильтрации;
  • использование инструкции rethrow из области с фильтрацией.
  • Семантика обработки исключений

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

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

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

  • оно описывает обработчик с фильтрацией;
  • адрес инструкции, породившей исключение, попадает в диапазон адресов защищенной области этого предложения;
  • исключение удовлетворяет фильтру обработчика.
  • Таким образом, на первом этапе finally и fault-блоки пропускаются. Кроме того, происходит последовательный вызов фильтров для блоков с пользовательской фильтрацией.

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

    Первый этап может завершиться либо нахождением подходящего предложения, либо обнаружением того факта, что подходящее предложение не существует на всей последовательности методов в стеке вызовов.

    В первом случае система переходит к следующему этапу, а во втором - выполнение программы аварийно завершается.

    На втором этапе система выполнения повторно просматривает массивы предложений, вызывая все обработчики без фильтрации. Она останавливается, когда доходит до предложения, найденного на первом этапе, после чего вызывает обработчик, описываемый этим предложением.

    Синтаксис ILASM

    В составе .NET Framework SDK поставляется ассемблер ILASM, который позволяет компилировать текстовые файлы, содержащие CIL-код и метаданные. В этом разделе мы проведем краткий обзор формата, в котором записываются эти файлы, и рассмотрим несколько примеров программ. Будем называть IL-форматом формат файлов, поддерживаемый ассемблером ILASM, а программы, записанные в IL-формате, - IL-программами.

    Основные элементы лексики

    Программы в IL-формате состоят из следующих лексических элементов:

  • идентификаторы;
  • метки;
  • константы;
  • зарезервированные слова;
  • специальные знаки;
  • комментарии.
  • Идентификаторы и метки чаще всего представляют последовательности символов, начинающиеся с латинской буквы (или с символов "_", "$", "@" и "?"), за которой следуют латинские буквы, цифры или символы "_", "$", "@" и "?". Кроме того, для идентификаторов и меток существует особая форма записи в апострофах: она позволяет включать в идентификаторы любые символы Unicode. Например:

    Label_1 $Name 'Идентификатор'

    Несколько идентификаторов могут быть объединены в один идентификатор с помощью точек. Например:

    System.Console.WriteLine

    Целочисленные константы записываются либо в десятичной системе счисления, либо в шестнадцатеричной (тогда перед ними ставится префикс "0x"). Например:

    128 -10 0xFF10B000

    В вещественных константах точка используется для разделения целой и дробной части, а символы "e" и "E" служат для указания экспоненциальной части. Кроме того, поддерживается особая форма записи float32 (целая_константа) и float64 (целая_константа), позволяющая представить целое число в виде числа с плавающей точкой. Например:

    5.5 -1.05e10 float32(128) float64(50)

    Строковые константы записываются в двойных кавычках и могут содержать Escape-последовательности "\t", "\n" и "\xxx", где восьмеричное число xxx задает код символа от 0 до 255. Для переноса строковой константы на другую строку программы используется символ "\". Кроме того, для строковых констант поддерживается операция конкатенации "+". Например:

    "Alpha Beta Gamma" "Hello, World\n" "Concat"+"enation"

    Комментарии в IL-программах записываются так же, как в языке C#:

  • Если в строке программы встречается "//", то остаток строки считается комментарием.
  • Текст, начинающийся с "/*", оканчивающийся на "*/" и не содержащий "*/", считается комментарием.
  • Синтаксис

    IL-программа представляет собой последовательность объявлений.

    В этом разделе мы рассмотрим синтаксис объявлений следующих элементов IL-программы:

  • сборка;
  • модуль;
  • тип;
  • поле;
  • метод.
  • Сборки и модули

    Каждый IL-файл для ассемблера ILASM представляет собой отдельный модуль сборки. Мы не будем касаться вопросов компиляции сборки, состоящей из нескольких модулей, поэтому приведем образец заголовка IL-файла для одномодульной сборки:

    .assembly MyProgram { }
    .module MyProgram.exe
    .assembly extern mscorlib { }

    В заголовке используются три директивы: директива ".assembly" позволяет задать имя нашей сборки. Директива ".module" определяет имя модуля и совпадает с именем исполняемого файла, в который будет записана откомпилированная сборка. Директива ".assembly extern" указывает, что мы будем импортировать сборку mscorlib, в которой находится основная часть библиотеки классов .NET. В фигурных скобках после имени сборки могут перечисляться свойства сборки, но в простейшем случае их можно оставить пустыми.

    Типы

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

  • последовательность атрибутов типа;
  • имя типа;
  • базовый тип;
  • список реализуемых интерфейсов.
  • Последовательность атрибутов следует непосредственно после ключевого слова ".class". В таблице 3.51 приведен набор наиболее часто используемых атрибутов.

    Атрибуты типов
    Атрибут Описание
    abstract Тип является абстрактным классом
    interface Тип является интерфейсом
    private Тип не экспортируется из сборки
    public Тип экспортируется из сборки
    sealed Тип не может являться базовым классом для другого типа (от него нельзя наследовать)
    serializable Экземпляры типа могут быть сериализованы

    После атрибутов следует идентификатор, задающий имя объявляемого типа.

    Если объявляемый тип наследует от какого-нибудь другого типа (базового класса), отличного от System.Object, то необходимо указать имя базового класса после ключевого слова "extends". При этом, если в качестве базового класса выбран System.ValueType, то объявляемый тип будет типом-значением.

    Если объявляемый тип реализует методы каких-либо интерфейсов, то должен быть приведен список этих интерфейсов после ключевого слова "implements".

    Рассмотрим несколько примеров:

  • Объявление экспортируемого абстрактного класса, реализующего интерфейс IEnumerable.
    .class public abstract MyAbstractClass
    extends [mscorlib]System.Object
    implements [mscorlib]System.Collections.IEnumerable
    { }
  • Объявление неэкспортируемого интерфейса.
    .class private interface MyInterface { }
  • Объявление экспортируемого типа-значения.
    .class public sealed MyValueType
      extends [mscorlib]System.ValueType
      { }
  • Обратите внимание, что перед именами библиотечных классов и интерфейсов в квадратных скобках указывается имя сборки, в которой они содержатся.

    Поля

    Поля объявляются внутри объявлений типов. Объявление поля осуществляется с помощью директивы ".field" и состоит из трех частей:

  • последовательность атрибутов поля;
  • тип;
  • имя поля.
  • Последовательность атрибутов следует непосредственно после ключевого слова ".field". В таблице 3.52 приведен набор наиболее часто используемых атрибутов.

    Атрибуты полей
    Атрибут Описание
    assembly Поле видимо внутри сборки
    family Поле видимо для наследников типа
    public Поле видимо для всех
    private Поле видимо только внутри типа
    static Поле является статическим

    После атрибутов следует тип поля и идентификатор, задающий имя поля.

    Рассмотрим несколько примеров:

  • Объявление поля x типа массив.
    .field private int32[] x
  • Объявление поля table типа Hashtable.
    .field public class
    [mscorlib]System.Collections.Hashtable table
  • Методы

    Методы объявляются внутри объявлений типов. Объявление метода осуществляется с помощью директивы ".method" и состоит из пяти частей:

  • последовательность атрибутов метода;
  • тип возвращаемого значения;
  • имя метода;
  • список параметров метода;
  • тело метода.
  • Последовательность атрибутов следует непосредственно после ключевого слова ".method". В таблице 3.53 приведен набор наиболее часто используемых атрибутов.

    После атрибутов следует тип возвращаемого значения и идентификатор, задающий имя метода. Если метод не возвращает значения, в качестве типа возвращаемого значения указывается void. Конструкторы всегда имеют имя ".ctor", а статические конструкторы - ".cctor".

    Атрибуты методов
    Атрибут Описание
    assembly Метод видим внутри сборки
    family Метод видим для наследников типа
    public Метод видим для всех
    private Метод видим только внутри типа
    abstract Метод является абстрактным
    virtual Метод является виртуальным
    final Метод не может переопределяться в наследниках
    static Метод является статическим

    Список параметров метода следует за именем метода и заключается в круглые скобки. Для каждого параметра указывается его тип и имя.

    Прежде чем перейти к рассмотрению синтаксиса объявлений тел методов, приведем несколько примеров заголовков методов:

  • Объявление конструктора с двумя параметрами.
    .method public void .ctor
      (int32 x, class [mscorlib]System.String s)
  • Виртуальный метод с управляемым указателем в качестве параметра.
    .method private virtual int32 myMethod(int32 pX)
  • Статический метод, возвращающий массив:
    .method public static int32[] MyStaticMethod()
  • Тело метода заключается в фигурные скобки и содержит инструкции языка CIL. Каждая инструкция записывается на новой строке программы. Если нужно, то инструкции может предшествовать метка, отделяемая от инструкции двоеточием. Например:

    Hello:	ldstr "Hello, World!"
      	call void [mscorlib]System.Console.WriteLine(string)

    Кроме инструкций CIL тело метода может содержать директивы тела метода. Они перечислены в таблице 3.54.

    Директивы тела метода
    Директива Описание
    .entrypoint Показывает, что данный метод является точкой входа в сборку (метод должен быть статическим, возвращать int32 или ничего не возвращать, иметь в качестве параметров массив строк или вообще не иметь параметров)
    .locals (объявления) Определяет набор локальных переменных метода. Локальные переменные объявляются аналогично параметрам метода
    .maxstack число Задает глубину стека вычислений

    Пример программы

    Давайте рассмотрим пример программы, написанной прямо на CIL с использованием синтаксиса ILASM. Мы не станем приводить весь текст программы сразу, а будем рассматривать ее постепенно, по частям.

    Естественно, наша программа будет начинаться с заголовка, объявляющего имена сборки и модуля и импортирующего стандартную библиотеку:

    .assembly Sample1 { }
    .module sample1.exe
    .assembly extern mscorlib { }

    Далее объявим тип-значение Point, реализующий понятие точки на плоскости. Он будет содержать два поля x и y типа float64, а также конструктор и статический метод, вычисляющий расстояние между двумя точками:

    .class public sealed Point extends [mscorlib]System.ValueType
    {
        .field public float64 x
        .field public float64 y
        .method public void .ctor (float64 x, float64 y)
        {
          .maxstack		3
          ldarg.0
          dup
          ldarg.1
          stfld  		float64 Point::x
          ldarg.2
          stfld  		float64 Point::y
          ret
        }
        .method public static float64 Distance
          (valuetype Point a, valuetype Point b)
        {
          .maxstack 	3
          ldarga  		a
          ldfld  		float64 Point::x
          ldarga  		b
          ldfld  		float64 Point::x
          sub
          dup
          mul
          ldarga  		a
          ldfld  		float64 Point::y
          ldarga  		b
          ldfld  		float64 Point::y
          sub
          dup
          mul
          add
          call  		float64 [mscorlib]System.Math::Sqrt(float64)
          ret
        }
    }

    А теперь объявим вспомогательный класс SampleClass, который будет содержать точку входа в нашу сборку. Метод Demo (точка входа) будет вычислять расстояние между точками (0.0,0.0) и (1.0,1.0) и выводить результат на экран:

    .class public SampleClass
    {
     .method public static void Demo()
     {
      .entrypoint
      .maxstack 3
      ldc.r8 	  0.0
      ldc.r8 	  0.0
      newobj 	  void Point::.ctor(float64,float64)
      ldc.r8 	  1.0
      ldc.r8 	  1.0
      newobj 	  void Point::.ctor(float64,float64)
      call 		  float64 Point::Distance(valuetype Point, valuetype Point)
      call 	  void [mscorlib]System.Console::WriteLine (float64)
      ret
       }
    }

    Откомпилируем нашу программу, которая записана в текстовом файле sample1.il. Подразумевается, что мы работаем в Windows и у нас переменная окружения path настроена таким образом, что программы ILASM и PEVERIFY можно вызывать без указания путей. Наберем в консоли команду:

    ilasm sample1.il

    Мы получим следующее сообщение от компилятора ILASM:

    Microsoft (R) .NET Framework IL Assembler. Version 1.1.4322.573
    Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
    Assembling 'sample1.il', no listing file, to EXE --> 'sample1.EXE'
    Source file is ANSI
    
    Assembled method Point::.ctor
    Assembled method Point::Distance
    Assembled method SampleClass::Demo
    Creating PE file
    Emitting members:
    Global
    Class 1 Fields: 2;   Methods: 2;
    Class 2 Methods: 1;
    Resolving member refs: 9 -> 9 defs, 0 refs
    Writing PE file
    Operation completed successfully

    Итак, наша программа успешно откомпилировалась и на диске появилась сборка sample1.exe. Попробуем провести ее верификацию:

    peverify sample1.exe

    В ответ верификатор выведет на экран:

    Microsoft (R) .NET Framework PE Verifier Version 1.1.4322.573
    Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
    
    All Classes and Methods in sample1.exe Verified

    Таким образом, сборка успешно прошла верификацию и мы можем рискнуть ее запустить:

    sample1.exe

    В результате на экран выводится расстояние между точками (0.0,0.0) и (1.0,1.0):

    1, 4142135623731
    Страницы:

    Существует два основных способа перехвата ошибок, возникающих в процессе работы программы:

  • Обработка кодов возврата.

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

    Этот способ хорошо работает, если глубина стека вызовов функций в программе относительно невелика. В противном случае код программы из-за постоянных проверок становится громоздким и трудночитаемым.

  • Обработка исключений.

    Этот способ заключается в том, что в случае возникновения ошибки генерируется так называемая исключительная ситуация (исключение), которая описывается некоторым объектом. Генерация исключения приводит к передаче управления на фрагмент кода программы, называемый обработчиком исключения. Преимуществом такого подхода является то, что перехват ошибок локализован в отдельной части программы, а не распределен по всему коду, как в случае с обработкой кодов возврата.

  • Основная трудность для понимания деталей реализации обработки исключений в CLI заключается в том, что обработка исключений частично закодирована в телах методов (в виде специальных инструкций), а частично - в заголовках методов. Скорее всего, такая смешанная схема была выбрана разработчиками CLI для обеспечения компактности сборок. Поэтому мы в данном разделе сначала рассмотрим ту часть информации об обработке исключений, которая расположена в заголовках методов, затем перейдем к инструкциям CIL и, в конце концов, свяжем все воедино, приведя семантику обработки исключений виртуальной системой выполнения.

    Предложения обработки исключений в заголовках методов

    Для дальнейшего изложения нам понадобится ввести понятия области в коде метода и координат области.

    Будем называть областью непрерывную последовательность инструкций в коде метода. При этом область будет определяться своими координатами, а именно парой чисел ( offset, length ), где offset - это смещение первой инструкции области относительно начала тела метода, а length - длина области. Как смещение, так и длину будем измерять в байтах.

    Заголовок каждого метода содержит специальный массив, элементы которого называются предложениями обработки исключений (exception handling clause).

    Каждое предложение обработки исключений представляет собой структуру, состоящую из нескольких полей. В этих полях записаны координаты двух или трех областей, а именно: в любом предложении присутствуют координаты защищенной области (protected block) и области обработчика (exception handler), а в некоторых предложениях дополнительно описана область фильтра (filter block).

    Если говорить в терминах языка C#, то защищенная область - это try-блок, а область обработчика - это либо catch-блок, либо finally-блок. Аналог для области фильтра в языке C# отсутствует, но зато он есть в Visual Basic .NET и в Visual C++ with Managed Extensions. Область фильтра содержит код, принимающий решение о том, может ли данное исключение быть обработано обработчиком. Естественно, такое представление о назначении областей в предложении обработки исключений несколько примитивно и понадобится нам лишь на начальном этапе.

    Давайте рассмотрим два возможных формата, в которых кодируется массив предложений обработки исключений. В каждом из двух форматов содержатся одни и те же поля, и различаются они только размерами полей. Первый формат называется коротким форматом (см. таблицу 3.43) и используется тогда, когда смещения областей не превышают 65535 байт, а длины областей не превышают 255 байт. Во втором, длинном формате (см. таблицу 3.44) допускаются любые смещения и длины. Строго говоря, не любые, а укладывающиеся в 32 бита. Но на практике этого более чем достаточно.

    Поля предложения обработки исключений в случае короткого формата
    Смещение Размер Поле Описание
    0 2 Flags Флаги
    2 2 TryOffset Координаты защищенной области
    4 1 TryLength
    5 2 HandlerOffset Координаты области обработчика
    7 1 HandlerLengh
    8 4 ClassToken Токен метаданных
    8 4 FilterOffset Смещение области фильтра

    Поля предложения обработки исключений в случае длинного формата
    Смещение Размер Поле Описание
    0 4 Flags Флаги
    4 4 TryOffset Координаты защищенной области
    8 4 TryLength
    12 4 HandlerOffset Координаты области обработчика
    16 4 HandlerLengh
    20 4 ClassToken Токен метаданных
    20 4 FilterOffset Смещение области фильтра

    Итак, координаты защищенной области задаются парой ( TryOffset, TryLength ), а координаты области обработчика - парой ( HandlerOffset, HandlerLength ). Для области фильтра указывается только ее смещение, потому что подразумевается, что она непосредственно предшествует области обработчика (длину области фильтра можно вычислить: она равна HandlerOffset - FilterOffset ).

    Обратите внимание, что смещения полей ClassToken и FilterOffset совпадают. Это означает, что фактически они представляют собой одно поле. Просто иногда оно интерпретируется как токен метаданных, а иногда - как смещение области фильтра.

    Поле Flags, возможные значения которого перечислены в таблице 3.45, задает тип обработчика.

    Допустимые значения поля Flags предложения обработки исключений
    Значение Описание
    0 Обработчик исключений с фильтрацией по типу
    1 Обработчик исключений с пользовательской фильтрацией
    2 Обработчик finally
    4 Обработчик fault

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

  • Обработчик с фильтрацией по типу.

    Получает управление, если тип исключения совместим по присваиванию с типом, указанным в поле ClassToken предложения обработки исключений.

  • Обработчик с пользовательской фильтрацией.

    Решение о том, получит или не получит управление обработчик, принимает код, содержащийся в области фильтра.

  • Обработчик finally.

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

  • Обработчик fault.

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

  • Первые два типа обработчиков мы будем относить к категории обработчиков с фильтрацией, а последние два - к категории обработчиков без фильтрации.

    Инструкции CIL для обработки исключений

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

    Инструкции для генерации исключений

    Инструкция throw (см. таблицу 3.46) генерирует исключение, включая тем самым механизм обработки исключений.

    Инструкция throw
    Код Инструкция Встроенный операнд Описание
    0x7A throw - Генерирует исключение

    Диаграмма стека для инструкции throw:

    ... , obj -> ...

    Объектная ссылка, которую инструкция throw потребляет со стека вычислений, должна указывать на объект в куче, описывающий исключение. Вообще говоря, в качестве такого объекта может выступать объект любого типа, в том числе упакованный тип-значение, но спецификация CLS требует, чтобы базовым классом для типа объекта-исключения являлся класс System.Exception.

    Если при выполнении инструкции throw на стеке вычислений лежит нулевая ссылка, то генерируется исключение System.NullReferenceException.

    Инструкция rethrow (см. таблицу 3.47) разрешена только внутри обработчика исключений с фильтрацией по типу и предназначена для генерации того же самого исключения, которое было поймано обработчиком.

    Инструкция rethrow
    Код Инструкция Встроенный операнд Описание
    0xFE 0x1A rethrow - Генерирует то же самое исключение, что было поймано обработчиком

    Диаграмма стека для инструкции rethrow:

    ... -> ...

    Инструкции передачи управления между блоками

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

    Инструкция leave
    Код Инструкция Встроенный операнд Описание
    0xDD leave int32 Выход из области
    0xDE leave.s int8 Выход из области (короткий период)

    Диаграмма стека для инструкции leave:

    ... ->

    Как видно из диаграммы, побочным эффектом при выполнении инструкции leave является очистка стека вычислений.

    Инструкция endfinally (см. таблицу 3.49) используется для выхода из областей обработчиков без фильтрации. У нее есть псевдоним - endfault.

    Инструкция endfinally
    Код Инструкция Встроенный операнд Описание
    0xDC endfinally (endfault) - Выход из обработчиков finally и fault

    Диаграмма стека для инструкции endfinally:

    ... -> ...

    Инструкция endfilter (см. таблицу 3.50) завершает область фильтра. Ее основная задача состоит в том, чтобы вернуть целое число (0 или 1). Значение 0 означает, что данное исключение не может быть обработано и нужно поискать другой обработчик. Значение 1 говорит о том, что нужно передать управление на обработчик.

    Инструкция endfilter
    Код Инструкция Встроенный операнд Описание
    0xFE 0x11 endfilter - Завершение области фильтра

    Диаграмма стека для инструкции endfilter:

    ... , value -> ...

    Правила размещения областей

    Итак, в общем случае, предложение обработки исключений определяет три области в коде метода: область защищенного блока, область фильтра и область обработчика (фильтр может отсутствовать). Эти области должны быть расположены в соответствии с определенными правилами:

  • Области, определяемые в предложении обработки исключений, не могут перекрываться.
  • Область фильтра всегда расположена непосредственно перед областью обработчика и завершается инструкцией endfilter.
  • Для любой пары предложений обработки исключений A и B должно быть справедливо следующее:
  • если защищенная область предложения A находится внутри защищенной области предложения B, то области фильтра и обработчика предложения A также должны располагаться внутри защищенной области предложения B;
  • если защищенная область предложения A не пересекается с защищенной областью предложения B, то области фильтров и обработчиков этих предложений тоже не должны пересекаться;
  • если защищенная область предложения A совпадает с защищенной областью предложения B, то области фильтров и обработчиков этих предложений не должны пересекаться.
  • Ограничения на передачу управления

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

  • Передача управления на обработчики осуществляется только через механизм обработки исключений.
  • Существует только два способа передать управление извне на защищенную область:
  • передача управления на первую инструкцию защищенной области;
  • использование инструкции leave из области обработчика с фильтрацией, связанной с данной защищенной областью (область обработки связана с защищенной областью, если их координаты указаны в одном и том же предложении обработки исключений).
  • Перед входом в защищенную область стек вычислений должен быть пустым.
  • Для выхода из защищенной области, из области фильтра или из области обработчика существуют только следующие возможности:
  • порождение исключения инструкцией throw ;
  • использование инструкции leave из защищенной области или области с фильтрацией;
  • использование инструкции endfilter из области фильтра;
  • использование инструкции endfinally из области без фильтрации;
  • использование инструкции rethrow из области с фильтрацией.
  • Семантика обработки исключений

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

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

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

  • оно описывает обработчик с фильтрацией;
  • адрес инструкции, породившей исключение, попадает в диапазон адресов защищенной области этого предложения;
  • исключение удовлетворяет фильтру обработчика.
  • Таким образом, на первом этапе finally и fault-блоки пропускаются. Кроме того, происходит последовательный вызов фильтров для блоков с пользовательской фильтрацией.

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

    Первый этап может завершиться либо нахождением подходящего предложения, либо обнаружением того факта, что подходящее предложение не существует на всей последовательности методов в стеке вызовов.

    В первом случае система переходит к следующему этапу, а во втором - выполнение программы аварийно завершается.

    На втором этапе система выполнения повторно просматривает массивы предложений, вызывая все обработчики без фильтрации. Она останавливается, когда доходит до предложения, найденного на первом этапе, после чего вызывает обработчик, описываемый этим предложением.

    Синтаксис ILASM

    В составе .NET Framework SDK поставляется ассемблер ILASM, который позволяет компилировать текстовые файлы, содержащие CIL-код и метаданные. В этом разделе мы проведем краткий обзор формата, в котором записываются эти файлы, и рассмотрим несколько примеров программ. Будем называть IL-форматом формат файлов, поддерживаемый ассемблером ILASM, а программы, записанные в IL-формате, - IL-программами.

    Основные элементы лексики

    Программы в IL-формате состоят из следующих лексических элементов:

  • идентификаторы;
  • метки;
  • константы;
  • зарезервированные слова;
  • специальные знаки;
  • комментарии.
  • Идентификаторы и метки чаще всего представляют последовательности символов, начинающиеся с латинской буквы (или с символов "_", "$", "@" и "?"), за которой следуют латинские буквы, цифры или символы "_", "$", "@" и "?". Кроме того, для идентификаторов и меток существует особая форма записи в апострофах: она позволяет включать в идентификаторы любые символы Unicode. Например:

    Label_1 $Name 'Идентификатор'

    Несколько идентификаторов могут быть объединены в один идентификатор с помощью точек. Например:

    System.Console.WriteLine

    Целочисленные константы записываются либо в десятичной системе счисления, либо в шестнадцатеричной (тогда перед ними ставится префикс "0x"). Например:

    128 -10 0xFF10B000

    В вещественных константах точка используется для разделения целой и дробной части, а символы "e" и "E" служат для указания экспоненциальной части. Кроме того, поддерживается особая форма записи float32 (целая_константа) и float64 (целая_константа), позволяющая представить целое число в виде числа с плавающей точкой. Например:

    5.5 -1.05e10 float32(128) float64(50)

    Строковые константы записываются в двойных кавычках и могут содержать Escape-последовательности "\t", "\n" и "\xxx", где восьмеричное число xxx задает код символа от 0 до 255. Для переноса строковой константы на другую строку программы используется символ "\". Кроме того, для строковых констант поддерживается операция конкатенации "+". Например:

    "Alpha Beta Gamma" "Hello, World\n" "Concat"+"enation"

    Комментарии в IL-программах записываются так же, как в языке C#:

  • Если в строке программы встречается "//", то остаток строки считается комментарием.
  • Текст, начинающийся с "/*", оканчивающийся на "*/" и не содержащий "*/", считается комментарием.
  • Синтаксис

    IL-программа представляет собой последовательность объявлений.

    В этом разделе мы рассмотрим синтаксис объявлений следующих элементов IL-программы:

  • сборка;
  • модуль;
  • тип;
  • поле;
  • метод.
  • Сборки и модули

    Каждый IL-файл для ассемблера ILASM представляет собой отдельный модуль сборки. Мы не будем касаться вопросов компиляции сборки, состоящей из нескольких модулей, поэтому приведем образец заголовка IL-файла для одномодульной сборки:

    .assembly MyProgram { }
    .module MyProgram.exe
    .assembly extern mscorlib { }

    В заголовке используются три директивы: директива ".assembly" позволяет задать имя нашей сборки. Директива ".module" определяет имя модуля и совпадает с именем исполняемого файла, в который будет записана откомпилированная сборка. Директива ".assembly extern" указывает, что мы будем импортировать сборку mscorlib, в которой находится основная часть библиотеки классов .NET. В фигурных скобках после имени сборки могут перечисляться свойства сборки, но в простейшем случае их можно оставить пустыми.

    Типы

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

  • последовательность атрибутов типа;
  • имя типа;
  • базовый тип;
  • список реализуемых интерфейсов.
  • Последовательность атрибутов следует непосредственно после ключевого слова ".class". В таблице 3.51 приведен набор наиболее часто используемых атрибутов.

    Атрибуты типов
    Атрибут Описание
    abstract Тип является абстрактным классом
    interface Тип является интерфейсом
    private Тип не экспортируется из сборки
    public Тип экспортируется из сборки
    sealed Тип не может являться базовым классом для другого типа (от него нельзя наследовать)
    serializable Экземпляры типа могут быть сериализованы

    После атрибутов следует идентификатор, задающий имя объявляемого типа.

    Если объявляемый тип наследует от какого-нибудь другого типа (базового класса), отличного от System.Object, то необходимо указать имя базового класса после ключевого слова "extends". При этом, если в качестве базового класса выбран System.ValueType, то объявляемый тип будет типом-значением.

    Если объявляемый тип реализует методы каких-либо интерфейсов, то должен быть приведен список этих интерфейсов после ключевого слова "implements".

    Рассмотрим несколько примеров:

  • Объявление экспортируемого абстрактного класса, реализующего интерфейс IEnumerable.
    .class public abstract MyAbstractClass
    extends [mscorlib]System.Object
    implements [mscorlib]System.Collections.IEnumerable
    { }
  • Объявление неэкспортируемого интерфейса.
    .class private interface MyInterface { }
  • Объявление экспортируемого типа-значения.
    .class public sealed MyValueType
      extends [mscorlib]System.ValueType
      { }
  • Обратите внимание, что перед именами библиотечных классов и интерфейсов в квадратных скобках указывается имя сборки, в которой они содержатся.

    Поля

    Поля объявляются внутри объявлений типов. Объявление поля осуществляется с помощью директивы ".field" и состоит из трех частей:

  • последовательность атрибутов поля;
  • тип;
  • имя поля.
  • Последовательность атрибутов следует непосредственно после ключевого слова ".field". В таблице 3.52 приведен набор наиболее часто используемых атрибутов.

    Атрибуты полей
    Атрибут Описание
    assembly Поле видимо внутри сборки
    family Поле видимо для наследников типа
    public Поле видимо для всех
    private Поле видимо только внутри типа
    static Поле является статическим

    После атрибутов следует тип поля и идентификатор, задающий имя поля.

    Рассмотрим несколько примеров:

  • Объявление поля x типа массив.
    .field private int32[] x
  • Объявление поля table типа Hashtable.
    .field public class
    [mscorlib]System.Collections.Hashtable table
  • Методы

    Методы объявляются внутри объявлений типов. Объявление метода осуществляется с помощью директивы ".method" и состоит из пяти частей:

  • последовательность атрибутов метода;
  • тип возвращаемого значения;
  • имя метода;
  • список параметров метода;
  • тело метода.
  • Последовательность атрибутов следует непосредственно после ключевого слова ".method". В таблице 3.53 приведен набор наиболее часто используемых атрибутов.

    После атрибутов следует тип возвращаемого значения и идентификатор, задающий имя метода. Если метод не возвращает значения, в качестве типа возвращаемого значения указывается void. Конструкторы всегда имеют имя ".ctor", а статические конструкторы - ".cctor".

    Атрибуты методов
    Атрибут Описание
    assembly Метод видим внутри сборки
    family Метод видим для наследников типа
    public Метод видим для всех
    private Метод видим только внутри типа
    abstract Метод является абстрактным
    virtual Метод является виртуальным
    final Метод не может переопределяться в наследниках
    static Метод является статическим

    Список параметров метода следует за именем метода и заключается в круглые скобки. Для каждого параметра указывается его тип и имя.

    Прежде чем перейти к рассмотрению синтаксиса объявлений тел методов, приведем несколько примеров заголовков методов:

  • Объявление конструктора с двумя параметрами.
    .method public void .ctor
      (int32 x, class [mscorlib]System.String s)
  • Виртуальный метод с управляемым указателем в качестве параметра.
    .method private virtual int32 myMethod(int32 pX)
  • Статический метод, возвращающий массив:
    .method public static int32[] MyStaticMethod()
  • Тело метода заключается в фигурные скобки и содержит инструкции языка CIL. Каждая инструкция записывается на новой строке программы. Если нужно, то инструкции может предшествовать метка, отделяемая от инструкции двоеточием. Например:

    Hello:	ldstr "Hello, World!"
      	call void [mscorlib]System.Console.WriteLine(string)

    Кроме инструкций CIL тело метода может содержать директивы тела метода. Они перечислены в таблице 3.54.

    Директивы тела метода
    Директива Описание
    .entrypoint Показывает, что данный метод является точкой входа в сборку (метод должен быть статическим, возвращать int32 или ничего не возвращать, иметь в качестве параметров массив строк или вообще не иметь параметров)
    .locals (объявления) Определяет набор локальных переменных метода. Локальные переменные объявляются аналогично параметрам метода
    .maxstack число Задает глубину стека вычислений

    Пример программы

    Давайте рассмотрим пример программы, написанной прямо на CIL с использованием синтаксиса ILASM. Мы не станем приводить весь текст программы сразу, а будем рассматривать ее постепенно, по частям.

    Естественно, наша программа будет начинаться с заголовка, объявляющего имена сборки и модуля и импортирующего стандартную библиотеку:

    .assembly Sample1 { }
    .module sample1.exe
    .assembly extern mscorlib { }

    Далее объявим тип-значение Point, реализующий понятие точки на плоскости. Он будет содержать два поля x и y типа float64, а также конструктор и статический метод, вычисляющий расстояние между двумя точками:

    .class public sealed Point extends [mscorlib]System.ValueType
    {
        .field public float64 x
        .field public float64 y
        .method public void .ctor (float64 x, float64 y)
        {
          .maxstack		3
          ldarg.0
          dup
          ldarg.1
          stfld  		float64 Point::x
          ldarg.2
          stfld  		float64 Point::y
          ret
        }
        .method public static float64 Distance
          (valuetype Point a, valuetype Point b)
        {
          .maxstack 	3
          ldarga  		a
          ldfld  		float64 Point::x
          ldarga  		b
          ldfld  		float64 Point::x
          sub
          dup
          mul
          ldarga  		a
          ldfld  		float64 Point::y
          ldarga  		b
          ldfld  		float64 Point::y
          sub
          dup
          mul
          add
          call  		float64 [mscorlib]System.Math::Sqrt(float64)
          ret
        }
    }

    А теперь объявим вспомогательный класс SampleClass, который будет содержать точку входа в нашу сборку. Метод Demo (точка входа) будет вычислять расстояние между точками (0.0,0.0) и (1.0,1.0) и выводить результат на экран:

    .class public SampleClass
    {
     .method public static void Demo()
     {
      .entrypoint
      .maxstack 3
      ldc.r8 	  0.0
      ldc.r8 	  0.0
      newobj 	  void Point::.ctor(float64,float64)
      ldc.r8 	  1.0
      ldc.r8 	  1.0
      newobj 	  void Point::.ctor(float64,float64)
      call 		  float64 Point::Distance(valuetype Point, valuetype Point)
      call 	  void [mscorlib]System.Console::WriteLine (float64)
      ret
       }
    }

    Откомпилируем нашу программу, которая записана в текстовом файле sample1.il. Подразумевается, что мы работаем в Windows и у нас переменная окружения path настроена таким образом, что программы ILASM и PEVERIFY можно вызывать без указания путей. Наберем в консоли команду:

    ilasm sample1.il

    Мы получим следующее сообщение от компилятора ILASM:

    Microsoft (R) .NET Framework IL Assembler. Version 1.1.4322.573
    Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
    Assembling 'sample1.il', no listing file, to EXE --> 'sample1.EXE'
    Source file is ANSI
    
    Assembled method Point::.ctor
    Assembled method Point::Distance
    Assembled method SampleClass::Demo
    Creating PE file
    Emitting members:
    Global
    Class 1 Fields: 2;   Methods: 2;
    Class 2 Methods: 1;
    Resolving member refs: 9 -> 9 defs, 0 refs
    Writing PE file
    Operation completed successfully

    Итак, наша программа успешно откомпилировалась и на диске появилась сборка sample1.exe. Попробуем провести ее верификацию:

    peverify sample1.exe

    В ответ верификатор выведет на экран:

    Microsoft (R) .NET Framework PE Verifier Version 1.1.4322.573
    Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
    
    All Classes and Methods in sample1.exe Verified

    Таким образом, сборка успешно прошла верификацию и мы можем рискнуть ее запустить:

    sample1.exe

    В результате на экран выводится расстояние между точками (0.0,0.0) и (1.0,1.0):

    1, 4142135623731
    Вернуться к учебному плану