С учётом большого количества топиков, в которых происходит гадание на том где и почему приложение падает создал этот топик. Это имеет смысл для ошибок возникающих в юзермоде. Итак матчасть. Когда при исполнении инструкции процессор не может её исполнить, по причине того, что некоторые внутренние проверки не прошли, например сегмент не доступен для записи он прерывает исполнение программы. Это исключительная ситуация именуется исключением. Процессор выполняет переход в зависимости от причины исключения на обработчик, определённый в одном из шлюзов дескриптороной таблицы прерываний. При этом происходит смена кольца защиты на нулевое(релоад кодового селектора), перезагрузка стека из сегмента состояния задачи на ядерный, сохранение в ядерном стеке регистра флагов EFlags, адреса инструкции вызвавшей исключение, селектора стека и указателя на стек. На стороне ядра обработчики именуются KiTrapXX, соответственно коду исключения(имена можно увидеть тут http://sasm.narod.ru/docs/pm/pm_app/app_1/app_1.htm . Ядро выполняет некоторые действия по диспетчеризации исключения, в частности вызывает ядерный отладчик если он активен, подготавливает фрейм, который содержит описание исключения и передаёт управление обратно в юзермод. Для любого исключения поток возвращается в юзермод на одну точку, она экспотрируется модулем ntdll и носит имя KiUserExceptionDispatcher. Проще говоря при возникновении исключения поток начинает исполнять диспетчер исключений. Далее можно забыть про SEH и остальное. При входе в диспетчер исключений в стеке находится структура, которая полностью описывает где и почиму произошло исключение. Регистр Esp указывает на структуру: Код (Text): EXCEPTION_POINTERS struct ExceptionRecord PEXCEPTION_RECORD ? ContextRecord PCONTEXT ? EXCEPTION_POINTERS ends Первое поле содержит указатель на описание исключения, это структура EXCEPTION_RECORD: Код (Text): EXCEPTION_RECORD struxt ExceptionCode NTSTATUS ? ; 0000h ExceptionFlags DWORD ? ; 0004h ExceptionRecord PVOID ? ; 0008h ExceptionAddress PVOID ? ; 000Ch NumberParameters DWORD ? ; 0010h ExceptionInformation DWORD EXCEPTION_MAXIMUM_PARAMETERS dup(?) ; 0014h EXCEPTION_RECORD ends Второй параметр структуры EXCEPTION_POINTERS это ContextRecord - указатель на полный контекст потока(содержание всех его регистров) на момент возникновения исключения. Благодаря этой структуре поток может вернуться на инструкцию, которая вызвала исключение, полностью восстановив состояние процессора. Структура CONTEXT содержащая полное состояние процессора на момент возникновения исключения: Код (Text): CONTEXT struct ContextFlags DWORD ? ; 0000h ;CONTEXT_DEBUG_REGISTERS regDr0 DWORD ? ; 0004h regDr1 DWORD ? ; 0008h regDr2 DWORD ? ; 000Ch regDr3 DWORD ? ; 0010h regDr6 DWORD ? ; 0014h regDr7 DWORD ? ; 0018h ;CONTEXT_FLOATING_POINT FloatSave FLOATING_SAVE_AREA <> ; 001Ch ;CONTEXT_SEGMENTS regSegGs DWORD ? ; 008Ch regSegFs DWORD ? ; 0090h regSegEs DWORD ? ; 0094h regSegDs DWORD ? ; 0098h ;CONTEXT_INTEGER regEdi DWORD ? ; 009Ch regEsi DWORD ? ; 00A0h regEbx DWORD ? ; 00A4h regEdx DWORD ? ; 00A8h regEcx DWORD ? ; 00ACh regEax DWORD ? ; 00B0h ;CONTEXT_CONTROL regEbp DWORD ? ; 00B4h regEip DWORD ? ; 00B8h regSegCs DWORD ? ; 00BCh regEFlags DWORD ? ; 00C0h regEsp DWORD ? ; 00C4h regSegSs DWORD ? ; 00C8h ;CONTEXT_EXTENDED_REGISTERS ExtendedRegisters BYTE MAXIMUM_SUPPORTED_EXTENSION dup(?) ; 00CCh CONTEXT ends Поле ContextFlags это набор флагов, определяющих какие группы регистров включены в эту структуру, соответственно константы CONTEXT_XX. При входе в диспетчер исключений сохраняется полный контекст потока, тоесть все поля валидны. Структура EXCEPTION_RECORD содержит причину исключения. Поле ExceptionCode содержит код исключения, эти коды определены в файле из сурцов винды ntstatus.h Поле ExceptionFlags содержит флажки, например может ли исключение быть обработано, обычно это поле можно игнорировать. ExceptionAddress содержит адрес инструкции, которая вызвала исключение. NumberParameters это число параметров следующих далее, они содержат более подробную информацию об исключении. Наиболее распространённое исключение - это исключение по доступу к памяти, имеет код STATUS_ACCESS_VIOLATION. Для этого исключения содержится дополнительная информация - адреc к которому произошло обращение. Имея всю эту информацию мы можем полностью идентифицировать исключение. Практически необходимо только два поля структуры EXCEPTION_RECORD - это адрес инструкции вызвавшей исключение и его код. Остальное можно извлечь из структуры контекст(все регистры там). Практический пример. На экспортируемую модулем ntdll процедуру KiUserExceptionDispatcher ставится точка останова. Когда возникает исключение произойдёт останов при входе в диспетчер исключений. Тут мы и выполняем анализ стекового фрейма. Допустим исполняется инструкция mov dword ptr ds:[0],eax Эта инструкция вызывает исключение и поток возвращается на диспетчер исключений: Отсюда мы находим инструкцию, которая вызвала исключение: Далее выполняется анализ кода, где находится эта инструкция. Определяется почему и как оказался не валидный адрес. Обычно удобно использовать стековые фреймы, который формируются при входе в процедуры для выделения памяти под локальные переменные. Используется тот факт, что при входе в диспетчер исключений не изменяется регистр Ebp, который эти фреймы и адресует. Тоесть мы смотрим что находится в стеке по адресу, на который указывает регистр Ebp - нормально там цепочка фреймов, где каждый элемент содержит адрес возврата из процедуры и указатель на следующий фрейм. Тоесть по адресу (Ebp + 4) находится указатель на адрес возврата из процедуры, в которой произошло исключение. Это позволяет быстро перейти к нужной процедуре. В большинстве случаев этого будет достаточно для определения исключения. В более сложных случаях необходимо рассмотреть текущий контекст потока при входе в диспетчер исключений, но не буду описывать это тут. Добавля что удобно использовать смещения от начала структур для определения необходимых регистров. Также в случае если селекторы отличны от нормальных, тоесть используется LDT необходимо учитывать базы сегментов. Довольно чато распространённая ситуация - при возникновении ошибки выводится окно с её описанием. Для идентификации причины возникновения данной ошибки, а она не всегда вызывается исключениями, следует выполнить следующее. Мессагу боксит сервис NtRaiseHardError, он вызывается ntdll так и kernel32, весьма часто при ошибках в загрузчике. Следует установить точку останова на данный сервис и рассмотреть цепочку фреймов, определив причину вызова данного сообщения. Надеюсь это краткое описание многим поможет.
Хорошая статья. Вот хидер (не мой но пользуюсь часто + кое что подправил) с двумя ф-циями для Си с реализацией вывода ошибки в виде строки (как это умеет Syser), а не в виде кода NTSTATUS. Может для отладки кому пригодится.http://depositfiles.com/files/mommloixs(в этом же архиве таблица NTSTATUS->DOS_ERROR->WINDOWS ERROR MESSAGE)
RET Есть функция RtlNtStatusToDosErrorNoTeb. Для преобразования NTSTATUS кода ошибки в её имя и наоборот много либ есть, у Бартона в блоге вроде видел, да и сам писал когдато. Вот в аттаче(не пикод, но переделать минутное дело). Да и основные коды исключений следует знать, там их не много.
ULONG WINAPI RtlNtStatusToDosError(NTSTATUS) и ULONG WINAPI RtlNtStatusToDosErrorNoTeb(NTSTATUS) возвращают DOS-код, а не символьное имя...Кстати, а чем они отличаются друг от друга (я лично не знаю)? В твоем примере тоже используется выборка (см. NtStatusTable.inc и размер приложения). В нативе и ядре нет прямой функции выдающей символьное имя ошибки, но есть цепочка, позволяющая получить то что мы называем "сообщение об ошибке виндовс": RtlNtStatusToDosError -> RtlFindMessage -> RtlFormatMessage ...
RET Я про DOS-код и говорил. Разница что в теб не пишет первая. Имя не нужно, я этим никогда не пользуюсь. Достаточно открыть ntstatus.h и найти описание ошибки, там и имя будет. Топик этот чтобы показать как выполнить анализ исключения и только.
Booster Просто там всё. Восстановить состояние процессора, подкорректировать как нужно контекст в стеке и выполнить NtContinue, ну или отдать управление далее диспетчеру исключений, он сам состояние восстановит. Для того примера - можно селектор перезагрузить в Ds, вариантов множество, смотря какая ошибка и как её обработать нужно.
Clerk Только я не совсем понимаю всё это. У меня когда отладчик ловит исключение, то он показывает инструкцию на которой произошёл креш, и в стеке нету никаких EXCEPTION_POINTERS.
Booster Если поставить точку останова на KiUserExceptionDispatcher, то можно ее поймать как раз в тот момент, когда она получает управление. Тогда можно увидеть структуру.
Booster Ну, для поиска ошибки обычно достаточно адреса падения. А после декомпилция (если написано не на ассемблере) и поиск причины. Вообще, я думал интерес больше академический .
Mika0x65 Не ну конечно интересно знать как всё устроено под капотом, но всё равно не понимаю при чём здесь поиск места падения, если отладчики это место прекрасно показывают без всяких плясок с бубном.
допустим у тя есть прога, которую ты даешь пользователям. и вот вдруг у когото она начинает падать. ты ж не будешь пользователю говорить "установите отладчек, жмите ф9, посморите циферке с букавкаме". а в прогу ты вшил простенький код, который инфу об ошибке пишет в файл. пользователю тогда достаточно прислать тебе файл(ну или прога сама пришлет)
Booster Причём тут отладчик, в большинстве случаев останов на диспетчер исключений единственный вариант нормально исполнить анализ. Я всегда так исключения в юзермоде разбираю.
Без отладчика, при работе с сторонними приложениями не обойтись, но процесс можно НА НАЧАЛЬНОЙ стадии в прикладном режиме автоматизировать заинжектив свою либу, перехватывающую в ntdll KiUserExceptionDispatcher, и выводящую содержимое отладочной структуы _EXCEPTION_POINTERS и иже сней. (Это думаю будет полезно, если потом проанализировав все в отладчике, дописать в эту библиотеку код изменяющий все в лушую сторону, как говорит Clerk http://wasm.ru/forum/viewtopic.php?pid=309914#p309914) И бывшее когда-то нерабочее чужое приложение станет работать, например не работает после обновления винды. Я сам сталкивался с таким после установки "переделанного" DirectX10 на XP, некоторые программы перестали работать на стадии загрузки, пришлось все править вручную). P>S>Может Clerk еще доходчиво напишет как работает Syser Debug (в плане ядерных точек останова. Например, постановка точки останова на какую либо API -вызывает останов на этом адресе во всех процессах. Это махинация с регистрами процессора или что?).
Диспетчерезиция исключения это системный механизм, как и прочие он имеет свою модель вызова. Не имеет смысла рассматривать этот механизм как запредельный для приложения. Вызов диспетчера исключений следует рассматривать как просто передачу управления на диспетчер, не более. Это главная точка, с которой начинает выполняться SEH и пр. Как например диспетчеризация APC рассматривается как передачу управления на какойто код, также и для исключений. Простой пример - юзермодные дебаггеры сбрасывают отладочные регистры, код выполняющий манипуляции с ними продебажить средствами отладчика не получится. Отладочный порт это кривой функционал, не многие задачи с его помощью решаются. Скока раз приходилось зацикливать диспетчер исключений, ибо юзермодный дебуггер был бесполезен. В случае ядерного отладчика - описанное единственный нормальный способ просмотреть инфу об исключении. На ISR брякаться или анализ TRAP_FRAME для исключений, вызванных в третим кольце защиты это пустая трата времени, делается всё куда проще и быстрее. В аттаче элитный док с вирустека, описывающий модель вызова, может кому пригодится. Да и вообще не следует надеятся на помощь дебаггера, это не на столько гибкий инструмент, чтобы решать подобнае задачи. RET Сисер ISR перехватывает для #BP, тем самым когда возникает исключение он получает управление.
Clerk А VEH тоже работает через KiUserExceptionDispatcher? А как у этого механизма с мультивиндовостью? Например если на его основе сделать свою версию SEH - будет работать в Висте, семёрке, 9х?