- 8
- 15
Всем привет, в этой статье я расскажу о том, как Windows обрабатывает исключения на уровне ядра. Эта тема необходима для тех, кто собирается заниматься низкоуровневой разработкой.
Содержание статьи:
1. Основные компоненты
2. KiDispatchException
3. SEH в ядре
4. Практическое использование обработчиков исключений
5. Вывод
Основные компоненты:
Ключевые структуры
Прежде чем разбирать конкретные компоненты, необходимо понять, какие структуры данных и механизмы участвуют в обработке исключений в ядре. Ниже приведены упрощённые определения - оригинальные имена полей соответствуют WDK.
EXCEPTION_RECORD - главная структура, где содержится информация об исключении:
- ExceptionCode - определяет тип исключения, например: STATUS_ACCESS_VIOLATION, STATUS_BREAKPOINT и так далее
- ExceptionAddress - адрес инструкции, где произошло исключение
CONTEXT - также немаловажная структура, где хранится полное состояние регистров на момент исключения.
Обработчик исключения может модифицировать CONTEXT, например, изменить Rip, чтобы перенаправить выполнение. Чтобы изменение вступило в силу, обработчик должен вернуть EXCEPTION_CONTINUE_EXECUTION - тогда ядро восстановит регистры из модифицированной структуры.
KTRAP_FRAME - структура, содержащая текущее состояние во время переключения из юзермод в ядро ( например, через прерывание, syscall, исключение ):
IDT ( Interrupt Descriptor Table )
Вся цепочка начинается с IDT. IDT - это таблица, в которой каждая запись содержит адрес обработчика для конкретного вектора прерывания или исключения. Когда процессор генерирует исключение, он использует номер вектора как индекс в IDT, извлекает адрес обработчика из соответствующего дескриптора и передаёт ему управление. Основные исключения и их обработчики:
- #PF ( Page Fault, вектор 14 ) - KiPageFault
- #GP ( General Protection, вектор 13 ) - KiGeneralProtectionFault
- #UD (I nvalid Opcode, вектор 6 ) - KiInvalidOpcodeFault
- #BP ( Breakpoint, вектор 3 ) - KiBreakpointTrap
- #DB ( Debug, вектор 1 ) - KiDebugTrapOrFault
Каждый из этих обработчиков сохраняет состояние в KTRAP_FRAME, выполняет первичный анализ исключения и передаёт управление в KiDispatchException.
KiDispatchException
KiDispatchException - это центральный диспетчер исключений ядра Windows. Любые исключения так или иначе проходят через неё. Прототип этой функции:
FirstChance определяет, первая ли это попытка обработать исключение. Если обработчики не справились, функция вызывается повторно с FirstChance = FALSE.
Логика работы KiDispatchException зависит от значения PreviousMode ( 0 = KernelMode, 1 = UserMode, получение его происходит из KTRAP_FRAME ).
Алгоритм при PreviousMode == KernelMode =>
1. Происходит исключение в ядре
2. Восстановление контекста из KTRAP_FRAME
3. First chance - отладчик: вызывается KdTrap. Если подключен отладчик ядра ( KdDebuggerEnabled == TRUE ), вызывается KdpTrap, иначе - KdpStub. Если отладчик обработал исключение - восстанавливается контекст и происходит возврат.
4. First chance - SEH: если отладчик не обработал исключение, вызывается RtlDispatchException. Эта функция ищет зарегистрированные обработчики исключений ( SEH ) и вызывает их. Каждый обработчик возвращает значение типа EXCEPTION_DISPOSITION:
- ExceptionContinueExecution - обработчик справился, выполнение продолжается с места исключения
- ExceptionContinueSearch - обработчик не справился, поиск продолжается по стеку
- ExceptionNestedException - при обработке возникло вложенное исключение, система пытается обработать его отдельно
Сама RtlDispatchException возвращает BOOLEAN: TRUE - исключение обработано, FALSE - не обработано.
5. Second chance - отладчик: если RtlDispatchException вернула FALSE, система повторно вызывает KdTrap ( уже с FirstChance = FALSE ), давая отладчику последний шанс обработать исключение.
6. BSOD: если и second chance через отладчик не помог, вызывается KeBugCheckEx с кодом KERNEL_MODE_EXCEPTION_NOT_HANDLED.
Алгоритм при PreviousMode == UserMode =>
1. Происходит исключение в юзермоде
2. Восстановление контекста из KTRAP_FRAME
3. First chance - отладчик ядра: исключение передаётся в KdTrap. Если отладчик ядра обработал - возврат и продолжение выполнения.
4. First chance - юзермодный отладчик: если отладчик ядра не обработал, исключение передаётся через DbgkForwardException юзермодному отладчику (если он подключен). Успех - возврат и продолжение выполнения.
5. Передача в юзермод: если все вышеперечисленные шаги закончились неудачно, система копирует EXCEPTION_RECORD и CONTEXT в стек юзермода, устанавливает Rip на ntdll!KiUserExceptionDispatcher и возвращает управление в юзермод. Там RtlDispatchException ищет обработчики (SEH/VEH) в юзермоде.
6. Second chance: если юзермодная обработка тоже не справилась, вызывается NtRaiseException, исключение передаётся через DbgkForwardException (уже на second chance). Если и это не помогло - процесс завершается.
SEH в ядре
SEH ( Structured Exception Handling ) - это механизм ( __try, __except и так далее ), который позволяет коду перехватывать исключения, не допуская BSOD.
На x64 информация об обработчиках хранится не в стеке ( как на x86 ), а в специальных таблицах в секции приложения. Ключевые структуры:
- RUNTIME_FUNCTION - запись в секции .pdata, описывающая диапазон адресов функции ( BeginAddress, EndAddress ) и ссылку на UNWIND_INFO
- UNWIND_INFO - содержит информацию для раскрутки стека и указатель на exception handler
- SCOPE_TABLE - таблица, описывающая блоки __try/__except внутри функции: диапазоны адресов и соответствующие обработчики
Поиск обработчика происходит через RtlDispatchException с таким алгоритмом:
1. Получение Rip из контекста
2. Поиск записи RUNTIME_FUNCTION в .pdata для функции, которая содержит этот Rip ( BeginAddress <= Rip && EndAddress > Rip )
3. Через RUNTIME_FUNCTION находится UNWIND_INFO. Если у функции есть exception handler - он вызывается. Обработчик может вернуть следующие значения:
- EXCEPTION_EXECUTE_HANDLER ( 1 ) - выполнение блока __except
- EXCEPTION_CONTINUE_SEARCH ( 0 ) - поиск дальше по стеку
- EXCEPTION_CONTINUE_EXECUTION ( -1 ) - продолжить выполнять код с места исключения
4. Если обработчик не найден - происходит раскрутка стека ( RtlUnwindEx ) на один фрейм и повторяется попытка поиска обработчика для вызывающей функции. Если весь стек пройден и исключение не было обработано, RtlDispatchException возвращает FALSE и цепочка продолжает идти к BSOD.
Практическое использование обработчиков исключений:
Обработчики исключений применяются практически везде, например:
1. Использование SEH для безопасного чтения памяти:
2. Перехват различных типов исключений для выполнения своей логики перед обработкой, например, перехват MmAccessFault для обработки #PF. Об этом более подробно расскажу в следующей статье.
Вывод
В этой статье мы разобрали полную цепочку обработки исключений в Windows, начиная с момента генерации исключения процессором, заканчивая BSOD или завершением процесса. Понимание обработки исключений позволяет писать стабильные драйверы, анализировать работу защитных механизмов, а также работать в ядре более безопасно. Всем хорошего дня.
Содержание статьи:
1. Основные компоненты
2. KiDispatchException
3. SEH в ядре
4. Практическое использование обработчиков исключений
5. Вывод
Основные компоненты:
Ключевые структуры
Прежде чем разбирать конкретные компоненты, необходимо понять, какие структуры данных и механизмы участвуют в обработке исключений в ядре. Ниже приведены упрощённые определения - оригинальные имена полей соответствуют WDK.
EXCEPTION_RECORD - главная структура, где содержится информация об исключении:
С++:
typedef struct _EXCEPTION_RECORD {
NTSTATUS ExceptionCode; // Код исключения
ULONG ExceptionFlags; // Флаги
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress; // Адрес инструкции, вызвавшей исключение
ULONG NumberParameters; // Количество параметров
ULONGLONG ExceptionInformation[ EXCEPTION_MAXIMUM_PARAMETERS ];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;
- ExceptionAddress - адрес инструкции, где произошло исключение
CONTEXT - также немаловажная структура, где хранится полное состояние регистров на момент исключения.
C++:
typedef struct _CONTEXT {
ULONGLONG Rax;
ULONGLONG Rcx;
ULONGLONG Rdx;
ULONGLONG Rbx;
ULONGLONG Rsp;
ULONGLONG Rbp;
ULONGLONG Rsi;
ULONGLONG Rdi;
ULONGLONG R8;
ULONGLONG R9;
ULONGLONG R10;
ULONGLONG R11;
ULONGLONG R12;
ULONGLONG R13;
ULONGLONG R14;
ULONGLONG R15;
ULONGLONG Rip;
// Дальше идут XMM регистры, флаги, DR
// ...
} CONTEXT, *PCONTEXT;
KTRAP_FRAME - структура, содержащая текущее состояние во время переключения из юзермод в ядро ( например, через прерывание, syscall, исключение ):
C++:
struct _KTRAP_FRAME
{ // ...
CHAR PreviousMode;
ULONGLONG Rax;
ULONGLONG Rcx;
ULONGLONG Rdx;
ULONGLONG R8;
ULONGLONG R9;
ULONGLONG R10;
ULONGLONG R11;
// ...
struct _M128A Xmm0;
struct _M128A Xmm1;
struct _M128A Xmm2;
struct _M128A Xmm3;
struct _M128A Xmm4;
struct _M128A Xmm5;
// ...
ULONGLONG Dr0;
ULONGLONG Dr1;
ULONGLONG Dr2;
ULONGLONG Dr3;
ULONGLONG Dr6;
ULONGLONG Dr7;
// ...
ULONGLONG TrapFrame;
ULONGLONG Rbx;
ULONGLONG Rdi;
ULONGLONG Rsi;
ULONGLONG Rbp;
union
{
ULONGLONG ErrorCode;
ULONGLONG ExceptionFrame;
ULONGLONG TimeStampKlog;
};
ULONGLONG Rip;
};
Вся цепочка начинается с IDT. IDT - это таблица, в которой каждая запись содержит адрес обработчика для конкретного вектора прерывания или исключения. Когда процессор генерирует исключение, он использует номер вектора как индекс в IDT, извлекает адрес обработчика из соответствующего дескриптора и передаёт ему управление. Основные исключения и их обработчики:
- #PF ( Page Fault, вектор 14 ) - KiPageFault
- #GP ( General Protection, вектор 13 ) - KiGeneralProtectionFault
- #UD (I nvalid Opcode, вектор 6 ) - KiInvalidOpcodeFault
- #BP ( Breakpoint, вектор 3 ) - KiBreakpointTrap
- #DB ( Debug, вектор 1 ) - KiDebugTrapOrFault
Каждый из этих обработчиков сохраняет состояние в KTRAP_FRAME, выполняет первичный анализ исключения и передаёт управление в KiDispatchException.
KiDispatchException
KiDispatchException - это центральный диспетчер исключений ядра Windows. Любые исключения так или иначе проходят через неё. Прототип этой функции:
C++:
void KiDispatchException(
PEXCEPTION_RECORD ExceptionRecord,
PKEXCEPTION_FRAME ExceptionFrame,
PKTRAP_FRAME TrapFrame,
KPROCESSOR_MODE PreviousMode,
BOOLEAN FirstChance
);
Логика работы KiDispatchException зависит от значения PreviousMode ( 0 = KernelMode, 1 = UserMode, получение его происходит из KTRAP_FRAME ).
Алгоритм при PreviousMode == KernelMode =>
1. Происходит исключение в ядре
2. Восстановление контекста из KTRAP_FRAME
3. First chance - отладчик: вызывается KdTrap. Если подключен отладчик ядра ( KdDebuggerEnabled == TRUE ), вызывается KdpTrap, иначе - KdpStub. Если отладчик обработал исключение - восстанавливается контекст и происходит возврат.
4. First chance - SEH: если отладчик не обработал исключение, вызывается RtlDispatchException. Эта функция ищет зарегистрированные обработчики исключений ( SEH ) и вызывает их. Каждый обработчик возвращает значение типа EXCEPTION_DISPOSITION:
- ExceptionContinueExecution - обработчик справился, выполнение продолжается с места исключения
- ExceptionContinueSearch - обработчик не справился, поиск продолжается по стеку
- ExceptionNestedException - при обработке возникло вложенное исключение, система пытается обработать его отдельно
Сама RtlDispatchException возвращает BOOLEAN: TRUE - исключение обработано, FALSE - не обработано.
5. Second chance - отладчик: если RtlDispatchException вернула FALSE, система повторно вызывает KdTrap ( уже с FirstChance = FALSE ), давая отладчику последний шанс обработать исключение.
6. BSOD: если и second chance через отладчик не помог, вызывается KeBugCheckEx с кодом KERNEL_MODE_EXCEPTION_NOT_HANDLED.
Алгоритм при PreviousMode == UserMode =>
1. Происходит исключение в юзермоде
2. Восстановление контекста из KTRAP_FRAME
3. First chance - отладчик ядра: исключение передаётся в KdTrap. Если отладчик ядра обработал - возврат и продолжение выполнения.
4. First chance - юзермодный отладчик: если отладчик ядра не обработал, исключение передаётся через DbgkForwardException юзермодному отладчику (если он подключен). Успех - возврат и продолжение выполнения.
5. Передача в юзермод: если все вышеперечисленные шаги закончились неудачно, система копирует EXCEPTION_RECORD и CONTEXT в стек юзермода, устанавливает Rip на ntdll!KiUserExceptionDispatcher и возвращает управление в юзермод. Там RtlDispatchException ищет обработчики (SEH/VEH) в юзермоде.
6. Second chance: если юзермодная обработка тоже не справилась, вызывается NtRaiseException, исключение передаётся через DbgkForwardException (уже на second chance). Если и это не помогло - процесс завершается.
SEH в ядре
SEH ( Structured Exception Handling ) - это механизм ( __try, __except и так далее ), который позволяет коду перехватывать исключения, не допуская BSOD.
На x64 информация об обработчиках хранится не в стеке ( как на x86 ), а в специальных таблицах в секции приложения. Ключевые структуры:
- RUNTIME_FUNCTION - запись в секции .pdata, описывающая диапазон адресов функции ( BeginAddress, EndAddress ) и ссылку на UNWIND_INFO
- UNWIND_INFO - содержит информацию для раскрутки стека и указатель на exception handler
- SCOPE_TABLE - таблица, описывающая блоки __try/__except внутри функции: диапазоны адресов и соответствующие обработчики
Поиск обработчика происходит через RtlDispatchException с таким алгоритмом:
1. Получение Rip из контекста
2. Поиск записи RUNTIME_FUNCTION в .pdata для функции, которая содержит этот Rip ( BeginAddress <= Rip && EndAddress > Rip )
3. Через RUNTIME_FUNCTION находится UNWIND_INFO. Если у функции есть exception handler - он вызывается. Обработчик может вернуть следующие значения:
- EXCEPTION_EXECUTE_HANDLER ( 1 ) - выполнение блока __except
- EXCEPTION_CONTINUE_SEARCH ( 0 ) - поиск дальше по стеку
- EXCEPTION_CONTINUE_EXECUTION ( -1 ) - продолжить выполнять код с места исключения
4. Если обработчик не найден - происходит раскрутка стека ( RtlUnwindEx ) на один фрейм и повторяется попытка поиска обработчика для вызывающей функции. Если весь стек пройден и исключение не было обработано, RtlDispatchException возвращает FALSE и цепочка продолжает идти к BSOD.
Практическое использование обработчиков исключений:
Обработчики исключений применяются практически везде, например:
1. Использование SEH для безопасного чтения памяти:
C++:
NTSTATUS read_buffer( void* address, void* buffer, size_t size ) {
__try {
RtlCopyMemory( buffer, address, size );
}
__except ( EXCEPTION_EXECUTE_HANDLER ) {
return GetExceptionCode( );
}
return STATUS_SUCCESS;
}
2. Перехват различных типов исключений для выполнения своей логики перед обработкой, например, перехват MmAccessFault для обработки #PF. Об этом более подробно расскажу в следующей статье.
Вывод
В этой статье мы разобрали полную цепочку обработки исключений в Windows, начиная с момента генерации исключения процессором, заканчивая BSOD или завершением процесса. Понимание обработки исключений позволяет писать стабильные драйверы, анализировать работу защитных механизмов, а также работать в ядре более безопасно. Всем хорошего дня.