Обработка исключений. Лекция 3-4
Обработка исключений
Непредусмотренные ошибки вызывают исключения.
Исключение, или исключительная ситуация – это аномалии,
которые могут возникнуть во время выполнения приложения.
Основные типы:
Ошибки при работе с ресурсами компьютера. Например,
ошибки чтения/записи данных или выделения памяти для
работы программы.
Ошибки логики приложения – возникают, если разработчик
допустил оплошность при разработке логики программы.
Пример:
деление
на
ноль,
выход
за
пределы
коллекции/массива.
Ошибки
пользователя
–
ошибки
ввода
данных
некорректные действия, которые нельзя предусмотреть.
или
1
2.
Некоторые стандартные типы исключений
Базовым для
Exception.
всех
типов
исключений
является
тип
2
3.
Конструкция try-catch-finally
try
// контролируемый блок (является обязательным)
>
catch
// обработчик исключения (их может быть несколько)
>
finally
// блок завершения (можно опустить)
>
В контролируемый блок включаются операторы, которые
потенциально могут вызвать ошибку.
В обработчике исключения описывается
обрабатываются ошибки различных типов.
то,
как
Блок завершения выполняется
возникла ли ошибка в блоке try.
от
того,
независимо
3
4.
Конструкция try-catch-finally
Вначале выполняются инструкции в блоке try.
Если в блоке try возникло исключение, выполнение
текущего блока прекращается и выполняется поиск
соответствующего обработчика исключения в блоке
catch, управление передается данному блоку.
В любом случае (была ошибка или нет) выполняется
блок finally, если он присутствует.
Если обработчик исключения не найден, вызывается
стандартный обработчик исключения. Исполняющая
система перехватит исключение, выдаст сообщение об
ошибке и аварийно завершит работу программы.
4
5.
Конструкция try-catch-finally
Пример 1.
using System;
namespace ConsoleApp1
class Program
static void Main()
try
Console.Write(«Введите x: «);
int x = int.Parse(Console.ReadLine());
Console.Write(«Введите y: «);
int y = int.Parse(Console.ReadLine());
double result = x / y;
Console.WriteLine(«Результат: » + result);
Console.ReadKey();
>
// Обработка исключения, возникающего при делении на ноль
catch (DivideByZeroException)
Console.WriteLine(«Делить на ноль нельзя!\n»);
Main();
>
// Обработка исключения при некорректном вводе числа
catch (FormatException)
Console.WriteLine(«Введены некорректные данные. Введите число.\n»);
Main();
>
>
>
>
5
6.
Конструкция try-catch-finally
Пример 2.
try
Console.Write(«Введите x: «);
int x = int.Parse(Console.ReadLine());
Console.Write(«Введите y: «);
int y = int.Parse(Console.ReadLine());
double result = x / y;
Console.WriteLine(«Результат: » + result);
>
// Обработка исключения, возникающего при делении на ноль
catch (DivideByZeroException ex)
Console.WriteLine($»Возникло исключение: «);
>
6
7.
Конструкция try-catch-finally
Пример 3.
try
Console.WriteLine(«Введите число: «);
int number = int.Parse(Console.ReadLine());
>
catch
Console.WriteLine(«\nВы ввели не число. «);
>
finally
Console.WriteLine(«\nЧто бы вы ни ввели, вы молодец!»);
>
Console.WriteLine(«\nКонец программы»);
Console.ReadLine();
7
8.
Конструкция try-catch-finally
Блок catch может иметь следующие формы:
Обрабатывает любое исключение из блока try:
catch
// выполняемые инструкции
>
Обрабатывает только те исключения, которые соответствуют
типу, указанному в скобках:
catch (тип_исключения)
// выполняемые инструкции
>
Обрабатывает только те исключения, которые соответствуют
типу, указанному в скобках, вся информация об исключении
помещается в переменную данного типа:
catch (тип_исключения имя_переменной)
// выполняемые инструкции
>
8
9.
Конструкция try-catch-finally
Фильтры исключений позволяют обрабатывать исключения
в зависимости от определенных условий. Для их
применения после выражения catch идет выражение when,
после которого в скобках указывается условие:
catch when (условие)
// выполняемые инструкции
>
Обработка в блоке catch происходит только в том случае, если
условие в выражении when истинно.
9
10.
Условные конструкции при обработке
исключений
В ряде случаев более оптимально будет применить
условные конструкции в тех местах, где можно применить
блок try-catch.
С точки зрения производительности использование блоков
try-catch более накладно, чем применение условных
конструкций.
По
возможности
лучше
использовать
условные конструкции на проверку исключительных
ситуаций вместо try-catch.
10
11.
Условные конструкции при обработке
исключений
Пример.
// При вводе нецелого числа возникнет исключение и будет совершен аварийный выход из программы
Console.Write(«Введите целое число: «);
int x = int.Parse(Console.ReadLine());
______________________________________________________________
Console.Write(«Введите целое число: «);
int x1;
if (int.TryParse(Console.ReadLine(), out x1))
Console.WriteLine($»Квадрат числа = » + Math.Pow(x1, 2));
>
else
Console.WriteLine(«Вы ввели не целое число.»);
>
11
12.
Оператор throw
позволяет самостоятельно генерировать исключительные
ситуации:
throw [объект_класса_исключений];
Например:
throw new DivideByZeroException();
В качестве параметра должен быть объект, порожденный
стандартным
классом
System.Exception.
Далее
он
используется для передачи информации об исключении его
обработчику.
12
13.
Оператор throw
Пример.
try
Console.Write(«Введите числитель: «);
int a = int.Parse(Console.ReadLine());
Console.Write(«Введите знаменатель: «);
int b = int.Parse(Console.ReadLine());
if (b == 0)
throw new Exception(«Ноль не может быть использован в качестве знаменателя!»);
>
else
Console.WriteLine(«Частное : » + (double)(a / b));
>
>
catch (Exception e)
Console.WriteLine($»Ошибка: «);
>
13
14.
Массивы
Массив – набор элементов одного типа, объединенных
общим именем.
Одномерный массив – это фиксированное количество
элементов одного и того же типа, объединенных общим
именем, где каждый элемент имеет свой номер.
Нумерация элементов массива в С# начинается с нуля:
если массив состоит из 5 элементов, то они будут иметь
следующие номера: 0, 1, 2, 3, 4.
14
15.
Одномерные массивы
Одномерный массив в С# реализуется как объект, поэтому его
создание представляет собой двухступенчатый процесс: сначала
объявляется
ссылочная
переменная
типа
массив,
затем
выделяется память под требуемое количество элементов базового
типа, и ссылочной переменной присваивается адрес нулевого
элемента в массиве.
Базовый тип определяет тип данных каждого элемента массива.
Количество элементов, которые будут храниться в массиве,
определяется размером массива. Размерность задается при
выделении памяти и не может быть изменена впоследствии.
Способы объявления:
1) базовый_тип[] имя_массива;
Например: int[] numbers;
2) базовый_тип[] имя_массива = new базовый_тип[размер];
Например: int[] numbers = new int[10];
15
16.
Одномерные массивы
В C# элементам массива присваиваются начальные
значения по умолчанию в зависимости от базового типа.
Для арифметических типов – нули, для ссылочных типов –
null, для символов – символ с кодом ноль.
Третий вариант
объявлении.
–
инициализация
массива
сразу
при
Например:
int[] nums1 = new int[4] < 1, 2, 3, 4 >;
int[] nums2 = new int[] < 1, 2, 3, 4 >;
int[] nums3 = new[] < 1, 2, 3, 4 >;
int[] nums4 = < 1, 2, 3, 4 >;
string[] colors = < "Red", "Orange", "Yellow" >;
16
17.
Одномерные массивы
Обращение к элементу массива происходит с помощью
индекса – номера элемента в массиве (нумерация
начинается с нуля!).
Например: arr[0], a[9], b[i].
Получение элемента массива:
int[] numbers = < 1, 2, 3, 5 >;
// вывод значения элемента массива на консоль
Console.WriteLine(numbers[3]); // 5
// получение элемента массива в переменную
var n = numbers[1];
// 2
Console.WriteLine(n); // 2
17
18.
Одномерные массивы
Изменение элемента массива:
int[] numbers = < 1, 2, 3, 5 >;
numbers[1] = 0;
Console.WriteLine(numbers[1]); // 0
Каждый массив имеет свойство Length, которое хранит
длину (размер) массива. Для получения длины массива
необходимо обратиться к свойству Length, указав его через
точку: numbers.Lentgh.
Например, получим последний элемент массива:
Console.WriteLine(numbers[numbers.Length — 1]); // 5
При работе с массивом автоматически выполняется
контроль выхода за его границы: если значение индекса
выходит за границы массива, генерируется исключение
IndexOutOfRangeException.
18
19.
Одномерные массивы
Так как массив представляет собой набор элементов,
обработка массива обычно производится в цикле.
Вывод массива на экран
При помощи цикла foreach:
int[] numbers = < 1, 2, 3, 4, 5 >;
// Последовательно и
foreach (int i in numbers)
// только для чтения
Console.WriteLine(i);
>
При помощи цикла for:
int[] numbers = < 1, 2, 3, 4, 5 >;
// Можно менять приращение
for (int i = 0; i < numbers.Length; i++)
// счетчика и изменять
// элементы
Console.WriteLine(numbers[i]);
>
19
20.
Одномерные массивы
Вывод массива на экран
При помощи цикла while:
int[] numbers = < 1, 2, 3, 4, 5 >;
int i = 0;
while(i < numbers.Length)
Console.WriteLine(numbers[i]);
i++;
>
20
21.
Базовый класс Array
Все массивы в C# имеют общий базовый класс Array,
определенный в пространстве имен System.
Некоторые элементы класса Array:
Clear (Статический метод) – Присваивает элементам массива
значения по умолчанию (для арифметических типов нули и
т.д.);
Copy (Статический метод)
массива в другой массив;
IndexOf (Статический метод) – Осуществляет поиск первого
вхождения элемента в одномерный массив, если найден –
возвращает индекс, иначе -1;
Length (Свойство) — Количество элементов массива;
Reverse (Статический метод) – Изменяет порядок следования
элементов в массиве на обратный;
Sort (Статический метод)
одномерного массива.
–
–
Копирует
элементы
Упорядочивание
одного
элементов
21
22.
Двумерные массивы
Многомерные массивы имеют более одного измерения. Чаще
всего используются двумерные массивы, которые представляют
собой таблицы. Каждый элемент массива имеет два индекса,
первый определяет номер строки, второй – номер столбца, на
пересечении которых находится элемент. Нумерация строк и
столбцов начинается с нуля.
Объявить двумерный массив можно одним из предложенных
способов:
1) базовый_тип[,] имя_массива;
Например: int[,] a;
2) базовый тип[,] имя_массива = new базовый_тип[размер1,
размер2];
Например: float[,] a= new float[3, 4];
Элементы массива инициализируются по умолчанию нулями.
22
23.
Двумерные массивы
Объявление с инициализацией:
3) базовый_тип[,] имя_массива = , … ,
>;
Например: int[,] a= new int[,] , >;
Все способы:
int[,] nums1;
int[,] nums2 = new int[2, 3];
int[,] nums3 = new int[2, 3] < < 0, 1, 2 >, < 3, 4, 5 >>;
int[,] nums4 = new int[,] < < 0, 1, 2 >, < 3, 4, 5 >>;
int[,] nums5 = new [,]< < 0, 1, 2 >, < 3, 4, 5 >>;
int[,] nums6 = < < 0, 1, 2 >, < 3, 4, 5 >>;
23
24.
Двумерные массивы
Обращение к элементу массива происходит с помощью
индексов: указывается имя массива, в квадратных скобках
номер строки и через запятую номер столбца, на
пересечении
которых
находится
данный
элемент.
Например, a[0, 0], b[2, 3], c[i, j].
Так как массив представляет собой набор элементов,
объединенных общим именем, то обработка массива
обычно производится с помощью вложенных циклов.
При обращении к свойству Length для двумерного массива
получаем общее количество элементов в массиве.
Чтобы получить количество строк, нужно обратиться к
методу GetLength с параметром 0. Чтобы получить
количество столбцов – к методу GetLength с параметром 1.
24
Исключения в Windows x64. Как это работает. Часть 4
Опираясь на материал, описанный в первой, второй и третьей частях данной статьи, мы продолжим обсуждение темы обработки исключений в Windows x64.
Описываемый материал требует знания базовых понятий, таких, как пролог, эпилог, кадр функции и понимания базовых процессов, таких, как действия пролога и эпилога, передача параметров функции и возврат результата функции. Если читатель не знаком с вышеперечисленным, то перед прочтением рекомендуется ознакомиться с материалом из первой части данной статьи. Если читатель не знаком со структурами PE образа, которые задействуются в процессе обработки исключения, тогда перед прочтением рекомендуется ознакомиться с материалом из второй части данной статьи. Также, если читатель не знаком с процессом поиска и вызова обработчиков исключений, рекомендуется ознакомиться с третьей частью данной статьи.
Приводимое описание относится к реализации в Windows, и, следовательно, не следует полагать, что прилагаемая к статье реализация данного механизма будет в точности совпадать с ней, хотя концептуально различий нет. Детали прилагаемой реализации в статье рассматриваться не будут, если об этом не будет сказано явно. Поэтому предполагается, что эти детали, по необходимости, следует изучить самостоятельно.
К статье прилагается реализация механизма, которая находится в папке exceptions хранилища git по этому адресу.
1. Раскрутка стека
В процессе обработки ошибок может возникнуть ситуация, когда необходимо напрямую вернуть управление одной из предыдущих функций, минуя промежуточные функции. Т.е. возврат управления будет выполнен не посредством обычного возврата из функции в вызывающую функцию, которая в свою очередь тоже должна будет выполнить такой возврат, а посредством изменения состояния процессора таким образом, чтобы сразу после изменения он продолжил выполнять целевую функцию. На рисунке 1 изображен пример такой ситуации, где стрелкой указано направление роста стека.
На примере выше стек состоит из кадров четырех функций, где функция Main вызвала Func1, Func1 вызвала Func2, а Func2 вызвала Func3. Поэтому, например, если функции Func3 требуется вернуть управление функции Main, тогда она воспользуется функцией RtlUnwind/RtlUnwindEx, которая экспортируется модулем ntdll.dll в пользовательском пространстве и модулем ntoskrnl.exe в пространстве ядра. Прототип функции RtlUnwindEx изображен ниже, на рисунке 2.
Параметр TargetFrame принимает адрес кадра той функции, до которой следует раскрутить стек. Параметр TargetIp принимает адрес инструкции, с которой продолжится выполнение после раскрутки. Параметр ExceptionRecord принимает указатель на EXCEPTION_RECORD структуру, которая будет передаваться обработчикам при раскрутке. Параметр ReturnValue записывается в RAX регистр процессора, т.е. сразу после передачи управления соответствующей функции регистр RAX будет содержать значение этого параметра. Параметр ContextRecord содержит указатель на CONTEXT структуру, которая используется функцией RtlUnwindEx при раскрутке функций и определении целевого состояния процессора после раскрутки. Параметр HistoryTable принимает указатель на структуру, которая используется для кэширования поиска. Формат этой структуры вы сможете найти в winnt.h.
Параметр TargetFrame является необязательным. Если его значение равно NULL, тогда функция RtlUnwindEx выполняет так называемую раскрутку при выходе (exit unwind), где раскручиваются кадры всех функций стека. В этом случае параметр TargetIp игнорируется. Параметр ExceptionRecord является необязательным, и если он равен NULL, тогда функция RtlUnwindEx инициализирует свою структуру EXCEPTION_RECORD, где поле ExceptionCode будет содержать STATUS_UNWIND значение, поле ExceptionRecord будет содержать NULL, поле ExceptionAddress будет содержать указатель на инструкцию функции RtlUnwindEx, а поле NumberParameters будет содержать 0. Параметр HistoryTable является необязательным.
Прототип функции RtlUnwind отличается лишь тем, что он не принимает два последних параметра.
Ниже, на рисунке 3, изображен пример работы функции RtlUnwind.
На рисунке выше изображен пример программы, состоящей из четырех функций: _tmain, Func1, Func2, Func3. Функция _tmain вызывает функцию Func1, функция Func1 вызывает Func2, а функция Func2 вызывает Func3. Функции Func1, Func2, Func3 возвращают булево значение. Функция Func3 выполняет виртуальную раскрутку трех предыдущих функций с целью: найти адрес кадра функции _tmain; найти адрес инструкции, с которой будет продолжено выполнение, и в данном примере адрес будет указывать на инструкцию сразу после инструкции вызова функции Func1. Справа от исходного кода изображен ассемблерный код _tmain и Func3 функций, адреса инструкций которых являются абсолютными. Справа от ассемблерного кода изображены состояния процессора и стеки вызовов для трех случаев: сверху изображено состояние процессора и стек вызовов сразу перед вызовом функции Func1; посередине изображено состояние процессора и стек вызовов сразу перед вызовом функции RtlUnwind; внизу изображено состояние процессора после выполнения функции RtlUnwind. Указатели инструкций этих состояний сопоставляются с ассемблерными инструкциями посредством уникальных номеров. Следует обратить внимание на последний случай, где RAX регистр принял значение параметра ReturnValue, а стек вызов сократился до одной функции, т.е. кадры функций Func1, Func2 и Func3 более не существуют в стеке. Поскольку значение RAX после раскрутки не нулевое, функция _tmain выведет сообщение на экран. В обычном случае, т.е. если бы раскрутка не выполнялась, это сообщение не будет выведено, т.к. функция Func3 возвращает false. Также следует обратить внимание на то, что цикл поиска указателя кадра функции _tmain выполняет четыре итерации, когда раскручиваемых функций всего три. Это связано с ранее обсуждаемыми особенностями функции RtlVirtualUnwind. Дело в том, что после вызова функции RtlVirtualUnwind параметры HandlerData и EstablisherFrame примут соответствующие значения для той функции, для которой выполнялась виртуальная раскрутка, когда параметр ContextRecord будет отражать состояние процессора сразу после вызова раскрученной функции. Следовательно, на третьей итерации цикла функция RtlVirtualUnwind вернет в параметр EstablisherFrame указатель кадра для функции Func1, когда параметр ContextRecord будет отражать состояние процессора сразу после вызова функции Func1. Поэтому требуется выполнить дополнительную итерацию, чтобы определить указатель кадра функции _tmain.
Функция RtlUnwind/RtlUnwindEx, также, до раскрутки стека, последовательно вызывает обработчики раскрутки всех функций, начиная с самой себя и до функции, которая является целевой, включительно. Поскольку функция RtlUnwind/RtlUnwindEx не имеет обработчиков исключений/раскрутки, то в процессе виртуальной раскрутки она будет просто пропущена и, следовательно, не будет никаких побочных эффектов. С другой стороны, это накладные расходы, т.к. чтобы найти кадр функции, которая вызвала функцию RtlUnwind/RtlUnwindEx, необходимо выполнить дополнительную виртуальную раскрутку. Процесс вызова обработчиков и изменение состояния процессора в целях передачи управления одной из предыдущих функций и является так называемой раскруткой.
Ниже на рисунке 4 изображена блок-схема функции RtlUnwindEx.
В начале своей работы функция получает нижний и верхний лимиты стека. Далее функция захватывает текущее состояние процессора посредством вызова функции RtlCaptureContext. Таким образом, структура CONTEXT будет отражать состояние процессора сразу после вызова функции RtlCaptureContext. Эта же структура используется в качестве первоначального состояния процессора, с которого начинается виртуальная раскрутка функций. Функция RtlUnwindEx в процессе своей работы использует две структуры CONTEXT: одна отражает состояние процессора в момент выполнения функции, для которой выполняется вызов обработчика (здесь и далее — текущий контекст); другая отражает состояние процессора сразу после возврата из этой функции (здесь и далее — предыдущий контекст). Это необходимо из-за ранее обсуждаемых особенностей функции RtlVirtualUnwind. Также функция RtlUnwindEx, как это уже ранее обозначалось, инициализирует структуру EXCEPTION_RECORD для последующей передачи обработчикам раскрутки, если соответствующий параметр не был передан при вызове функции.
Далее функция формирует первоначальное значение поля ExceptionFlags для структуры EXCEPTION_RECORD. Это значение хранится в локальной переменной и изначально не хранится в поле самой структуры. Функция устанавливает флаг EXCEPTION_UNWINDING, и если адрес кадра целевой функции не был передан функции, тогда функция также устанавливает флаг EXCEPTION_EXIT_UNWIND. Таким образом, флаг EXCEPTION_UNWINDING для обработчиков означает, что выполняется раскрутка, а флаг EXCEPTION_EXIT_UNWIND означает, что раскручиваются кадры всех функций.
Далее функция посредством функции RtlLookupFunctionEntry получает адрес PE образа и указатель на RUNTIME_FUNCTION структуру функции этого образа, обработчик которой необходимо вызвать (здесь и далее — текущая функция). Адрес одной из инструкций этой функции извлекается из текущего контекста. На первой итерации это будет адрес инструкции самой функции RtlUnwindEx. Если функция RtlLookupFunctionEntry не вернула указатель, тогда считается, что текущая функция, для которой выполнялась попытка вызова её обработчика, — простая, и, следовательно, функция не имеет кадра. Т.к. простые функции не выделяют память в стеке, значение их RSP будет указывать на адрес возврата, следовательно, для таких функций функция RtlUnwindEx извлекает этот адрес, копирует его значение в текущий контекст и увеличивает значение поля Rsp текущего контекста на 8. Теперь текущий контекст отражает состояние процессора в момент выполнения следующей по стеку выше функции. Затем функция продолжит свою работу, начиная с получения адреса PE образа и указателя на RUNTIME_FUNCTION структуру, для адреса новой инструкции, уже для следующей по стеку выше функции.
Для кадровых функций функция RtlLookupFunctionEntry вернет указатель на RUNTIME_FUNCTION структуру. В таком случае вызывается функция RtlVirtualUnwind с целью определить указатель кадра текущей функции, а также адрес ее обработчика и указатель на данные для этого обработчика. Перед вызовом функции RtlVirtualUnwind функция RtlUnwindEx скопирует текущий контекст в предыдущий. Это выполняется с той целью, чтобы сохранить состояние процессора, описывающее момент выполнения текущей функции на тот случай, если функция окажется целевой. Уже неоднократно упоминалось, что функция RtlVirtualUnwind возвращает адрес кадра той функции, которая выполнялась в переданном состоянии процессора, когда по возврату из функции RtlVirtualUnwind состояние будет описывать следующую по стеку выше функцию. Следовательно, когда функции RtlUnwindEx потребуется возобновить выполнение целевой функции, невозможно будет использовать то состояние процессора, которое вернула функция RtlVirtualUnwind, т.к. оно будет отражать выполнение той функции, которая вызвала целевую функцию. Сразу после вызова функции RtlVirtualUnwind, функция RtlUnwindEx выполнит проверку указателя кадра раскрученной функции на выход за пределы лимита стека. Также функция проверит, располагается ли кадр текущей функции в стеке выше, чем кадр целевой функции, что в свою очередь будет означать, что функция RtlUnwindEx пропустила кадр целевой функции вследствие повреждения стека, повреждения .pdata секции и т.п. В обоих случаях функция сгенерирует исключение STATUS_BAD_STACK. В противном случае, если функция RtlVirtualUnwind не вернула адрес обработчика, то функция RtlUnwindEx поменяет местами текущий и предыдущий контексты, если текущая функция не являлась целевой. Таким образом, следующая по стеку выше функция станет текущей. Далее, функция продолжит свою работу, начиная с получения адреса PE образа и указателя на RUNTIME_FUNCTION структуру, для адреса новой инструкции, уже для следующей по стеку выше функции.
Если функция RtlVirtualUnwind вернула адрес обработчика для текущей функции, тогда ее обработчик необходимо вызвать. Перед его вызовом функция RtlUnwindEx установит флаг EXCEPTION_TARGET_UNWIND в том случае, если текущая функция является целевой. Таким образом, обработчик этой функции сможет определить, что его соответствующая функция является функцией, управление которой передается. Затем функция RtlUnwindEx обновит содержимое поля ExceptionFlags структуры EXCEPTION_RECORD из своей локальной копии. Обработчик исключения впервые обсуждался в разделе 3 второй части данной статьи, а его прототип изображен на рисунке 5. Перед вызовом обработчика функция, как и функция RtlDispatchException, обсуждаемая в разделе 2.2 третьей части данной статьи, подготавливает структуру DISPATCHER_CONTEXT, которая активно используется в случаях вложенных исключений (nested exception) и активной раскрутки (collided unwind). Определение самой структуры также изображено на рисунке 17 в разделе 2.2 третьей части данной статьи. Поля этой структуры инициализируются так же, как и в случае с функцией RtlDispatchException, с тем исключением, что поле TargetIp будет содержать значение соответствующего параметра переданного функции RtlUnwindEx, т.е. адрес инструкции, с которого будет возобновлено выполнение после раскрутки; поле ContextRecord будет содержать указатель на структуру CONTEXT, которая описывает состояние процессора в момент выполнения текущей функции, а не следующей по стеку выше; поле ScopeIndex содержит текущее значение локальной переменной и будет более подробно рассмотрено при обсуждении конструкций try/except и try/finally.
Обработчик, как и в случае с функцией RtlDispatchException, не вызывается напрямую, и вместо этого используется вспомогательная функция RtlpExecuteHandlerForUnwind, которая принимает такие же параметры, как и сам обработчик, а также возвращает такое же значение. Данная функция фактически является оберткой над функцией обработчика раскрутки и используется для того, чтобы перехватывать исключения, возникшие в процессе выполнения самого обработчика. Ассемблерное представление функции представлено ниже на рисунке 5.
Как отражено на рисунке, сначала функция выделяет память в стеке для регистровых переменных и одной переменой, сохраняет указатель на переданную DISPATCHER_CONTEXT структуру в этой переменной и вызывает обработчик исключения, адрес которого хранится в поле LanguageHandler структуры DISPATCHER_CONTEXT. Также обратите внимание на присутствие заполнителя тела функции. Его роль такая же, как и для функции RtlpExecuteHandlerForException. Ассемблерное представление функции обработчика исключения представлено ниже на рисунке 6.
Как отражено на рисунке, обработчик копирует контекст предыдущего процесса раскрутки в структуру DISPATCHER_CONTEXT текущего процесса поиска обработчика или раскрутки. Это позволяет продолжить поиск обработчика с того места, где была ранее прервана раскрутка, или продолжить ранее прерванную раскрутку. Также это позволяет пропустить вызов обработчиков тех функций, для которых такой вызов уже был выполнен во время предыдущей раскрутки. Следует также отметить, что вызов обработчиков возобновляется с той функции, на которой был прерван процесс раскрутки. Т.е. для таких функций обработчик будет вызван повторно. Более подробное пояснение этому будет дано во время обсуждения конструкций try/except и try/finally.
После того, как структура DISPATCHER_CONTEXT была подготовлена, функция RtlUnwindEx вызывает соответствующий обработчик. Сразу после вызова обработчика функция сбрасывает флаги EXCEPTION_COLLIDED_UNWIND и EXCEPTION_TARGET_UNWIND.
Если обработчик вернул ExceptionContinueSearch, то функция поменяет местами текущий и предыдущий контексты, если текущая функция не являлась целевой. Таким образом, следующая по стеку выше функция станет текущей. Далее функция продолжит свою работу, начиная с получения адреса PE образа и указателя на RUNTIME_FUNCTION структуру, для адреса новой инструкции, уже для следующей по стеку выше функции.
Если обработчик вернул ExceptionCollidedUnwind, то это означает, что в процессе раскрутки была обнаружена другая активная раскрутка, в контексте которой возникло исключение. В этом случае структура DISPATCHER_CONTEXT функции RtlUnwindEx будет содержать контекст прерванной раскрутки, т.к. он был скопирован обработчиком функции RtlpExecuteHandlerForUnwind. Следовательно, функция обновит текущий контекст из поля ContextRecord структуры DISPATCHER_CONTEXT, посредством функции RtlVirtualUnwind получит предыдущий, установит флаг EXCEPTION_COLLIDED_UNWIND и вызовет обработчик, в контексте которого ранее возникло исключение, и в зависимости от его возвращаемого результата выполнит ранее описанные действия.
Во всех остальных случаях функция RtlUnwindEx сгенерирует исключение STATUS_INVALID_DISPOSITION.
На каждой итерации перед получением адреса PE образа и указателя на RUNTIME_FUNCTION структуру функция посредством функции RtlpIsFrameInBounds проверяет, что указатель кадра функции, для которой выполнялась попытка вызова ее обработчика, находится в пределах лимита стека и не является указателем кадра целевой функции. Если такая проверка дает положительный результат, то работа функции продолжается. Иначе, если указатель кадра выходит за пределы лимита и указатель не является адресом кадра целевой функции, значит либо выполнялась раскрутка при выходе, и в процессе раскрутки ни один из обработчиков не остановил этот процесс, либо указатель кадра функции не был найден вследствие повреждения стека, повреждения .pdata секции и т.п. В таком случае функция RtlUnwindEx породит исключение, чтобы предоставить возможность отладки, но не в целях его обработки. Во всех остальных случаях работа функции завершится, т.к. найден кадр целевой функции. В этом случае в поле Rax текущего контекста будет записано значение переданного параметра ReturnValue, а в поле Rip этого же контекста будет записано значение переданного параметра TargetIp, если кодом исключения не является код STATUS_UNWIND_CONSOLIDATE. Т.к. этот случай не имеет непосредственного отношения к обсуждаемой теме, данный код не будет обсуждаться в данной статье. Здесь следует только отметить, что для раскрутки с таким кодом функцией RtlRestoreContext будет вызван обработчик перед возобновлением работы, и если поле Rip будет обновлено, обработчик получит неверное представление о состоянии процессора. Далее функция RtlUnwindEx вызывает функцию RtlRestoreContext, которой она передает два параметра: текущий контекст и указатель на структуру EXCEPTION_RECORD, который был либо передан функции RtlUnwindEx, либо передается указатель на локально сформированную структуру. К моменту вызова функции RtlRestoreContext обработчики раскрутки всех функций в стеке, начиная с его вершины и до целевой функции включительно, были вызваны. Функция RtlRestoreContext не возвращает управления, т.к. применяет к процессору новое состояние.
Стоит отметить, что проверка указателя кадра на каждой итерации является ошибкой в реализации, т.к. следует проверять указатель стека из текущего контекста, а не указатель кадра. В первую очередь данная проверка выполняется сразу после виртуальной раскрутки текущей функции. И если результат проверки отрицательный, то функция сгенерирует исключение. Следовательно, данная проверка на каждой итерации никогда не даст отрицательного результата, а поскольку не проверяется указатель стека, функция в процессе своей работы может выйти за его пределы. Данная ошибка сохраняется до сих пор.
2. Конструкции try/except и try/finally
С точки зрения операционной системы, как это уже было рассмотрено при описании обработки исключений и раскрутки стека, сама обработка всегда является обычным вызовом соответствующей функции. Конструкции try/except и try/finally являют собой механизм, который позволяет во время разработки размещать код обработки исключений прямо в теле функции. Следовательно, поскольку код обработки размещается непосредственно в теле, эти части кода не могут быть вызваны операционной системой напрямую. Чтобы обеспечить корректное функционирование этих конструкций, компилятор генерирует вспомогательную информацию, которой пользуются вызываемые операционной системой обработчики исключений. Ранее упоминалось, что всю обработку исключений условно можно поделить на две фазы. Фаза поиска и передача управления обработчикам исключений обсуждаемых конструкций и является второй фазой. Такое разделение необходимо, поскольку разные языки программирования по-разному обрабатывают исключения; таким образом, сама операционная система абстрагирована от понимания разнообразия механизмов разных языков программирования.
Компилятор C/C++ резервирует функцию __C_specific_handler. Именно эта функция отвечает за поиск и передачу управления соответствующей конструкции. Сама функция должна быть реализована программистом. Такой подход позволяет абстрагировать компилятор от понимания работы самой операционной системы и адаптировать исполняемый образ к любой среде исполнения, например, к подсистеме Win32, к среде исполнения ядра Windows или к любой другой среде. Также реализация этой функции экспортируется модулем ntdll.dll в пользовательском пространстве и модулем ntoskrnl.exe в пространстве ядра. Поставляемые Windows SDK и WDK содержат библиотеки, которые импортируют эту функцию из соответствующего модуля. Поле ExceptionHandlerAddress структуры EXCEPTION_HANDLER будет содержать указатель на эту функцию, когда поле LanguageSpecificData этой же структуры будет содержать структуру SCOPE_TABLE, которая описывает расположение всех конструкций в теле функции. Прототип функции изображен на рисунке 5 в разделе 3 второй части данной статьи. Определение структуры SCOPE_TABLE представлено ниже, на рисунке 7.
Поле Count содержит количество конструкций в теле функции и, следовательно, количество элементов ScopeRecord в структуре. Компилятор генерирует для каждой конструкции соответствующий ScopeRecord элемент структуры, который в свою очередь описывает расположение соответствующей конструкции в теле функции, а также расположение его обработчиков. Элементы ScopeRecord сортируются в следующем порядке: невложенные конструкции следуют друг за другом в порядке их появления в коде, когда вложенные конструкции всегда следуют перед конструкцией, в которую они вложены. Поле BeginAddress элемента ScopeRecord содержит адрес начала try блока. Поле EndAddress содержит адрес инструкции, следующей за последней инструкцией, заключенной в try блок. Поле JumpTarget, если не равно нулю, содержит адрес первой инструкции кода, заключенной в except блок. Код except блока следует сразу после кода, заключенного в try блок. Поле HandlerAddress содержит адрес функции фильтра except блока. Несмотря на то, что фильтр исключения заключается в скобках после except выражения, код фильтра генерируется компилятором в виде отдельной функции, прототип которой изображен ниже, на рисунке 8.
Функция принимает два параметра. Первый параметр содержит указатель на структуру, определение которой приведено ниже, на рисунке 9. Второй параметр содержит указатель кадра функции, в которой располагается соответствующая конструкция. Этот указатель используется фильтром в том случае, если во время фильтрации необходимо получить доступ к локальным переменным функции, в которой располагается соответствующая конструкция.
Как это отражено на рисунке выше, структура содержит два указателя. Первый указывает на структуру, описывающую причину исключения, второй — на структуру, описывающую состояние процессора в момент возникновения исключения.
Функция фильтра возвращает следующие значения: EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_SEARCH, EXCEPTION_CONTINUE_EXECUTION. Первое значение означает, что требуется передать управление обработчику исключения, для которого была вызвана функция фильтра. Также это значение может быть закодировано непосредственно в поле HandlerAddress. В таком случае конструкция не имеет фильтра, и передача управления обработчику исключения этой конструкции выполняется всегда. Второе значение указывает на то, что следует продолжить поиск обработчика исключения. Третье значение означает, что следует прервать поиск и возобновить выполнение прерванного потока.
Если поле JumpTarget равно нулю, тогда данная конструкция является finally конструкцией, и код, заключенный в finally блоке, следует сразу после кода, заключенного в try блок. В этом случае поле HandlerAddress содержит адрес функции, которая по своему содержимому повторяет код, заключенный в finally блок. Прототип этой функции изображен на рисунке 10.
Поскольку код, заключенный в finally блок, выполняется независимо от того, возникало исключение или нет, то в случае, если исключение имело место, этот код не может быть вызван напрямую, т.к. он располагается в теле функции. И поскольку во время раскрутки вызывать этот код необходимо, компилятор дублирует код, заключенный в finally блок, в виде отдельной функции. Первый параметр является булевым значением, означающим, что код finally блока выполняется из-за ненормального завершения кода, заключенного в try блок (т.е. в процессе его выполнения возникло исключение). Второй параметр содержит указатель кадра функции, в которой располагается соответствующая конструкция. Этот указатель используется функцией так же, как и функцией фильтра исключения – доступ к локальным переменным функции, в которой располагается соответствующая конструкция. Функция не возвращает никаких значений.
В тех случаях, когда в процессе выполнения кода, заключенного в try блоке, не возникло исключения, выполняется код finally блока, который следует сразу после кода try блока.
Все адреса в структуре SCOPE_TABLE являются адресами относительно начала образа.
Ниже, на рисунке 11, изображен пример структуры SCOPE_TABLE, которую сгенерирует компилятор.
На рисунке выше изображен пример программы, _tmain функция которой включает в себя try/except и try/finally конструкции. Слева от исходного кода изображено ассемблерное представление функций: _tmain, функции фильтра нижней try/except конструкции и функции, дублирующей код, заключенный в finally блок. Функции перечислены снизу вверх. Адреса ассемблерных инструкций являются абсолютными. Зелеными маркерами сопоставляется код, заключенный в блоки, с его ассемблерными эквивалентами. Следует обратить внимание на то, что блок кода с маркером 2 в ассемблерном представлении встречается дважды: в теле функции _tmain и в самой верхней функции. Последнее является дубликатом кода, заключенного в блок finally. Также следует обратить внимание на присутствие инструкции nop после инструкции вызова функции FailExecution в блоке кода с маркером 1. Данная инструкция также является заполнителем, как и в случаях с функциями шлюзов, функцией RtlpExecuteHandlerForException и функцией RtlpExecuteHandlerForUnwind. Если заполнитель будет отсутствовать, то при проверке инструкции на принадлежность к той или иной конструкции может быть сделано ошибочное предположение об ее принадлежности. В данном случае будет сделано ошибочное предположение о том, что инструкция вызова функции FailExecution не принадлежит блоку кода с маркером 1, т.к. функция RtlVirtualUnwind после раскрутки вернет адрес не на инструкцию вызова функции FailExecution, а на инструкцию сразу после нее. По этой причине компилятор добавляет заполнитель после инструкции вызова функции, если та в свою очередь является последней инструкцией в блоке. Если инструкция вызова является не последней инструкцией в блоке, тогда такого заполнителя не будет.
В левой части рисунка изображены структуры, которые сгенерирует компилятор. Вверху изображен элемент таблицы функций, ниже него изображена структура UNWIND_INFO, на которую ссылается этот элемент. Несмотря на то, что структура EXCEPTION_HANDLER не является частью структуры UNWIND_INFO, на рисунке она представлена как часть этой структуры, т.к. если она присутствует, то следует сразу после структуры UNWIND_INFO. Ниже структуры UNWIND_INFO изображено более подробное представление структуры EXCEPTION_HANDLER, ниже него изображено более подробное представление поля LanguageSpecificData этой структуры, в котором размещается структура SCOPE_TABLE. В самом низу последовательно изображены элементы ScopeRecord массива этой структуры. Все адреса в сгенерированных структурах являются относительными. Также эти адреса сопоставляются с адресами ассемблерного кода посредством уникальных номеров.
Более подробно стоит остановиться на элементах ScopeRecord массива. Элемент 0 описывает расположение блока с маркером 1. Поле HandlerAddress этого элемента содержит адрес функции, дублирующей код finally блока с маркером 2. Поле JumpAddress содержит 0, т.к. это finally блок. Элемент 1 описывает расположение блока с маркером 3. Поле HandlerAddress этого элемента содержит значение 1, что в свою очередь означает, что конструкция не имеет фильтра, и при возникновении исключения следует всегда передавать управление коду блока с маркером 4. Поле JumpAddress содержит адрес начала блока с маркером 4. Элемент 2 описывает расположение блока с маркером 5. Поле HandlerAddress этого элемента содержит адрес функции фильтра, код которого заключен в скобки после ключевого слова except. Ассемблерное представление функции фильтра располагается посередине, между функцией _tmain и функцией, дублирующей finally блок. Как изображено на рисунке, функция фильтра вызывает функцию ExceptionFilter, которая принимает указатель на структуру, описывающую контекст исключения. Поле JumpAddress содержит адрес начала блока с маркером 6.
Несмотря на то, что функция __C_specific_handler не представлена на рисунке, поле ExceptionHandlerAddress структуры EXCEPTION_HANDLER, содержит адрес этой функции. Эта функция будет вызвана операционной системой во время поиска обработчика исключения или во время раскрутки стека. Следовательно, реализация этой функции отвечает за интерпретацию структуры SCOPE_TABLE, вызов фильтров, вызов finally блоков и передачу управления except блокам.
Блок-схема функции __C_specific_handler изображена ниже, на рисунке 12.
В начале своей работы функция получает: адрес начала PE образа; относительный адрес инструкции, принадлежащий телу функции, для которой обработчик был вызван; указатель на структуру SCOPE_TABLE. В зависимости от выполняемой операции (поиск обработчика или раскрутка) работа функции варьируется.
Если выполняется поиск обработчика, то функция подготавливает структуру EXCEPTION_POINTERS, указатель на которую передается фильтрам соответствующих конструкций. Затем функция последовательно сканирует ScopeRecord элементы структуры SCOPE_TABLE и проверяет, принадлежит ли ранее полученный адрес инструкции какой-либо конструкции. Если принадлежит, тогда также проверяется, является ли конкретная конструкция try/except конструкцией, и, если нет, то она просто игнорируется, и проверяется следующий элемент. В противном случае вызывается фильтр этой конструкции. Если фильтр вернул EXCEPTION_CONTINUE_SEARCH, то данная конструкция игнорируется, и проверяется следующий элемент. Если фильтр вернул EXCEPTION_CONTINUE_EXECUTION, то функция завершает свою работу и возвращает ExceptionContinueExecution, чтобы указать операционной системе прекратить поиск обработчика и возобновить выполнение прерванного потока. Если фильтр вернул EXCEPTION_EXECUTE_HANDLER, то функция вызывает функцию RtlUnwind, которой в качестве кадра целевой функции указывается кадр функции, обработчик которой был вызван; в качестве адреса инструкции, с которой будет продолжено выполнение, передается адрес первой инструкции except блока; а также передается код исключения, который будет содержаться в RAX регистре сразу после передачи управления целевой функции. Функция RtlUnwind перед передачей управления последовательно вызовет обработчики всех промежуточных функций.
Если выполняется раскрутка, тогда функция ведет себя иначе. Сначала функция получает относительный адрес инструкции, с которой будет возобновлено выполнение. Затем функция последовательно сканирует ScopeRecord элементы структуры SCOPE_TABLE и проверяет, принадлежит ли ранее полученный адрес инструкции какой-либо конструкции. Если принадлежит, тогда также проверяется, является ли конкретная конструкция try/finally конструкцией. Если является, тогда вызывается ее обработчик, который по сути является дубликатом кода, заключенного в finally блок. Перед вызовом функция увеличит значение поля ScopeIndex структуры DISPATCHER_CONTEXT на единицу. Значение параметра AbnormalTermination при вызове этого обработчика всегда является TRUE. Следовательно, макрос AbnormalTermination всегда будет возвращать TRUE для этих блоков, вызванных таким образом. Для кода finally блока, располагающегося в теле самой функции, этот же макрос всегда будет возвращать FALSE. В этих случаях компилятор явно подставляет это значение. Иначе говоря, макрос AbnormalTermination возвращает TRUE только тогда, когда выполняется раскрутка. Практически, вследствие исключения. Если конструкция не является try/finally, тогда проверяется, не является ли адрес начала except блока адресом, с которого будет продолжено выполнение. И, если является, тогда работа функции завершается. Такая проверка необходима потому, что конструкция try/except может быть вложена в другую конструкцию try/finally, как это изображено ниже, на рисунке 13.
Как видно из рисунка, если такую проверку не выполнить, то во время раскрутки будет вызван finally блок внешней конструкции. А это недопустимо.
Если функция выполняет раскрутку для целевой функции, т.е. флаг EXCEPTION_TARGET_UNWIND установлен в поле ExceptionFlags структуры EXCEPTION_RECORD, тогда она выполняет дополнительную проверку перед вызовом обработчика. Суть проверки заключается в том, чтобы определить, не принадлежит ли адрес, с которого будет продолжено выполнение, самой конструкции, а не ее обработчику. И если принадлежит, тогда работа функции завершается. Подобная ситуация может быть только в случае использования в пределах finally блоков операторов goto, которые указывают за пределы этих блоков. Данная ситуация изображена ниже, на рисунке 14.
Как видно из рисунка, если такую проверку не выполнить, то во время раскрутки будет вызван finally блок внешней конструкции. А это недопустимо. Также, если функция не является целевой, тогда данная проверка не нужна.
Следует отметить, что в обоих случаях (и в случае поиска обработчика, и в случае выполнения раскрутки) сканирование элементов начинается не с начала, а с элемента, номер которого хранится в поле ScopeIndex структуры DISPATCHER_CONTEXT. Как это уже было отмечено, функция перед вызовом обработчика try/finally конструкции увеличивает значение поля ScopeIndex структуры DISPATCHER_CONTEXT на единицу. Ранее упоминалось, что если в процессе поиска обработчика или выполнения раскрутки будет обнаружен незавершенный процесс раскрутки, то продолжение поиска обработчика или выполнения раскрутки будет возобновлено с прерванного места. При этом обработчик функции, который породил исключение, будет вызван повторно, когда обработчики остальных функций вызваны не будут. В такой ситуации недопустимо, чтобы обработчики конструкций, которые уже были вызваны, оказались вызваны повторно. Эта ситуация изображена ниже, на рисунке 15.
На рисунке выше изображен стек вызова функций, слева от которого изображена стрелка направления его роста, а справа изображена часть кода функции Func1. Функции RtlDispatchException и RtlUnwindEx хоть и вызывают обработчики функций посредством функций RtlpExecuteHandlerForException и RtlpExecuteHandlerForUnwind, но в стеке вызовов эти функции для краткости не присутствуют. Функция Func1 вызвала функцию Func2, которая в свою очередь вызвала функцию Func3, которая породила исключение. Как только функция RtlDispatchException получала управление, она последовательно вызвала обработчики для функций: сначала Func3, затем Func2 и в конечном счете Func1. Обработчик функции Func1 нашел конструкцию, которая может обработать исключение, и вызвал функцию RtlUnwind для передачи управления обработчику этой конструкции. Функция RtlUnwind в свою очередь вызвала RtlUnwindEx, которая последовательно вызывала обработчики для функций сначала Func3, затем Func2 и в конечном счете Func1. Обработчик функции Func1 вызвал обработчик самого вложенного finally блока, который в свою очередь породил новое исключение. Как только функция RtlDispatchException получила управление, она последовательно вызывала обработчики предыдущих функций. Один из этих обработчиков окажется обработчиком функции RtlpExecuteHandlerForUnwind, которую вызывает функция RtlUnwindEx при передаче управления обработчику раскручиваемой функции. Обработчик функции RtlpExecuteHandlerForUnwind скопирует контекст раскрутки из RtlUnwindEx функции в функцию RtlDispatchException и, после возврата управления ей, поиск обработчика продолжится с того места, где была прервана раскрутка. Поскольку функция RtlUnwindEx ранее раскрутила функции Func3 и Func2, их обработчики вызываться не будут. Но поскольку функция Func1 породила исключение, она не была раскручена функцией, и, следовательно, ее обработчик будет вызван. Поскольку функция __C_specific_handler во время раскрутки увеличивает значение поля ScopeIndex структуры DISPATCHER_CONTEXT на единицу перед вызовом обработчика конструкции, то в скопированном контексте это поле будет равно 1. Следовательно, когда функция __C_specific_handler будет вызвана для функции Func1 вновь, поиск конструкции начнется с конструкции с индексом 1. Таким образом конструкция, породившая исключение, будет пропущена. Возобновление раскрутки выполняется аналогичным образом. Несмотря на то, что операционная система и компилятор абстрагированы друг от друга, наличие поля ScopeIndex в структуре DISPATCHER_CONTEXT является нарушением этой абстракции.
В завершение обсуждения try/except и try/finally конструкций стоит описать принцип работы макроса GetExceptionCode. Использование этого макроса возможно только в except блоках. Этот макрос читает содержимое регистра RAX, а при описании функции __C_specific_handler упоминалось, что передача управления конкретному except блоку выполняется посредством функции RtlUnwind, которая принимает параметр, значение которого будет записано в RAX регистре после передачи управления. Через этот параметр передается код возникшего исключения.
Также стоит описать принцип работы макроса GetExceptionInformation. Использование этого макроса возможно только в выражениях, заключенных в скобках после ключевого слова except. Поскольку в действительности выражение, заключенное в скобки (иначе говоря, фильтр), является отдельной функцией, которая принимает два параметра, данный макрос получает значение первого параметра. При описании функции __C_specific_handler упоминалось, что функция фильтра принимает два параметра, где первый параметр является указателем на структуру, описывающую исключение и состояние процессора в момент возникновения исключения.
3. Недостатки реализации механизма
Одним из недостатков данного механизма является использование в finally блоках операторов goto, которые указывают за пределы этих блоков. В этом случае компилятор C/C++, вместо прямой передачи управления, использует зарезервированную функцию _local_unwind, прототип которой изображен ниже, на рисунке 16.
Первый параметр функции принимает адрес кадра функции, до которой следует раскрутить стек, когда второй принимает адрес инструкции, которой следует передать управление после раскрутки. Сама функция должна быть реализована программистом. Также реализация этой функции экспортируется модулем ntdll.dll в пользовательском пространстве и модулем ntoskrnl.exe в пространстве ядра. Поставляемые Windows SDK и WDK содержат библиотеки, которые импортируют эту функцию из соответствующего модуля. Реализация самой функции очень простая, она вызывает функцию RtlUnwind, которой передает два своих параметра. Остальные параметры функции RtlUnwind при вызове обнулены.
Использование компилятором функции _local_unwind вместо прямой передачи управления в первую очередь связано с невозможностью передать управление в произвольное место функции в том случае, если finally блок был вызван в результате раскрутки. В таком случае передать управление в нужное место функции возможно только посредством нового процесса раскрутки. Такой подход имеет побочные эффекты. Оператор goto, в своей основе, передает прямое управление, когда раскрутка приводит к вызову finally блоков. Следовательно, до фактической передачи управления будут вызваны finally блоки, которые могут изменить контекст самой функции. Microsoft не рекомендует использовать оператор goto таким образом, а компилятор выдаст соответствующее предупреждение.
Заключение
В данной части статьи мы закончили обсуждение механизма обработки исключений. Необходимость в его реализации пришла из практики. И в первую очередь применяется в boot-time гипервизоре с целью упростить и ускорить разработку. В процессе реализации возникало множество проблем, которые были устранены, а сама статья, в первую очередь, нацелена на облегчение понимания тех, кто также заинтересован в подобных разработках.
- Блог компании Аладдин Р.Д.
- Open source
- C++
- Системное программирование
Programming stuff
Одной из новых возможностей языка C# 6.0 являются фильтры исключений.
Общая идея довольно простая: рядом с блоком catch появляется возможность задать некоторый предикат, который будет определять, будет ли вызван блок catch или нет.
Данный вариант синтаксиса доступен в публичной версии VS2015, но он будет изменен в финальной версии языка C#. Вместо if будет использоваться ключевое слово when.
Фильтр исключений логически эквивалентен условию в блоке catch с последующим пробросом исключения, в случае невыполнения условия. Но в случае полноценных фильтров исключений уровня CLR, порядок исполнения будет иным.
Генерация исключения в CLR происходит следующим образом:
1. Идет поиск ближайшего блока catch, который удовлетворяет типу генерируемого исключения.
2. Исполняется предикат фильтра исключения. Если предикат возвращает true, то данный блок catch считается подходящим и начинается раскрутка стека и выполнение всех блоков finally на пути от места генерации исключения к обработчику.
3. Если фильтр исключения возвращает false, то поиск подходящего блока catch продолжается.
Это значит, что порядок исполнения генерации и обработки исключений будет таким:
Сценарии использования
Выполнить некоторое действие до раскрутки стека: например, сохранить дамп падающего процесса до вызова блоков finally, закрытия файлов или освобождения блокировок и т.п.
Использовать более декларативную обработку исключений, когда одно и тоже исключение содержит еще и коды ошибок.
Эмуляция блока fault CLR.
У меня ни разу не возникало необходимости в фильтрах исключения для генерации более точных дампов, но команды Roslyn и TypeScript этим пользуются.
Второй сценарий использования связан с тем, что коды ошибок иногда проникают в исключения. SqlException содержит код ошибки, что может приводить к их императивному анализу, вместо использования разных блоков catch. Фильтры исключений здесь могут сильно помочь:
CLR содержит особый блок обработки исключений под названием fault – аналог блока finally, но который вызывается лишь в случае исключения. Этот блок не может обработать исключение и по его завершению исключение обязательно пробрасывается дальше.
С помощью фильтров исключений можно добавить этого же поведения:
Первый блок catch(Exception) можно рассматривать аналогом блока fault!
В этом случае всегда будет вызываться метод LogException, после чего начнется стандартный поиск нужного блока исключения. Так, в случае генерации InvalidOperationException, оно будет вначале залогировано, а обработано блоком catch(Exception).
Пример с логированием часто приводится в качестве одного из сценариев использования фильтров исключений. Тот же Мэдс Торгесен использует его в статье “New Features in C# 6”. Использовать фильтры исключений для этих целей вполне нормально, но нужно не забывать о правильном порядке блоков catch: первым блоком должен идти catch с фильтром, всегда возвращающим false, ПОСЛЕ которого должны располагаться все остальные блоки catch.
Опасности фильтров исключений
Основная опасность фильтров исключений кроется в ее природе. Поскольку фильтры вызываются до блоков finally, то они вызываются в момент, когда блокировки еще не отпущены, файлы не закрыты, транзакции не завершены и т.д. В большинстве случаев проблем не будет, но отпилить себе ногу все же можно.
Например, генерация исключения из блока lock может легко привести к дедлогку:
Если CanHandle попробует захватить блокировку хитрым образом, то мы получим взаимоблокировку:
Мало шансов столкнуться с взаимоблокировкой в таком простом виде, но более сложные сценарии все же могут привести к проблемам.
Фильтры исключений в F#
В каждой второй статье о фильтрах исключений в C# 6.0 говорится, что эта возможность есть также в VB.NET и в F#. К VB.NET претензий нет, а вот в F# фильтров исключений нет. Точнее как, они есть, но их нетJ.
let willThrow() =
try
printfn "throwing. "
failwith "Oops!"
finally
printfn "finally"
let check (ex: Exception) =
printfn "check"
true
let CheckFilters() =
try willThrow() with
| ex when check(ex) -> printfn "caught!"
()
Если запустить этот код, то вывод на экран будет таким:
Фильтры исключений в F# не используют фильтры исключений CLR – это обычное выражение сопоставления с образцом!
С чего начинается фильтр исключений в конструкции
За обработку исключения отвечает блок catch , который может иметь следующие формы:
catch < // выполняемые инструкции >
catch (тип_исключения) < // выполняемые инструкции >
Обрабатывает только те исключения, которые соответствуют типу, указаному в скобках после оператора catch. Например, обработаем только исключения типа DivideByZeroException:
try < int x = 5; int y = x / 0; Console.WriteLine($"Результат: "); > catch(DivideByZeroException)
catch (тип_исключения имя_переменной) < // выполняемые инструкции >
Обрабатывает только те исключения, которые соответствуют типу, указаному в скобках после оператора catch. А вся информация об исключении помещается в переменную данного типа. Например:
try < int x = 5; int y = x / 0; Console.WriteLine($"Результат: "); > catch(DivideByZeroException ex) < Console.WriteLine($"Возникло исключение "); >
Фильтры исключений
Фильтры исключений позволяют обрабатывать исключения в зависимости от определенных условий. Для их применения после выражения catch идет выражение when , после которого в скобках указывается условие:
catch when(условие)
В этом случае обработка исключения в блоке catch производится только в том случае, если условие в выражении when истинно. Например:
int x = 1; int y = 0; try < int result1 = x / y; int result2 = y / x; >catch (DivideByZeroException) when (y == 0) < Console.WriteLine("y не должен быть равен 0"); >catch(DivideByZeroException ex)
В данном случае будет выброшено исключение, так как y=0. Здесь два блока catch, и оба они обрабатывают исключения типа DivideByZeroException, то есть по сути все исключения, генерируемые при делении на ноль. Но поскольку для первого блока указано условие y == 0 , то именно этот блок будет обрабатывать данное исключение — условие, указанное после оператора when возвращает true.
int x = 0; int y = 1; try < int result1 = x / y; int result2 = y / x; >catch (DivideByZeroException) when (y == 0) < Console.WriteLine("y не должен быть равен 0"); >catch(DivideByZeroException ex)
В данном случае будет выброшено исключение, так как x=0. Условие первого блока catch — y == 0 теперь возвращает false. Поэтому CLR будет дальше искать соответствующие блоки catch далее и для обработки исключения выберет второй блок catch. В итоге если мы уберем второй блок catch, то исключение вообще не будет обрабатываться.