Диспетчеризация исключений
В отличие от прерываний, которые могут происходить в любое время, исключения являются условиями, являющимися непосредственными результатами выполнения запущенной программы. В Windows используется средство, известное как структурная обработка исключения (structured exception handling), которое позволяет приложениям получить управление при возникновении исключения.
Затем приложение может исправить ситуацию и вернуться к месту возникновения исключения, вернуть назад указатель стека (завершая тем самым выполнение подпрограммы, выдавшей исключение) или объявить системе, что исключение не распознано и система должна продолжить поиск обработчика исключения, который может справиться с исключением. В данном разделе предполагается, что вы знакомы с основными понятиями, положенными в основу структурной обработки исключений Windows.
Если это не так, то перед тем как продолжить чтение вам, нужно прочитать обзор справочной документации по Windows API в Windows SDK или главы с 23 по 25 в книге Джеффри Рихтера (Jeffrey Richter) и Кристофера Назара (ChristopheNasarre) «WindowsviaC/C++». Следует иметь в виду, что, несмотря на доступность обработки исключений средствами расширений языка программирования (например, конструкции __try в Microsoft Visual C++), данный механизм является системным и, следовательно, не имеет отношения к какому-то определенному языку.
На процессорах x86 и x64 все исключения имеют предопределенные номера прерываний, которые напрямую соотносятся с записью в IDT-таблице, указывающей на обработчик системного прерывания для конкретного исключения.
В таблице показывается исключения, определенные для x86-системы и заданные для них номера прерываний. Поскольку, как уже говорилось, первые записи в IDT-таблице используются для исключений, аппаратные прерывания назначаются тем записям, которые следуют в этой таблице позже.
Все исключения, кроме самых простых, разрешаемых с помощью обработчика системных прерываний, обслуживаются модулем ядра, который называется диспетчером исключений. Задачей этого диспетчера является поиск подходящего обработчика исключения. К исключениям, независящим от архитектуры и определяемым ядром, можно отнести нарушения доступа к памяти, целочисленные деления на нуль, целочисленные переполнения, исключения при работе с числами с плавающей точкой и контрольные точки отладчика. Для получения полного перечня исключений, независящих от архитектуры, нужно обратиться к справочной документации по Windows SDK.
Ядро перехватывает и обрабатывает некоторые из этих исключений прозрачно для пользовательских программ. Например, встреча контрольной точки при выполнении отлаживаемой программы приводит к выдаче исключения, которое ядро обрабатывает путем вызова отладчика. Ядро обрабатывает некоторые другие исключения, возвращая вызывающей программе код неудачного состояния.
Некоторые исключения могут передаваться в нетронутом виде назад в пользовательский режим. Например, некоторые виды ошибок обращения к памяти или арифметическое переполнение генерируют исключение, не обрабатываемое операционной системой. Для работы с этими исключениями 32-разрядные приложения могут устанавливать фреймовые обработчики исключений.
Понятие фреймовые относится к обработчикам исключений, связанным с активацией конкретной процедуры. При вызове процедуры в стек помещается стековый фрейм, представляющий ее активацию. Стековый фрейм может иметь связанный с ним один или несколько обработчиков исключений, каждый из которых защищает конкретный блок кода в исходной программе. При выдаче исключения ядро осуществляет поиск обработчика исключения, связанного с текущим стековым фреймом.
Если таковой отсутствует, ядро ищет обработчик исключения, связанный с предыдущим стековым фреймом, и так далее, до тех пор, пока не будет найден фреймовый обработчик исключений. Если обработчик исключений не найден, ядро вызывает свои собственные обработчики исключений, используемые по умолчанию.
Для 64-разрядных исключений в структурированной обработке исключений фреймовые обработчики не используются. Вместо этого во время компиляции в образ встраивается таблица обработчиков. Ядро ищет обработчики, связанные с каждой функцией, и следует в целом тому же алгоритму, который был описан для 32-разрядного кода.
Структурированная обработка исключений активно используется в самом ядре, поэтому оно запросто может проверить, можно ли безопасно обращаться к указателям из пользовательского режима для доступа по чтению или по записи.
Драйверы могут воспользоваться такой же технологией при работе с указателями, отправленными вместе с кодами управления ввода-вывода (IOCTL-кодами).
Еще один механизм обработки исключений называется векторной обработкой исключений. Этот метод может быть использован только приложениями пользовательского режима. Дополнительную информацию о нем можно найти в Windows SDK или в библиотеке MSDN.
При выдаче исключения, либо явной, со стороны программного обеспечения, либо неявной, инициированной оборудованием, цепочка событий начинается в ядре. Аппаратура центрального процессора передает управление обработчику системных прерываний ядра, который создает фрейм системного прерывания (так же, как при возникновении прерывания). Фрейм системного прерывания (trap frame) позволяет системе возобновить выполнение с того же места, на котором оно было остановлено, если исключение будет разрешено. Обработчик системного прерывания также создает запись исключения, в которой содержится причина исключения и другая, относящаяся к нему информация.
Если исключение возникло в режиме ядра, диспетчер исключений просто вызывает процедуру, локализующую фреймовый обработчик исключения.
Поскольку необработанные исключения режима ядра считаются фатальными ошибками операционной системы, можно предположить, что диспетчер всегда находит обработчик исключения. Тем не менее некоторые системные прерывания не приводят к обработчику исключения, потому что ядро всегда считает такие ошибки фатальными. Это относится к тем ошибкам, которые могли быть вызваны только критическими сбоями во внутреннем коде ядра или серьезными несогласованностями в коде драйвера, возникающими только благодаря преднамеренным, низкоуровневым системным изменениям, за которые драйверы не должны нести ответственность. Такие фатальные ошибки приводят к сбою проверки с кодом UNEXPECTED_KERNEL_MODE_TRAP.
Если исключение выдается в пользовательском режиме, диспетчер исключений выполняет более сложные действия. Как будет показано, у подсистемы Windows есть порт отладки (который фактически является объектом отладчика) и порт исключения для получения уведомлений пользовательского режима в процессах Windows1. Как показано на рисунке, ядро использует эти порты в своей обработке исключений, используемой по умолчанию.
Обычными источниками исключений являются контрольные точки отладчика.
Поэтому первым делом диспетчер исключений проверяет, не связан ли процесс, выдавший исключение, с процессом отладки. Если связан, диспетчер исключений отправляет сообщение объекта отладчика объекту отладки, связанному с процессом. Внутри себя система обращается к объекту отладки как к «порту» для совместимости с программами, которые могут зависеть от поведения в Windows 2000, где используется не объект отладки, а LPC-порт.
Если у процесса нет подключенного к нему процесса отладчика или если отладчик не обрабатывает исключение, диспетчер исключений переключается в пользовательский режим, копирует фрейм системного прерывания в пользовательский стек, отформатированный как структура данных CONTEXT (документация по которой есть в Windows SDK), и вызывает процедуру для поиска структурированного или векторного обработчика исключения. Если таковой не будет найден или если исключение ничем не обрабатывается, диспетчер исключений переключается обратно в режим ядра и снова вызывает отладчик, чтобы дать возможность пользователю провести дополнительную отладку программы.
Это называется повторным уведомлением.
Если отладчик не запущен и не найдено ни одного обработчика исключения пользовательского режима, ядро отправляет сообщение порту исключения, связанному с процессом потока. Этот порт исключения, если таковой имеется, был зарегистрирован подсистемой окружения, контролирующей этот поток. Порт исключения дает подсистеме окружения, которая предположительно прослушивает порт, возможность транслировать исключение в характерный для этого окружения сигнал или исключение. Например, когда подсистема для приложений UNIX — Subsystem for UNIX Applications — получает сообщение от ядра о том, что один из его потоков сгенерировал исключение, подсистема Subsystem for UNIX Applications отправляет сигнал в UNIX-стиле тому потоку, который стал причиной исключения.
Но если ядро заходит столь далеко в обработке исключения, и подсистема это исключение не обрабатывает, ядро отправляет сообщение на общесистемный порт ошибки, который подсистема времени выполнения клиент/сервер (Client/ServerRun-TimeSubsystem, Csrss) использует для системы отчета об ошибках Windows Error Reporting (WER) и запускает обработчик исключений, используемый по умолчанию. Этот обработчик просто завершает тот процесс, чей поток стал причиной исключения.