Практические советы по восстановлению системы в боевых условиях

Автор: (c)Крис Касперски

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

Введение

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

Вообще-то, хорошо отлаженная система, базирующаяся на ОС типа FreeBSD (или подобной ей) способна без сбоев работать годами, не требуя к себе совершенно никакого внимания. Системы, построенные на базе Windows NT, этим, увы, похвастаться не могут и для достижения сколь-нибудь стабильной работы за ними приходится постоянно ухаживать.

Аппаратное обеспечение, собираемое на коленках в ближайшем подвале, также не отличается высокой надежностью, а отличить качественную подделку от оригинала по внешним признакам достаточно трудно. На просторах России свободно продаются отбракованные чипы, левым путем добытые у производителей и выдаваемые за настоящие. Кстати, многие из именитых производителей грешат передачей своих торговых марок третьим фирмам, выпускающим довольно посредственное оборудование, но продающих его по "брэндовским" ценам. Яркий тому пример - пишущий привод TEAC 552E, к которому фирма TEAC вообще не имеет никакого отношения. Про материнские платы и модули памяти вообще говорить не стоит. Их клепают все, кому не лень и многие модели вообще не работают, кое-как запускаясь на пониженных таймингах и частотах.

Словом, если сбой старушки БЭСМ-6 был настоящим ЧП, то зависание современного сервера - вполне обычное дело, воспринимаемое администраторами как неизбежное зло. Эта статья не убережет вас ни от критических ошибок приложений, ни от отказа оборудования, но по крайней мере, научит быстро и безошибочно находить их источник. Речь пойдет преимущественно о Windows NT и производных от нее системах (Windows 2000, Windows XP), хотя поклонники UNIX'a также найдут здесь немало интересного.

Аппаратная часть

Вот два основных аппаратных виновника нестабильной работы системы - оперативная память и блок питания. Рассмотрим их поподробнее, отмечая особенности взаимодействия с памятью в современных чипсетах, таких как Intel 875P и подобных ему.

Тесная связь между программным и аппаратным обеспечением затрудняет деление статьи на две равные части, поскольку ряд сбоев системы (и пресловутых голубых экранов смерти - в том числе) вызван отнюдь не алгоритмическими ошибками, а неисправностью железа. Но на начальном этапе анализа "голубого экрана смерти" (далее по тексту просто голубого экрана) мы не можем надежно установить его источник и чтобы не описывать одни и те же методики дважды, условимся относить все критические ошибки системы к программной среде. В действительности же это не вызывает никакого противоречия, поскольку с аппаратными ошибками приходится бороться и программными средствами (помните известное: "как нематериальная душа возвращается в тело в результате материальных действий врача?).

Оперативная память

Оперативная память относится к одному из наименее надежных компонентов вычислительной системы, и потому львиная доля всех сбоев приходится именно на нее. Проявления их могут быть самыми разнообразными: от критических ошибок приложений до периодических или непериодических ошибок чтения(записи) на жесткий диск или даже каскадных ошибок приема/передачи TCP/IP пакетов (что не покажется удивительным, если вспомнить о кэширующем приводе всех драйверов, обслуживающих устройства ввода/вывода). Любой аппаратный ресурс, требующий для своей работы некоторого количества оперативной памяти, так или иначе зависим от работоспособности последней.

Существует мнение, что память "с четностью" полностью решает проблему своей надежности и сводит риск разрушения данных к разумному минимуму. На самом деле это не так. Память с четностью распознает лишь одиночные ошибки и не гарантирует обнаружение групповых. Память типа ECC (Error Check & Correction/Error Correction Code - Контроль и Исправление Ошибок), способна автоматически исправлять любые одиночные ошибки и обнаруживать любые двойные. До тех пор, пока оперативная память функционирует более или менее нормально, противостояние энтропии и помехозащитных кодов решается в пользу последних. Однако при полном или частичном выходе одного или нескольких модулей памяти из строя, корректирующих способностей контролирующих кодов перестает хватать и система начинает работать крайне нестабильно.

Концепция виртуальной памяти, реализованная в операционных системах семейства Windows и UNIX, рассматривает основную оперативную память, как своеобразный кэш. А это значит, что одни и те же логические страницы адресного пространства в разное время могут отображаться на различные физические адреса. Разрушение одной-единственной физической ячейки памяти затрагивает множество виртуальных ячеек и потому сбои памяти практически всегда проявляются "коллективными" критическими ошибками приложений, рассредоточенными в широком диапазоне адресов. Если же критические ошибки возникают лишь в некоторых процессах и располагаются по более или менее постоянным адресам - с высокой степенью вероятности можно предположить, что это программная, а не аппаратная ошибка. Исключение составляет неоткачиваемая область памяти (non-paged pool), занятая ядром системы и всегда размещающаяся по одним и тем же физическим адресам. Наличие дефектных ячеек в данной области обычно приводит к синему экрану смерти и/или полному зависанию системы, хотя в некоторых случаях ошибки драйверов передаются на прикладной уровень и роняют один или несколько процессов.

Самое интересное, что при прогоне нестабильно работающих драйверов/процессов под отладчиком ошибка волшебным образом может исчезать. В действительности ничего загадочного тут нет. За счет многократного снижения интенсивности доступа к памяти, отладчик позволяет "вытянуть" даже дефектные ячейки, затрудняя их локализацию. Некоторые руководства рекомендуют исследовать дамп, сброшенный системой при возникновении критической ошибки в ядре системы, наивно надеясь на то, что искаженные ячейки будут выглядеть как бессмысленный мусор, сразу бросающийся в глаза даже при минимальных навыках дизассемблирования. При разрушении большого количества ячеек памяти, затрагивающих исполняемый код, это действительно так. Однако искажение областей данных предложенный алгоритм выявить не в состоянии. Только чрезвычайно опытный разработчик драйверов заподозрит, что здесь что-то не так. А ведь в некоторых случаях неисправный модуль содержит всего лишь один-единственный дефектный бит информации, который при визуальном осмотре дампа вообще нереально обнаружить. К тому же, не стоит забывать, что "замусоривание" памяти может быть вызвано не только аппаратными, но и программными ошибками (например, программист забыл проинициализировать буфера или направил указатель в "космос", передав управление по произвольному адресу памяти).

Худший случай - это разрушение буферов ввода/вывода, зачастую приводящее к полному краху файловой системы без какой-либо надежды на ее восстановление. По непонятной причине разработчики дисковых драйверов отказались от подсчета контрольной суммы пересылаемых через них блоков данных, что сделало файловую систему чрезвычайно уязвимой. Причем, NTFS отказывается даже в худшей ситуации чем FAT32, поскольку требует значительно меньшего объема буферной памяти для своей поддержки и к тому же значительно легче поддается "ручному" восстановлению. Автор настоящей статьи использует отказоустойчивые буфера, построенные на основе демонстрационных драйверов, входящих в состав DDK и дополненные специальными средствами контроля. Главное ноу-хау данной технологии состоит в том, что обмен с диском ведется на "сыром" (RAW MODE) уровне, т.е. помимо области пользовательских данных в сектор входит контрольная сумма, по которой драйвер с одной и привод с другой стороны контролируют целостность данных. В жизни автора эта технология срабатывала дважды (в смысле - выявляла дефектный модуль памяти, пытавшийся разрушить жесткий диск), так что усилия, затраченные на разработку драйверов, многократно окупили себя сполна!

Кстати, тестирование оперативной памяти путем прогона специальных программ (Check It, PC Diagnostic и им подобных) - не самый лучший путь для выявления ее работоспособности. В силу физической неоднородности подсистемы памяти дефектность бракованных модулей зачастую проявляется не на любой, а на строго определенной последовательности запросов и при определенном сочетании содержимого разрушенной и окрестных ячеек. Тестирующие программы перебирают ограниченное количество наиболее типичных шаблонов и потому обнаруживают лишь некоторые, наиболее дефектные дефекты (шутка!). Ряд серверных чипсетов содержит в себе более или менее продвинутые средства тестирования памяти, работающие в фоновом режиме и работающие достаточно эффективно.

Ряд тестовых пакетов таких, например как TestMem от SERJ_M, перебирают большое количество разнотипных шаблонов и довольно лихо выявляют скрытые дефекты модулей памяти, в обычной жизни проявляющиеся лишь при стечении множества маловероятных обстоятельств. К сожалению, эволюция чипсетов в конце концов привела к тому, что и эти шаблоны перестали работать. При слишком интенсивном обмене с памятью чипсет Intel 875P и другие подобные ему чипсеты начинают вставлять холостые циклы, давая памяти время "остыть", и предотвращая тем самым ее перегрев. С одной стороны, такое конструкторское решение можно только приветствовать, поскольку оно значительно повышает надежность системы, но с другой - чрезвычайно затрудняет ее тестирование. Для получения сколь-нибудь достоверных результатов тестирующая программа должна подобрать такую интенсивность прогона памяти, при которой холостые циклы еще не вставляются, но система работает уже на пределе. Насколько известно автору, подобных программ еще нет и когда они появятся на рынке - неизвестно. Так что, спасение утопающих - забота самих утопающих.

Кстати, о птичках. Сама по себе память может быть и не виновата. Источником ошибок вполне может быть и северный мост чипсета, содержащий контроллер памяти. Исследуя чипсет VIA KT133, автор обнаружил несколько критических ошибок планировщика очередей, приводящих к искажению передаваемых данных и визуально проявляющихся как типичные дефекты памяти.

Блок питания

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

Практически все уважающие себя производители материнских плат оснащают свои детища развитой электронной системной контроля основных (опорных) напряжений, показания которых отображаются специальными утилитами. Убедитесь, что питающий потенциал соответствует норме, отклонясь от нее не более чем на 5...10% и остается более или менее постоянным в процессе работы компьютера. Причем "недобор" напряжения намного более опасен, чем "перебор". Увеличение потенциала на 15...20% практически никогда не приводит к моментальному выходу электроники из строя, правда вызывает ее перегрев, но при наличии качественной системы охлаждения с этим можно и смириться. Но даже незначительное уменьшение потенциала заметно снижает реакционность переходных процессов полупроводниковых элементов и система не успевает поспевать за тактовой частотой, что приводит к зависаниям, критическим ошибкам, перезагрузкам и т.д.

На рис. 1 приведен плохой блок питания, обнаруживающих значительную просадку на линии 12 вольт и чудовищные пульсации напряжения. Линия 3,3 вольт, обслуживающая святую святых - оперативную память - также слегка пульсирует, хотя стабилизируется отнюдь не блоком питания, но самой материнской платой, предел стабилизации которой, впрочем, тоже не безграничен, и даже качественная материнская плата бессильна выправить кривой от рождения блок питания.

Пример плохого блока питания

Рисунок 1. Пример плохого блока питания.

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

...и все-все-все

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

Жесткие диски, становясь все более и более интеллектуальными устройствами, достаточно неприхотливы, правда при неправильной установке терминаторов на SCSI-устройствах Windows NT может выбрасывать голубой экран смерти, но хорошие диски терминируют себя самостоятельно.

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

Программная часть

Катастрофическая небрежность тестирования фирменного и кустарного ПО приводит к появлению многочисленных критических ошибок при его исполнении: "Программа выполнила недопустимую операцию и будут закрыта. Если ошибка будет повторяться, обратитесь к разработчику". К несчастью, критические ошибки приложения (в терминологии Windows 2000 просто "ошибки приложения") имеют устойчивую тенденцию появляться в самые ответственные моменты времени, например накануне сдачи финансового отчета. А разработчики... они (в большинстве своем) такие сообщения просто игнорируют. В частности, потому что просто не знают - как эту информацию интерпретировать и потому что вообще не заботятся о проблемах своих пользователей.

Многие сетуют на тупость Windows и ее неспособность противостоять критическим ошибкам. Но эти обвинения совершенно безосновательны. Возникновение критической ошибки свидетельствует о том, что программа поехала крышей и пошла вразнос. Все, что только операционная система может сделать - это пристрелить ее, в противном случае программа возвратит заведомо неверные данные, чего допускать ни в коем случае нельзя. Так что, операционную систему не ругать следует, а благодарить!

Иногда сообщения о критических ошибках удается предотвратить установкой нового сервис-пака, а иногда наоборот - путем удаления нового. Еще можно попробовать переустановить операционную систему или только само нестабильно работающее приложение. Однако никаких гарантий, что после всех этих манипуляций сбой действительно у вас исчезнет, нет. Достаточно вспомнить нашумевшую историю с червем MSBLASTER, вызывающим критическую ошибку в системном сервисе svchost. Но сколько бы вы ни переустанавливали свою Windows 2000, сколько бы ни меняли железо, ситуация не улучшалась. Антивирусы, правда, сообщали о наличии вируса на компьютере (да и то, не всегда), однако не объясняли, какие меры безопасности следует принять. К тому же, обрушение svchost'а происходило отнюдь не вследствие инфицирования компьютера вирусом, а лишь при неудачной попытке оного. Именно неумение хакеров сорвать стек, не уронив при этом всю систему, и демаскировало вирус, попутно организовав разрушительную DoS-атаку, до сих пор приносящую весьма ощутимые убытки.

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

Приложения, недопустимые операции и все-все-все

Различные операционные системы по-разному реагируют на критические ошибки. Так, например, NT резервирует два региона своего адресного пространства для выявления некорректных указателей. Один находится на самом "дне" карты памяти и предназначен для отлавливания нулевых указателей. Другой расположен между "кучей" и областью памяти, закрепленной за операционной системой. Он контролирует выход за пределы пользовательской области памяти и вопреки расхожему мнению никак не связан в функцией WriteProcessMemory (см. техническую заметку Q92764 в MSDN). Оба региона занимают по 64Кб, и всякая попытка доступа к ним расценивается системой, как критическая ошибка. В 9x имеется всего лишь один 4Кб регион, следящий за нулевыми указателями, поэтому по своим контролирующим способностям она значительно уступает NT.

В Windows NT экран критической ошибки (см. рис.2) содержит следующую информацию: а) адрес машинной инструкции, возбудившей исключение; б) словесное описание категории исключения (или его код, если категория исключения неизвестна); в) параметры исключения (адрес недействительной ячейки памяти, род операции и т.д.).

Сообщение о критической ошибке

Рисунок 2. Сообщение о критической ошибке, выдаваемое операционной системой Windows 2000.

Операционные системы семейства Windows 9x в этом отношении намного более информативны (см. рис.3) и помимо категории исключения выводят содержимое регистров ЦП на момент сбоя, состояние стека и байты памяти по адресу CS:EIP (т.е. текущему адресу исполнения). Впрочем, наличие "Доктора Ватсона" (о нем - ниже) стирает различие между двумя системами и потому можно говорить лишь об удобстве и эргономике 9x, сразу предоставляющей весь минимум необходимых сведений, в то время как в NT отчет об ошибке создается отдельной утилитой.

Сообщение о критической ошибке

Рисунок 3. Сообщение о критической ошибке, выдаваемое операционной системой Windows 98.

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

Запустите "Редактор Реестра" и перейдите в раздел "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug". Если такого раздела нет - создайте его самостоятельно. Строковой параметр "Debugger" задает путь к файлу отладчика со всеми необходимыми ключами; строковой параметр "Auto" указывает, должен ли отладчик запускаться автоматически (значение "1") или предлагать пользователю свободу выбора ("0"). Наконец, двойное слово параметра "UserDebuggerHotKey" специфицирует скэн-код горячей клавиши для принудительного вызова отладчика.

Доктор Ватсон

"Доктор Ватсон" является штатным обработчиком критических ошибок, входящим в базовый пакет поставки всех операционных систем семейства Windows. По своей природе он представляет статическое средство сбора релевантной информации. Предоставляя исчерпывающий отчет о причинах сбоя, "Доктор Ватсон" в тоже самое время лишен активных средств воздействия на некорректно работающее программы. Утихомирить разбушевавшееся приложение, заставив его продолжить свою работу с помощью одного "Доктора Ватсона", вы не сможете и для этого вам придется прибегать к интерактивным отладчикам, одним из которых является Microsoft Visual Studio Debugger, входящий в состав одноименной среды разработки и рассматриваемый несколькими страницами ниже.

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

Для установки "Доктора Ватсона" отладчиком по умолчанию добавьте в реестр следующую запись или запустите файл Drwtsn32.exe c ключом "-i" (для выполнения обоих действий вы должны иметь права администратора):

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug]
"Auto"="1"
"Debugger"="drwtsn32 -p %ld -e %ld -g"
"UserDebuggerHotKey"=dword:00000000

Листинг 1. Установка "Доктора Ватсона" отладчиком по умолчанию.

Теперь возникновение критических ошибок программы станет сопровождаться генерацией отчета, составляемого "Доктором Ватсоном" и содержащим более или менее подробные сведения о характере ее происхождения.

Реакция Доктора Ватсона на критическую ошибку

Рисунок 4. Реакция "Доктора Ватсона" на критическую ошибку.

Образец дампа, созданный "Доктором Ватсоном", приведен ниже:

Исключение в приложении:
        Прил.:  (pid=612)
        ; pid процесса, в котором произошло исключение

        Время: 14.11.2003 @ 22:51:40.674
        ; время, когда произошло исключение

        Номер: c0000005 (нарушение прав доступа)
        ; код категории исключения
        ; расшифровку кодов исключений можно найти в WINNT.H
        ; входящим в состав SDK, прилагаемом к любому Windows-компилятору
        ; подробное описание всех исключений содержится в документации
        ; по процессорам Intel и AMD, бесплатно распространяемой их производителями
        ; (внимание: для перевода кода исключения операционной системы в
        ; вектор прерывания ЦП, вы должны обнулить старшее слово)
        ; в данном случае это 0x5 - попытка доступа к памяти по запрещенному адресу


*----> Сведения о системе <----*
        Имя компьютера: KPNC
        Имя пользователя: Kris Kaspersky
        Число процессоров: 1
        Тип процессора: x86 Family 6 Model 8 Stepping 6
        Версия Windows 2000: 5.0
        Текущая сборка: 2195
        Пакет обновления: None
        Текущий тип: Uniprocessor Free
        Зарегистрированная организация:
        Зарегистрированный пользователь: Kris Kaspersky
        ; краткие сведения о системе


*----> Список задач <----*
   0 Idle.exe
   8 System.exe
 232 smss.exe
:
1244 os2srv.exe
1164 os2ss.exe
1284 windbg.exe
1180 MSDEV.exe
1312 cmd.exe
 612 test.exe
1404 drwtsn32.exe
   0 _Total.exe

(00400000 - 00406000)
(77F80000 - 77FFA000)
(77E80000 - 77F37000)
; перечень загруженных DLL
; согласно документации, справа от адресов должны быть перечислены имена
; соответствующих модулей, однако практически все они так хорошо "замаскировались",
; что стали совершенно не видны. вытащить их имена из файла протокола все-таки можно,
; но придется немного пошаманить (см. ниже "таблицу символов")

Копия памяти для потока 0x188
; ниже идет копия памяти потока, вызывавшего исключение

eax=00000064 ebx=7ffdf000 ecx=00000000 edx=00000064 esi=00000000 edi=00000000
eip=00401014 esp=0012ff70 ebp=0012ffc0 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000202
; содержимое регистров и флагов

функция: <nosymbols>
; распечатка окрестной точки cбоя

        00400ffc 0000            add        [eax],al                 ds:00000064=??
        ; записываем в ячейку на которую ссылает EAX значение AL
        ; значение адреса ячейки, вычисленной Доктором Ватсоном, равно 64h
        ; что, очевидно, не соответствует действительности;
        ; Доктор Ватсон подставляет в выражение значение регистра EAX
        ; на момент возникновения сбоя, и это совсем не то значение, которое
        ; было в момент исполнения! к сожалению, чему был равен EAX в момент
        ; исполнения ни нам, ни Доктору Ватсону не известен.

        00400ffe 0000            add        [eax],al                 ds:00000064=??
        ; записываем в ячейку, на которую ссылает EAX значение AL
        ; как? опять? что это  за бред?! вообще-то так кодируется
        ; последовательность 00 00 00 00, по всей видимости являющаяся
        ; осколком некоторой машинной команды, неправильно интерпретированной
        ; дизассемблерным движком Доктора Ватсона;

        00401000 8b542408        mov        edx,[esp+0x8]            ss:00f8d547=????????
        ; загружаем в EDX аргумент функции
        ; какой именно аргумент - сказать невозможно, т.к. мы не знаем адрес
        ; стекового фрейма;

        00401004 33c9            xor        ecx,ecx
        ; обнуляем ECX

        00401006 85d2            test       edx,edx
        00401008 7e18            jle        00409b22
        ; если EDX == 0, прыгаем на адрес 409B22h

        0040100a 8b442408        mov        eax,[esp+0x8]            ss:00f8d547=????????
        ; загружаем уже упомянутый аргумент в регистр EAX

        0040100e 56              push       esi
        ; сохраняем ESI в стеке, перемещая тем самым указатель вершины стека
        ; на 4 байта вверх (в область младших адресов)

        0040100f 8b742408        mov        esi, [esp+0x8]           ss:00f8d547=????????
        ; загружаем в ESI очередной аргумент
        ; поскольку ESP был только что изменен, это совсем не тот аргумент,
        ; с которым мы имели дело ранее

        00401013 57              push       edi
        ; сохраняем регистр EDI в стеке

СБОЙ -> 00401014 0fbe3c31        movsx      edi,byte ptr [ecx+esi]   ds:00000000=??
        ; вот мы и добрались до инструкции, возбудившей исключение доступа
        ; она обращается к ячейке памяти, на которую указывает сумма регистров ECX и ESI
        ; а чему равно их значение? Прокручиваем экран немного вверх и находим, что
        ; ECX и ESI равны 0, о чем Доктор Ватсон нам и сообщает: "ds:000000"
        ; отметим, что этой информации можно верить, поскольку подстановка
        ; эффективного адреса осуществлялась непосредственно в момент исполнения
        ; теперь вспомним, что ESI содержит копию переданного функции аргумента
        ; и что ECX был обнулен явно, следовательно в выражении [ECX+ESI]
        ; регистр ESI - указатель, а ECX - индекс.
        ; раз ESI равен нулю, то нашей функции передали указатель на невыделенную
        ; область памяти. Обычно это происходит либо вследствие алгоритмической
        ; ошибки в программе, либо вследствие исчерпания виртуальной памяти
        ; к сожалению, Доктор Ватсон не осуществляет дизассемблирование
        ; материнской функции, и какой из двух предполагаемых вариантов правильный
        ; нам остается лишь гадать: правда, можно дизассемблировать дамп памяти
        ; процесса (если, конечно, он был сохранен), но это уже не то:

        00401018 03c7            add        eax, edi
        ; сложить содержимое регистра EAX с регистром EDI и записать результат в EAX

        0040101a 41              inc        ecx
        ; увеличить ECX на единицу

        0040101b 3bca            cmp        ecx,edx
        0040101d 7cf5            jl         00407014
        ; до тех пор пока ECX < EDX, прыгать на адрес 407014
        ; (очевидно, мы имеем дело с циклом, управляемым счетчиком ECX)
        ; при интерактивной отладке мы могли бы принудительно выйти
        ; из функции, возвратив флаг ошибки, чтобы материнская функция
        ; (а с ней и вся программа целиком) могла продолжить свое выполнение
        ; и в этом случае потерянной окажется лишь последняя операция, но все
        ; остальные данные окажутся неискаженными;

        0040101f 5f              pop        edi
        00401020 5e              pop        esi
        00401021 c3              ret
        ; выходим из функции

*----> Обратная трассировка стека <----*
; содержимое стека на момент возникновения сбоя
; распечатывает адреса и параметры предыдущих выполняемых функций,
; при интерактивной отладке мы могли бы просто передать управление
; на одну из вышележащих функций, что эквивалентно возращению в прошлое
; это только в реальной жизни разбитую чашку восстановить нельзя,
; в компьютерной вселенной возможно все!
FramePtr ReturnAd Param#1  Param#2  Param#3  Param#4  Function Name
; FramePtr:      указывает на значение фрейма стека,
;                выше (т.е. в более младших адресах) содержатся аргументы функции
;                ниже - ее локальные переменные
;
; ReturnAd:      бережно хранит адрес возврата в материнскую функцию
;                если здесь содержится мусор и обратная трассировка стека
;                начинает характерно шуметь, с высокой степенью вероятности
;                можно предположить, что мы имеем дело с ошибкой "срыва стека"
;                а, возможно, и с попыткой атаки вашего компьютера
;
; Param#:        четыре первых параметра функции - именно столько параметров
;                Доктор Ватсон отображает на экране; это достаточно жесткое
;                ограничение - многие функции имеют десятки параметров и
;                четыре параметра еще ни о чем не говорят; однако недостающие
;                параметры легко вытащить из копии необработанного стека вручную,
;                достаточно лишь перейти по указанному в поле FramePtr адресу
;
; Func Name:     имя функции (если только его возможно определить); реально
;                отображает лишь имена функций, импортируемые из других DLL,
;                поскольку встретить коммерческую программу, откомпилированную
;                вместе с отладочной информацией практически нереально
;
0012FFC0 77E87903 00000000 00000000 7FFDF000 C0000005 !<nosymbols>
0012FFF0 00000000 00401040 00000000 000000C8 00000100 kernel32!SetUnhandledExceptionFilter
; функции перечисляются в порядке их исполнения; самой последней исполнялась
; kernel32!SetUnhandledExceptionFilter функция, обрабатывающая данное исключение

*----> Копия необработанного стека <----*
; копия необработанного стека содержит стек таким, какой он есть
; очень помогает при обнаружении buffer overfull атак - весь shell-код,
; переданный злоумышленником, будет распечатан Доктором Ватсоном, и вам
; останется всего лишь опознать его (подробнее об этом рассказывается
; в моей книге "Техника сетевых атак")
0012ff70  00 00 00 00 00 00 00 00 - 39 10 40 00 00 00 00 00  ........9.@.....
0012ff80  64 00 00 00 f4 10 40 00 - 01 00 00 00 d0 0e 30 00  d.....@.......0.
:
00130090  00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00  ................
001300a0  00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00  ................

*----> Таблица символов <----*
; таблица символов содержит имена всех загруженных DLL вместе с именами
; импортируемых функций - используя эти адреса в качестве отправной точки,
; мы без труда сможем восстановить "перечень загруженных DLL"

ntdll.dll
77F81106 00000000   ZwAccessCheckByType
:
77FCEFB0 00000000   fltused

kernel32.dll
77E81765 0000003d   IsDebuggerPresent
:
77EDBF7A 00000000   VerSetConditionMask
;
; итак, возвращаемся к таблице загруженных DLL
; (00400000 - 00406000)  - это, очевидно, область памяти, занятая самой программой
; (77F80000 - 77FFA000)  - это KERNEL32.DLL
; (77E80000 - 77F37000)  - это NTDDL.DLL

Листинг 2. Образец отчета "Доктора Ватсона" с комментариями автора.

Microsoft Visual Studio Debug

При установке среды разработки Microsoft Visual Studio она регистрирует свой отладчик основным отладчиком критических ошибок по умолчанию. Это простой в использовании, но функционально ущербный отладчик, не поддерживающий даже такой банальной операции, как поиск hex-последовательности в оперативной памяти. Единственная "вкусность", отличающая его от продвинутого во всех отношениях Microsoft Kernel Debugger'a - это возможность трассировки "упавших" процессов, выбросивших критическое исключение.

В опытных руках отладчик Microsoft Visual Studio Debugger способен творить настоящие чудеса и одно из таких чудес - это возобновление работы приложений, совершивших недопустимую операцию и при нормальном течении событий аварийно завершаемых операционной системой без сохранения данных. В любом случае, интерактивный отладчик (коим Microsoft Visual Studio Debugger и является) предоставляет намного более подробную информацию о сбое и значительно упрощает процесс выявления источников его возникновения. К сожалению, тесные рамки журнальной статьи не позволяют изложить всю методику поиска неисправностей целиком и приходится ограничиваться лишь узким кругом наиболее интересных (и наименее известных!) вопросов.

Для ручной установки Microsoft Visual Studio Debugger'а основным отладчиком критических ошибок добавьте в реестр следующие данные:

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug]
"Auto"="1"
"Debugger"="\"C:\\Prg Files\\MS VS\\Common\\MSDev98\\Bin\\msdev.exe\" -p %ld -e %ld"
"UserDebuggerHotKey"=dword:00000000

Листинг 3. Установка Microsoft Visual Studio Debugger'a основным отладчиком критических ошибок.

// функция возвращает сумму n символов типа char
// если ей передать null-pointer, она "упадет",
// хотя источник ошибки не в ней, а в аргументах,
// переданных материнской функцией
test(char *buf, int n)
{
       int a, sum;
       for (a = 0; a < n; a++) sum += buf[a]; // здесь возбуждается исключение
       return sum;
}

main()
{
       #define N 100
       char *buf = 0; // инициализируем указатель на буфер

       /* buf = malloc(100); */ "забываем" выделить память, здесь ошибка
       test(buf, N); // передаем null-pointer некоторой функции
}

Листинг 4. Демонстрационная программа, вызывающая сообщение о критической ошибке.

Обитатели сумеречной зоны, или из морга в реанимацию

Хотите узнать, как заставить приложение продолжить нормальную работу после появления сообщения о критической ошибке? Это действительно очень актуально. Представьте, что рухнуло приложение, содержащее уникальные и еще не сохраненные данные. По минимуму, их придется набивать заново, по максимуму - они потеряны для вас навсегда. На рынке имеется некоторое количество утилит, нацеленных на эту задачу (взять те же Norton Utilities), но их интеллектуальность оставляет желать лучшего и в среднем они срабатывают один раз из десяти. В то же самое время, ручная реанимация программа воскрешает ее в 75...90% случаев.

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

Существует, по меньшей мере, три различных способа реанимации: а) принудительный выход из функции, возбудившей исключение; б) "раскрутка" стека с передачей управления назад; в) передача управления на функцию обработки сообщений. Рассмотрим каждый из этих способов подробнее, используя в качестве примера тестовую утилиту testt.

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

Принудительный выход из функции

Запускаем тестовую программу, набиваем в одном или нескольких окнах какой-нибудь текст, затем в меню "Help" выбираем пункт "About TestCEdit" и в появившемся диалоговом окне щелкаем по кнопке "make error". Оп-ля! Программа выбрасывает критическую ошибку и, если мы нажмем на "ОК", все несохраненные данные необратимо погибнут, что никак не входит в наши планы. Однако при наличии предварительно установленного отладчика мы еще можем кое-что предпринять. Пусть для определенности это будет Microsoft Visual Studio Debugger.

Нажимаем "Отмену" и отладчик немедленно дизассемблирует функцию, возбудившую исключение (см. листинг, приведенный ниже).

0040135C        push      esi
0040135D        mov       esi,dword ptr [esp+8]
00401361        push      edi
00401362        movsx     edi,byte ptr [ecx+esi]
00401366        add       eax,edi
00401368        inc       ecx
00401369        cmp       ecx,edx
0040136B        jl        00401362
0040136D        pop       edi
0040136E        pop       esi
0040136F        ret       8

Листинг 5. Отладчик Microsoft Visual Studio Debugger дизассемблировал функцию, возбудившую исключение.

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

Переместив курсор в окно дампа, набьем в строке адреса название регистра указателя вершины стека - "ESP" и нажмем на <Enter>. Содержимое стека тут же предстанет перед нашими глазами:

0012F488  0012FA64  0012FA64  004012FF
0012F494  00000000  00000064  00403458
0012F4A0  FFFFFFFF  0012F4C4  6C291CEA
0012F4AC  00000019  00000000  6C32FAF0
0012F4B8  0012F4C0  0012FA64  01100059
0012F4C4  006403C2  002F5788  00000000
0012F4D0  00640301  77E16383  004C1E20

Листинг 6. Поиск адреса возврата из текущей функции (выделен жирным шрифтом).

Первые два двойных слова соответствуют машинным командам POP EDI/POP ESI и не представляют для нас совершенно никакого интереса. А вот следующее двойное слово содержит адрес выхода в материнскую процедуру (в приведенном выше листинге оно выделено жирным шрифтом). Как раз его-то нам и надо!

Нажимаем <Ctrl-D> и затем 0x4012FF, отладчик послушно отображает следующий дизассемблерный текст:

004012FA        call     00401350
004012FF        cmp      eax,0FFh
00401302        je       0040132D
00401304        push     eax
00401305        lea      eax, [esp+8]
00401309        push     405054h
0040130E        push     eax
0040130F        call     dword ptr ds:[4033B4h]
00401315        add      esp, 0Ch
00401318        lea      ecx, [esp+4]
0040131C        push     0
0040131E        push     0
00401320        push     ecx
00401321        mov      ecx, esi
00401323        call     00401BC4
00401328        pop      esi
00401329        add      esp,64h
0040132C        ret
0040132C
0040132D        push     0
0040132D        ; эта ветка получает управление, если Функция 401350h вернет FFh
0040132F        push     0
00401331        push     405048h
00401336        mov      ecx,esi
00401338        call     00401BC4
0040133D        pop      esi
0040133E        add      esp,64h
00401341        ret

Листинг 7. Дизассемблерный листинг материнской функции.

Смотрите - если регистр EAX равен FFh, то материнская функция передает управление на ветку 40132Dh и спустя несколько машинных команд завершает свою работу, передавая "бразды правления" функции более высокого уровня. Напротив, если EAX != FFh, то его значение передается функции 4033B4h. Следовательно, мы можем предположить, что FFh - это флаг ошибки и есть. Возвращаемся в подопытную функцию, нажав <Ctrl-G> и "EIP", переходим в окно "Registers" и меняем значение EAX на FFh.

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

В общем случае число PUSH-команд должно в точности соответствовать количеству POP (также учитывайте, что PUSH DWORD X эквивалентен SUB ESP, 4, а POP DWORD X - ADD ESP, 4). Проанализировав дизассемблерный листинг функции, мы приходим к выводу, что для достижения гармонии добра и зла мы должны стащить с вершины стека два двойных слова, соответствующие машинным командам 40135С:PUSH ESI и 401361:PUSH EDI. Это достигается передачей управления по адресу 40136Dh, где живут два добродушных POP'а, приводящие стек в равновесное состояние. Подводим сюда курсор и уверенным щелчком правой клавиши мыши вызываем контекстное меню, среди пунктов которого выбираем "Set Next Statement". Как вариант можно перейти в окно регистров и изменить значение EIP с 401362h на 40136Dh.

Нажатием <F5> мы заставляем процессор продолжить выполнение программы и... о, чудо! Она действительно продолжает свою работу (незлобное ругательство на ошибку последней операции - не в счет!). Несохраненные данные спасены!

Раскрутка стека

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

Процедура отката состоит из трех шагов: а) построения дерева вызовов; б) определения координат стекового фрейма для каждого из них; в) восстановления регистрового контекста материнской функции. Хороший отладчик все это сделает за нас, и вам останется лишь записать в регистры EIP и ESP соответствующие значения. К сожалению, отладчик Microsoft Visual Studio Debugger к хорошим отладчикам не относится. Он довольно посредственно трассирует стек, пропуская FPO-функции (Frame Point Omission - функции с оптимизированным фреймом) и не сообщает координат стекового фрейма, "благодаря" чему самую трудоемкую часть работы нам приходится выполнять самостоятельно.

Впрочем, даже такой стек вызовов все же лучше, чем совсем ничего. Раскручивая его вручную мы будем отталкиваться от того, что координаты фрейма естественным образом определяются по адресу возврата. Допустим, содержимое окна "Call Stack" выглядит так:

TESTCEDIT! 00401362()
MFC42! 6c2922ae()
MFC42! 6c298fc5()
MFC42! 6c292976()
MFC42! 6c291dcc()
MFC42! 6c291cea()
MFC42! 6c291c73()
MFC42! 6c291bfb()
MFC42! 6c291bba()

Листинг 8. Содержимое окна Call Stacks отладчика Microsoft Visual Studio Debugger.

Попробуем найти в стеке адреса 6C2922AEh и 6C298FC5h, соответствующие двум последним ступеням исполнения. Нажимаем <ATL-6> для перехода в окно дампа и, воспользовавшись горячей комбинацией клавиш <Ctrl-G> в качестве базового адреса отображения, выбираем "ESP". Прокручивая окно дампа вниз, мы обнаруживаем оба адреса возврата (в приведенном ниже листинге они выделены синим цветом):

0012F488 0012FA64 0012FA64 004012FF <- 0040136F:ret 8 первый адрес возврата
0012F494 00000000 00000064 00403458 <- 00401328:pop esi
0012F4A0 FFFFFFFF 0012F4C4 6C291CEA
0012F4AC 00000019 00000000 6C32FAF0
0012F4B8 0012F4C0 0012FA64 01100059
0012F4C4 00320774 002F5788 00000000
0012F4D0 00320701 77E16383 004C1E20
0012F4DC 00320774 002F5788 00000000
0012F4E8 000003E8 0012FA64 004F8CD8
0012F4F4 0012F4DC 002F5788 0012F560
0012F500 77E61D49 6C2923D8 00403458 <- 0040132C:ret;
0012F50C 00000111 0012F540 6C2922AE <- 6C29237E:pop ebx/pop ebp/ret 1Ch
0012F518 0012FA64 000003E8 00000000
0012F518 0012FA64 000003E8 00000000
0012F524 004012F0 00000000 0000000C
0012F530 00000000 00000000 0012FA64
0012F53C 000003E8 0012F564 6C298FC5
0012F548 000003E8 00000000 00000000
0012F554 00000000 000003E8 0012FA64

Листинг 9. Содержимое стека после раскрутки.

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

Возвращаясь к листингу 5, отметим, что два двойных слова, лежащие на верхушке стека, соответствуют машинным командам POP EDI и POP ESI, а следующий за ними адрес - 4012FFh - это тот самый адрес, управление которому передается командой 40136Fh:RET 8. Для продолжения раскрутки стека мы должны дизассемблировать код по этому адресу:

004012FA        call     00401350
004012FF        cmp      eax,0FFh
00401302        je       0040132D
00401304        push     eax
00401305        lea      eax,[esp+8]
00401309        push     405054h
0040130E        push     eax
0040130F        call     dword ptr ds:[4033B4h]
00401315        add      esp,0Ch
00401318        lea      ecx,[esp+4]
0040131C        push     0
0040131E        push     0
00401320        push     ecx
00401321        mov      ecx,esi
00401323        call     00401BC4
00401328        pop      esi
00401329        add      esp,64h
0040132C        ret                     ; SS:[ESP] = 6C2923D8

Листинг 10. Дизассемблерный листинг праматеринской функции ("бабушки").

Прокручивая экран вниз, мы замечаем инструкцию ADD  ESP, 64, закрывающую текущий кадр стека. Еще восемь байт снимает инструкция 40136Fh:RET 8 и четыре байта оттягивает на себя 401328:POP ESI. Таким образом, позиция адреса возврата в стеке равна: current_ESP + 64h + 8 + 4 = 70h. Спускаемся на 70h байт ниже и видим:

0012F500  77E61D49  6C2923D8  00403458 <- 00401328:POP ESI/ret;

Листинг 11. Адрес возврата из праматеринской функции.

Первое двойное слово - это значение регистра ESI, который нам предстоит вручную восстановить; второе - адрес возврата из функции. Нажатием <Ctrl-G>, "0x6C2923D8" мы продолжаем раскручивать стек:

6C2923D8        jmp      6C29237B
...
6C29237B        mov      eax,ebx
6C29237D        pop      esi
6C29237E        pop      ebx
6C29237F        pop      ebp
6C292380        ret      1Ch

Листинг 12. Дизассемблерный листинг пра-праматеринской функции.

Вот мы и добрались до восстановления регистров! Сместившись на одно двойное слово вправо (оно только что было вытолкнуто из стека командой RET), переходим в окно "Registers" и восстанавливаем регистры ESI, EBX, EBP, извлекая сохраненные значения из стека:

0012F500 77E61D49 6C2923D8 00403458 <- 6C29237D:pop esi
0012F50C 00000111 0012F540 6C2922AE <- 6C29237E:pop ebx/pop ebp/ret 1Ch

Листинг 13. Содержимое регистров, ранее сохраненных в стеке вместе с адресом возврата.

Как вариант, можно переместить регистр EIP на адрес 6C29237Dh, а регистр ESP - на адрес 12F508h, после чего нажать на <F5> для продолжения выполнения программы. И этот прием действительно срабатывает! Причем, реанимированная программа уже не ругается на ошибку последней операции (как это было при восстановлении путем принудительного выхода из функции), а просто ее не выполняет. Красота!

Передача управления на функцию обработки сообщений

Двум предыдущим способам "реанимации" приложений присущи серьезные ограничения и недостатки. При тяжелых разрушениях стека, вызванных атаками типа buffer overfull или же просто алгоритмическими ошибками, содержимое важнейших регистров процессора окажется искажено, и мы уже не сможем ни совершить откат (стек утерян), ни выйти из текущей функции (EIP смотрит в космос). В консольных приложениях в такой ситуации действительно очень мало, что можно сделать... Вот GUI - другое дело! Концепция событийно ориентированной архитектуры наделяет всякое оконное приложение определенными серверными функциями. Даже если текущий контекст выполнения необратимо утерян, мы можем передать управление на цикл извлечения и диспетчеризации сообщений, заставляя программу продолжить обработку действий пользователя.

Классический цикл обработки сообщений выглядит так:

while (GetMessage(&msg, NULL, 0, 0))
{
       TranslateMessage(&msg);
       DispatchMessage(&msg);
}

Листинг 14. Классический цикл обработки сообщений.

Все, что нам нужно - это передать управление на цикл while, даже не заботясь о настройке кадра стека, поскольку оптимизированные программы (а таковых большинство) адресуют свои локальные переменные не через EBP, а непосредственно через сам ESP. Конечно, при обращении к переменной msg функция угробит содержимое стека, лежащее ниже его вершины, но это уже неважно.

Правда, при выходе из приложения, оно упадет окончательно (ведь вместо адреса возврата из функции обработки сообщений машинная команда RET обнаружит на вершине стека неизвестно что), но это произойдет после сохранения всех данных и потому никакой угрозы не несет. Исключение составляют приложения, "забывающие" закрыть все открытые файлы и перекладывающие эту работу на плечи функции ExitProcess. Что ж! Можно так подправить адрес возврата, чтобы он указывал на ExitProcess!

Давайте создадим простейшее Windows-приложение и поэкспериментирует с ним. Запустив Microsoft Visual Studio выберем "New -> Project -> Win32 Application" и там - "Typical Hello, World application". Добавим новый пункт меню, а в нем: char *p; *p = 0; и откомпилируем этот проект с отладочной информацией.

Роняем приложение на пол и, запустив отладчик, подгоняем мышь к первой строке цикла обработки сообщений и в появившемся контекстном меню находим пункт "Set Next Statement". Нажимаем <F5> для возобновления работы программы и... она действительно возобновляет свою работу!

А теперь откомпилируем наш проект в чистовом варианте (т.е. без отладочной информации) и попробуем реанимировать приложение в голом машинном коде. Пользуясь тем обстоятельством, что Windows - это действительно многозадачная среда, в которой крушение одного процесса не мешает работе всех остальных, запустим свой любимый дизассемблер (например IDA PRO) и проанализируем таблицу импорта отлаживаемой программы (вообще-то это может сделать и бесплатно распространяемый dumpbin, но его отчет не так нагляден).

Целью нашего поиска будут функции TranslateMessage/DispatchMessage и перекрестные ссылки, ведущие к циклу выборки сообщений.

.idata:004040E0 ; BOOL __stdcall TranslateMessage(const MSG *lpMsg)
.idata:004040E0 extrn TranslateMessage:dword    ; DATA XREF: _WinMain@16+71^r
.idata:004040E0                                 ; _WinMain@16+8D^r
.idata:004040E4 ; LONG __stdcall DispatchMessageA(const MSG *lpMsg)
.idata:004040E4 extrn DispatchMessageA:dword    ; DATA XREF: _WinMain@16+94^r
.idata:004040E8

Листинг 15. Поиск функций TranslateMessage/DispatchMessage в таблице импорта.

С функцией DispatchMessage связана всего лишь одна перекрестная ссылка, со всей очевидностью ведущая к искомому циклу обработки сообщений, дизассемблерный код которого выглядит так:

.text:00401050                mov      edi, ds:GetMessageA
.text:00401050 ; первый вызов GetMessageA (это еще не цикл, это только его предверие)
.text:00401050
.text:00401056                push     0                   ; wMsgFilterMax
.text:00401058                push     0                   ; wMsgFilterMin
.text:0040105A                lea      ecx, [esp+2Ch+Msg]
.text:0040105A ; ECX указывает на область памяти, через которую GetMessageA
.text:0040105A ; станет возвращать сообщение. Текущее значение ESP может быть
.text:0040105A ; любым, главное, чтобы оно указывало на действительную область
.text:0040105A ; памяти (см. карту памяти, если значение ESP оказалось искажено
.text:0040105A ; настолько, что вывело его в "космос")
.text:0040105A ;
.text:0040105E                push     0                   ; hWnd
.text:00401060                push     ecx                 ; lpMsg
.text:00401061                mov      si, eax
.text:00401063                call     edi ; GetMessageA
.text:00401063 ; вызываем GetMessageA
.text:00401063
.text:00401065                test     eax, eax
.text:00401067                jz       short loc_4010AD
.text:00401067 ; проверка на наличие необработанных сообщений в очереди
.text:00401067
...
.text:00401077 loc_401077:                                 ; CODE XREF: _WinMain@16+A9vj
.text:00401077 ; начало цикла обработки сообщений
.text:00401077
.text:00401077                mov      eax, [esp+2Ch+Msg.hwnd]
.text:0040107B                lea      edx, [esp+2Ch+Msg]
.text:0040107B ; EDX указывает на область памяти, используемую для передачи сообщений
.text:0040107B
.text:0040107F                push     edx                 ; lpMsg
.text:00401080                push     esi                 ; hAccTable
.text:00401081                push     eax                 ; hWnd
.text:00401082                call     ebx                 ; TranslateAcceleratorA
.text:00401082 ; вызываем функцию TranslateAcceleratorA
.text:00401082
.text:00401084                test     eax, eax
.text:00401086                jnz      short loc_40109A
.text:00401086 ; проверка на наличие в очереди необработанных сообщений
.text:00401086
.text:00401088                lea      ecx, [esp+2Ch+Msg]
.text:0040108C                push     ecx                 ; lpMsg
.text:0040108D                call     ebp                 ; TranslateMessage
.text:0040108D ; вызываем функцию TranslateMessage, если есть, что транслировать
.text:0040108D
.text:0040108F                lea      edx, [esp+2Ch+Msg]
.text:00401093                push     edx                 ; lpMsg
.text:00401094                call     ds:DispatchMessageA
.text:00401094 ; диспетчеризуем сообщение
.text:0040109A
.text:0040109A loc_40109A:                                 ; CODE XREF: _WinMain@16+86^j
.text:0040109A                push     0                   ; wMsgFilterMax
.text:0040109C                push     0                   ; wMsgFilterMin
.text:0040109E                lea      eax, [esp+34h+Msg]
.text:004010A2                push     0                   ; hWnd
.text:004010A4                push     eax                 ; lpMsg
.text:004010A5                call     edi                 ; GetMessageA
.text:004010A5 ; читаем очередное сообщение из очереди
.text:004010A5
.text:004010A7                test     eax, eax
.text:004010A9                jnz      short loc_401077
.text:004010A9 ; вращаем цикл обработки сообщений
.text:004010A9
.text:004010AB                pop      ebp
.text:004010AC                pop      ebx
.text:004010AD
.text:004010AD loc_4010AD:                                 ; CODE XREF: _WinMain@16+67^j
.text:004010AD                mov      eax, [esp+24h+Msg.wParam]
.text:004010B1                pop      edi
.text:004010B2                pop      esi
.text:004010B3                add      esp, 1Ch
.text:004010B6                retn     10h
.text:004010B6 _WinMain@16    endp

Листинг 16. Дизассемблерный листинг функции обработки сообщений.

Мы видим, что цикл обработки сообщений начинается с адреса 401050h и именно на этот адрес следует передать управление, чтобы возобновить работу упавшей программы. Пробуем сделать это и... программа работает!

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

Рассмотрим, как устроен цикл обработки сообщений в MFC. Большую часть своего времени исполнения MFC-приложения проводят внутри функции CWinThread::Run(void), которая периодически опрашивает очередь на предмет поступления свежих сообщений и рассылает их соответствующим обработчикам. Если один из обработчиков споткнулся и довел систему до критической ошибки, выполнение программы может быть продолжено в функции Run. В этом-то и заключается ее главная прелесть!

Функция не имеет явных аргументов, но принимает скрытый аргумент this, указывающей на экземпляр класса CWinThread или производный от него класс, без которого функция просто не сможет работать. К счастью, таблицы виртуальных методов класса CWinThread содержат достаточно количество "родимых пятен", чтобы указатель this можно было воссоздать вручную.

Загрузим функцию Run в дизассемблер и отметим все обращения к таблице виртуальных методов, адресуемой через регистр ECX.

.text:6C29919D n2k_Trasnlate_main:                          ; CODE XREF: MFC42_5715+1F^j
.text:6C29919D                                              ; MFC42_5715+67|j ...
.text:6C29919D                mov     eax, [esi]
.text:6C29919F                mov     ecx, esi
.text:6C2991A1                call    dword ptr [eax+64h]   ; CWinThread::PumpMessage(void)
.text:6C2991A4                test    eax, eax
.text:6C2991A6                jz      short loc_6C2991DA
.text:6C2991A8                mov     eax, [esi]
.text:6C2991AA                lea     ebp, [esi+34h]
.text:6C2991AD                push    ebp
.text:6C2991AE                mov     ecx, esi
.text:6C2991B0                call    dword ptr [eax+6Ch]   ; CWinThread::IsIdleMessage(MSG*)
.text:6C2991B3                test    eax, eax
.text:6C2991B5                jz      short loc_6C2991BE
.text:6C2991B7                push    1
.text:6C2991B9                mov     [esp+14h], ebx
.text:6C2991BD                pop     edi
.text:6C2991BE
.text:6C2991BE loc_6C2991BE:                                ; CODE XREF: MFC42_5715+51^j
.text:6C2991BE                push    ebx                   ; wRemoveMsg
.text:6C2991BF                push    ebx                   ; wMsgFilterMax
.text:6C2991C0                push    ebx                   ; wMsgFilterMin
.text:6C2991C1                push    ebx                   ; hWnd
.text:6C2991C2                push    ebp                   ; lpMsg
.text:6C2991C3                call    ds:PeekMessageA
.text:6C2991C9                test    eax, eax
.text:6C2991CB                jnz     short n2k_Trasnlate_main
.text:6C2991CD

Листинг 17. Дизассемблерный листинг функции Run (фрагмент).

Таким образом, функция Run ожидает получить указатель на двойное слово, указывающее на таблицу виртуальных методов, 0x19 и 0x1B, элементы которой представляют собой функции PumpMessage и IsIdleMessage, соответственно (или переходники к ним). Адреса импортируемых функций, если только динамическая библиотека не была перемещена, можно узнать в том же дизассемблере; в противном случае следует отталкиваться от базового адреса модуля, отображаемого отладчиком по команде "Modules". При условии, что эти две функции не были перекрыты программистом, поиск нужной нам виртуальной таблицы не составит никакого труда.

По непонятным причинам библиотека MFC42.DLL не экспортирует символьных имен функций и эту информацию нам приходится добывать самостоятельно. Обработав библиотеку MFC42.LIB утилитой dumpbin, запущенной с ключом "/ARCH", мы определим оридиналы обеих функций (ординал PumpMessage - 5307, а IsIdleMessage - 4079). Остается найти эти значения в экспорте библиотеки MFC42.DLL (dumpbin /EXPORTS mfc42.dll > mfc42.txt), из чего мы узнаем, что адрес функции PumpMessage: 6C291194h, а IsIdleMessage - 6С292583h.

Теперь мы должны найти указатели на функции PumpMessage/IsIdleMessage в памяти, а точнее - в секции данных, базовый адрес которой содержится в заголовке PE-файла, только помните, что в x86-процессорах наименее значимый байт располагается по меньшему адресу, т.е. все числа записываются задом наперед (к сожалению, отладчик Microsoft Visual Studio Debugger не поддерживает операцию поиска в памяти и нам приходится действовать обходным путем - копировать содержимое дампа в буфер обмена, вставлять его в текстовой файл и, нажав <F7>, искать адреса уже там).

Долго ли, коротко ли, но интересующие нас указатели обнаруживаются по адресам 403044h/40304Сh (естественно, у вас эти адреса могут быть и другими). Причем обратите внимание: расстояние между указатели в точности равно расстоянию между указателями на [EAX + 64h] и [EAX + 6Ch], а очередность их размещения в памяти обратна порядку объявления виртуальных методов. Это - хороший признак и мы, скорее всего, находимся на правильном пути:

00403044        6C2911D4 6C292583 6C291194 ; IsIdleMessage/PumpMessage
00403050        6C2913D0 6C299144 6C297129
0040305C        6C297129 6C297129 6C291A47

Листинг 18. Адреса функций IsIdleMessage/PumpMessage, найденные в секции данных.

Указатели, указывающие на адреса 403048h/40304Ch, очевидно, и будут кандидатами в члены искомой таблицы виртуальных методов класса CWinThread. Расширив сферу поиска всем адресным пространством отлаживаемого процесса, мы обнаруживаем два следующих переходника:

00401A20       jmp        dword ptr ds:[403044h] ; IsIdleMessage
00401A26       jmp        dword ptr ds:[403048h] ;
00401A2C       jmp        dword ptr ds:[40304Ch] ; PumpMessage

Листинг 19. Переходники к функциям IsIdleMessage/PumpMessage, найденные там же.

Ага, уже теплее! Мы нашли не сами виртуальные функции, но переходники к ним. Раскручивая этот запутанный клубок, попробуем отыскать ссылки на 401A26h/401A2Ch, которые передают управление на приведенный выше код:

00403490 00401A9E 00401040 004015F0 <- 0x0,  0x1,  0x2 элементы
0040349C 00401390 004015F0 00401A98 <- 0x3,  0x4,  0x5 элементы
004034A8 00401A92 00401A8C 00401A86 <- 0x6,  0x7,  0x8 элементы
004034B4 00401A80 00401A7A 00401A74 <- 0x9,  0xA,  0xB элементы
004034C0 00401010 00401A6E 00401A68 <- 0xC,  0xD,  0xE элементы
004034CC 00401A62 00401A5C 00401A56 <- 0xF,  0x10, 0x11 элементы
004034D8 00401A50 00401A4A 00401A44 <- 0x12, 0x13, 0x14 элементы
004034E4 00401A3E 004010B0 00401A38 <- 0x15, 0x16, 0x17 элементы
004034F0 00401A32 00401A2C 00401A26 <- 0x18, 0x19, 0x1A элементы (PumpMessage)
004034FC 00401A20 00401A1A 00401A14 <- 0x1B, 0x1C, 0x1D элементы (IsIdleMessage)

Листинг 20. Виртуальная таблица класса CWinThread.

Даже неопытный исследователь программ распознает в этой структуре данных таблицу виртуальных функций. Указатели на переходники к PumpMessage/IsIdleMessage разделяются ровно одним элементом, как того и требуют условия задачи. Предположим, что эта виртуальная таблица, которая нам и нужна. Для проверки этого предположения отсчитаем 0x19 элементов верх от 4034F4h и попытаемся найти указатель, ссылающийся на ее начало. Если повезет и он окажется экземпляром класса CwinThread, тогда программа сможет корректно продолжить свою работу:

004050B8 00403490 00000001 00000000
004050C4 00000000 00000000 00000001

Листинг 21. Экземпляр класса CWinThread, вручную найденный нами в памяти,

Действительно, в памяти обнаруживается нечто похожее. Записываем в регистр ECX значение 4050B8h, находим в памяти функцию Run (как уже говорилось, если только она не была перекрыта, ее адрес - 6C299164h - известен). Нажимаем <Ctrl-G>, затем "0x6C299164" и в контекстном меню, вызванном правой клавишей мыши, выбираем "Set Next Statement". Программа, отделавшись легким испугом, продолжает свое исполнение, ну а мы на радостях идем пить пиво (кофе, квас, чай - по вкусу).

Аналогичным путем можно вернуть к жизни и зависшие приложения, потерявшие нить управления и не реагирующие ни на мышь, ни на клавиатуру.

Как подключить дамп памяти

Дамп памяти (memory dump, также называемый корой [от английского core - сердцевина], crash- или аварийным дампом), сброшенный системой при возникновении критической ошибки - не самое убедительное средство для выявления причин катастрофы, но ничего другого в руках администратора зачастую просто не бывает. Последний вздох операционной системы, похожий на дурно пахнущую навозную кучу, из которой высовывается чей-то наполовину разложившийся труп, мгновенным снимком запечатленный в момент неустранимого сбоя - вот что такое дамп памяти! Копание в нем вряд ли доставит вам удовольствие. Не исключено, что истинного виновника краха системы вообще не удастся найти. Допустим, некий некорректно работающий драйвер вторгся в область памяти, принадлежащую другому драйверу, и наглым образом затер критические структуры данных, сделав из чисел винегрет. К тому моменту, когда драйвер-жертва пойдет "в разнос", драйвер-хищник может быть вообще выгружен из системы и определить его причастность к крушению системы по одному лишь дампу практически нереально.

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

Первым делом необходимо войти в конфигурацию системы ("Панель управления" -> "Система"/"Control Panel" -> "System"") и убедиться, что настройки дампа соответствуют предъявляемым к ним требованиям ("Дополнительно" -> "Загрузка и восстановление" -> "Отказ системы"/"Startup/Shutdown" -> "Recovery" в Windows 2000 RUS и Windows NT 4.0 ENG соответственно). Операционная система Windows 2000 поддерживает три разновидности дампов памяти: малый дамп памяти (small memory dump), дамп памяти ядра (kernel memory dump) и полный дамп памяти (complete dump memory). Для изменения настроек дампа вы должны иметь права администратора.

Малый дамп памяти занимает всего лишь 64 Кб (а отнюдь не 2 Мб, как утверждает контекстная помощь) и включает в себя: а) копию голубого экрана смерти; б) перечень загруженных драйверов; в) контекст обрушившегося процесса со всеми его потоками; г) первые 16 Кб содержимого ядерного стека обрушившегося потока. Разочаровывающее малоинформативные сведения! Непосредственный анализ дампа дает нам лишь адрес возникновения ошибки и имя драйвера, к которому этот адрес принадлежит. При условии, что конфигурация системы не была изменена после возникновения сбоя, мы можем загрузить отладчик и дизассемблировать подозреваемый драйвер, но это мало что даст. Ведь содержимое сегмента данных на момент возникновения сбоя нам неизвестно, более того - мы не можем утверждать, что видим те же самые машинные команды, что вызвали сбой. Поэтому малый дамп памяти полезен лишь тем администраторам, которым достаточно одного лишь имени нестабильного драйвера. Как показывает практика, в подавляющем большинстве случаев этой информации оказывается вполне достаточно. Разработчикам драйвера отсылается гневный бан-рапорт (вместе с дампом!), а сам драйвер тем временем заменяется другим - более новым и надежным. По умолчанию малый дамп памяти записывается в директорию %SystemRoot%\Minidump, где ему присваивается имя "Mini", дата записи дампа и порядковый номер сбоя на данный день.

Дамп памяти ядра содержит намного более полную информацию о сбое и включает в себя всю память, выделенную ядром и его компонентами (драйверами, уровнем абстракции от оборудования и т.д.), а также копию экрана смерти. Размер дампа памяти ядра зависит от количества установленных драйверов и варьируется от системы к системе. Контекстная помощь утверждает, что эта величина составляет от 50 до 800 мегабайт. Ну, на счет 800 мегабайт авторы явно загнули, и объем в 50 - 100 мегабайт выглядит более вероятным (техническая документация на систему сообщает, что ориентировочный размер дампа ядра составляет треть объема физической оперативной памяти, установленной на системе). Это наилучший компромисс между накладными расходами на дисковое пространство, скорости сброса дампа и информативностью последнего. Весь джентльменский минимум информации - в вашем распоряжении. Практически все типовые ошибки драйверов и прочих ядерных компонентов могут быть локализованы с точностью до байта, включая и те, что вызваны физическом сбоем аппаратуры (правда, для этого вы должны иметь некоторый патологоанатомический опыт исследования трупных дампов системы). По умолчанию дамп памяти ядра записывается в файл %SystemRoot%\Memory.dmp, затирая или не затирая (в зависимости от текущих настроек "Системы") предыдущий дамп.

Полный дамп памяти включает в себя все содержимое физической памяти компьютера, занятое как прикладными, так и ядерными компонентами. Полный дамп памяти оказывается особенно полезным при отладке ASPI/SPTI приложений, которые в силу своей специфики могут уронить ядро даже с прикладного уровня. Несмотря на довольно большой размер, равный размеру оперативной памяти, полный дамп остается наиболее любимым дампом всех системных программистов (системные же администраторы в своей массе предпочитают малый дамп). Это не покажется удивительным, если вспомнить, что объемы жестких дисков давно перевалили за отметку 100 Гб, а оплата труда системных программистов за последние несколько лет даже несколько возросла. Лучше иметь невостребованный полный дамп под рукой, чем кусать локти при его отсутствии. По умолчанию полный дамп памяти записывается в файл %SystemRoot%\Memory.dmp, затирая или не затирая (в зависимости от текущих настроек "Системы") предыдущий дамп.

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

а) Комплект разработчика драйверов (Driver Development Kit или сокращенно DDK), бесплатно распространяемый фирмой Microsoft и содержащий в себе подробную техническую документацию по ядру системы; несколько компиляторов Си/Си++ и ассемблера, а также достаточно продвинутые средства анализа дампа памяти;

б) Драйвер W2K_KILL.SYS или любой другой драйвер-убийца операционной системы, например BDOS.EXE от Марка Русиновича, позволяющий получить дамп в любое удобное для нас время, не дожидаясь возникновения критической ошибки (бесплатную копию программы можно скачать с адреса http://www.sysinternals.com);

в) Файлы символьных идентификаторов (symbol files), необходимые отладчикам ядра для его нормального функционирования и делающие дизассемблерный код более наглядным. Файлы символьных идентификаторов входят в состав "зеленого" набора MSDN, но, в принципе, без них можно и обойтись, однако переменная окружения _NT_SYMBOL_PATH по любому должна быть определена, иначе отладчик i386kd.exe работать не будет;

г) Одна или несколько книжек, описывающих архитектуру ядра системы. Очень хороша в этом смысле "Внутреннее устройство Windows 2000" Марка Руссиновича и Дэвида Соломона, интересная как системным программистам, так и администраторам.

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

*** STOP: 0x0000001E (0xC0000005, 0xBE80B000, 0x00000000, 0x00000000) KMODE_EXEPTION_NOT_HALTED

*** Address 0xBE80B000 base at 0xBE80A000, Date Stamp 389db915 - w2k_kill.sys

Beginning dump of physical memory
Dumping physical memory to disk: 69


Рисунок 5. Голубой экран смерти (BSOD - Blue Screen Of Death), свидетельствующий о возникновении неустранимого сбоя системы с краткой информацией о нем.

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

Мы начнем с левого верхнего угла экрана и, зигзагами спускаясь вниз, трассируем все надписи по порядку.

Категория

Описание

Hex-код

Cимвольное имя

0x0A

IRQL_NOT_LESS_OR_EQUAL

Драйвер попытался обратиться к странице памяти на уровне DISPATCH_LEVEL или более высоком, что и привело к краху, поскольку менеджер виртуальной памяти работает на более низком уровне;

Источником сбоя может быть и BIOS, и драйвер, и системный сервис (особенно этим грешат вирусные сканеры и FM-тюнеры);

Как вариант - проверьте кабельные терминаторы на SCSI-накопителях и Master/Slayer на IDE, отключите кэширование памяти в BIOS;

Если и это не поможет, обратитесь к четырем параметрам Bug Check кода, содержащим ссылку на память, к которой осуществлялся доступ, уровень IRQ, тип доступа (чтение/запись) и адрес машинной инструкции драйвера.

0x1E

KMODE_EXCEPTION_NOT_HANDLED

Компонент ядра возбудил исключение и "забыл" его обработать; номер исключения содержится в первом Bug Check параметре. Обычно он принимает одно из следующих значений:

(0x80000003) STATUS_BREAKPOINT: встретилась программная точка останова - отладочный рудимент, по небрежности не удаленный разработчиком драйвера;

(0xC0000005) STATUS_ACCESS_VIOLATION: доступ к запрещенному адресу (четвертый Bug Check параметр уточняет, к какому) - ошибка разработчика;

(0xC000021A) STATUS_SYSTEM_PROCESS_TERMINATED: сбой процессов CSRSS и/или Winlogon, источником которого могут быть как компоненты ядра, так и пользовательские приложения. Обычно это происходит при заражении машины вирусом или нарушении целостности системных файлов;

(0xC0000221) STATUS_IMAGE_CHECSUM_MISMATCH: целостность одного из системных файлов оказалась нарушена;

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

0x24

NTFS_FILE_SYSTEM

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

0x2E

DATA_BUS_ERROR

Драйвер обратился к несуществующему физическому адресу; если только это не ошибка драйвера, оперативная память и/или кэш-память процессора (видеопамять) неисправны или же работают на запредельных тактовых частотах.

0x35

NO_MORE_IRP_STACK_LOCATIONS

Драйвер более высокого уровня обратился к драйверу более низкого уровня через IoCallDriver-интерфейс, однако свободного пространства в IRP-стеке не оказалось и передать весь IRP-пакет целиком не удалось;

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

0x3F

NO_MORE_SYSTEM_PTES

Результат сильной фрагментации таблицы PTE, приводящей к невозможности выделения затребованного драйвером блока памяти; обычно это характерно для аудио/видео-драйверов, манипулирующих огромными блоками памяти и к тому же не всегда их вовремя освобождающих; для решения проблемы попробуйте увеличить кол-во PTE (до 50000 максимум) в следующей ветке реестра: HLLM\SYSTEM\CurrentControlSet\Control\Session-Manager\Memory Management\SystemPages

0x50

PAGE_FAULT_IN_NONPAGED_AREA

Обращение к несуществующей странице памяти, вызванное либо неисправностью оборудования (как правило - оперативной, видео или кэш-памяти), либо некорректно спроектированным сервисом (этим грешат многие антивирусы, в том числе Касперский и Доктор Веб), либо разрушениями NTFS-тома (запустите chkdsk с ключами /f и /r), также попробуйте запретить кэширование памяти в BIOS.

0x58

FTDISK_INTERNAL_ERROR

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

0x76

PROCESS_HAS_LOCKED_PAGES

Драйвер не смог освободить залоченные страницы после завершения операции ввода-вывода; для определения имени дефектного драйвера следует обратиться к ветке HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management и установить параметр TrackLockedPages типа DWORD в значение 1, потом перезагрузить систему, после чего та будет сохранять трассируемый стек и, если нехороший драйвер вновь начнет чудить, возникнет BSOD с Bug Check кодом 0xCB, позволяющим определить виновника.

0x77

KERNEL_STACK_INPAGE_ERROR

Страница данных памяти ядра по техническим причинам недоступна, если первый Bug Check код не равен нулю, то он может принимать одно из следующих значений:

(0xC000009A) STATUS_INSUFFICIENT_RESOURCES - недостаточно системных ресурсов;

(0xC000009C) STATUS_DEVICE_DATA_ERROR - ошибка чтения с диска (bad-сектор?);

(0xC000009D) STATUS_DEVICE_NOT_CONNECTED - система не видит привод (неисправность контроллера, плохой контакт шлейфа);

(0xC000016A) STATUS_DISK_OPERATION_FAILED - ошибка чтения диска (bad-сектор или неисправный контроллер);

(0xC0000185) STATUS_IO_DEVICE_ERROR - неправильное терминирование SCSI-привода или конфликт IRQ IDE-приводов;

Нулевое же значение первого Bug Check кода указывает на неизвестную аппаратную проблему;

Такое сообщение может появляться и при заражении системы вирусами, и при разрушении диска старыми докторами, и при отказе RAM - войдите в консоль восстановления и запустите ChkDsk с ключом /r.

0x7A

KERNEL_DATA_INPAGE_ERROR###

Страница данных памяти ядра по техническим причинам недоступна, второй Bug Check параметр содержит статус обмена, четвертый - виртуальный страничный адрес, загрузить который не удалось.

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

0x7B

INACCESSIBLE_BOOT_DEVICE

Загрузочное устройство недоступно - таблица разделов повреждена или не соответствует файлу boot.ini.

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

Также не помешает проверить общую исправность оборудования и наличие вирусов на диске.

0x7F

UNEXPECTED_KERNEL_MODE_TRAP

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

Проверьте исправность оборудования и удалите все посторонние драйвера.

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

0x00 - попытка деления на нуль;

0x01 - исключение системного отладчика;

0x03 - исключение точки останова;

0x04 - переполнение;

0x05 - генерируется инструкцией BOUND;

0x06 - неверный опкод;

0x07 - двойной отказ (Double Fault);

Описание остальных исключений содержится в документации на Intel и AMD процессоры.

0xC2

BAD_POOL_CALLER

Текущий поток вызвал некорректный pool-request, что обычно происходит по причине алгоритмической ошибки, допущенной разработчиком драйвера. однако, судя по всему, и сама система не остается без ошибок, поскольку для устранения этого голубого экрана Microsoft рекомендует установить SP2.

0xCB

DRIVER_LEFT_LOCKED_PAGES_IN_PROCESS

После завершения процедуры ввода/вывода драйвер не может освободить заблокированные страницы (см. PROCESS_HAS_LOCKED_PAGES)

Первый Bug Check параметр содержит вызываемый, а второй Bug Check параметр вызывающий адрес. последний, четвертый параметр указывает на UNICODE-строку с именем драйвера.

0xD1

DRIVER_IRQL_NOT_LESS_OR_EQUAL

То же самое, что и IRQL_NOT_LESS_OR_EQUAL

0xE2

MANUALLY_INITIATED_CRAS

Сбой системы, спровоцированный вручную, путем нажатия горячей комбинации клавиш <Ctrl+Scroll Loock>, при условии, что параметр CrashOnCtrlScroll реестра

HKLM\System\CurrentControlSet\Services\i8042prt\Parameters содержит ненулевое значение

0x7A

KERNEL_DATA_INPAGE_ERROR

Страница данных памяти ядра по техническим причинам недоступна, второй Bug Check параметр содержит статус обмена, четвертый - виртуальный страничный адрес, загрузить который не удалось.

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

Таблица 1. Физический смысл наиболее популярных Bug Check кодов с краткими пояснениями. Определение рейтинга популярности Bug Check кодов осуществлялось путем подсчета упоминаний о них в конференциях Интернет (спасибо старику Googl'у).

Восстановление системы после критического сбоя

Операционные системы семейства NT достаточно безболезненно переносят критические сбои, даже если те произошли в самое неудобный момент времени (например в период дефрагментации диска). Отказоустойчивый драйвер файловой системы все сделает сам (хотя запустить ChkDsk все же не помешает).

Если был выбран "полный дамп памяти" или "дамп памяти ядра", то при следующей успешной загрузке системы жесткий диск будет долго молотить головкой, даже если к нему и не происходит никаких обращений. Не пугайтесь! Просто Windows перемещает дамп из виртуальной памяти на место его постоянного проживания. Запустив "Диспетчер Задач", вы увидите новый процесс в списке - SaveDump.exe, - вот он этим и занимается. Необходимость в подобной двухтактной схеме сброса дампа объясняется тем, что в момент возникновения критической ошибки работоспособность драйверов файловой системы уже не гарантируется и операционная система не может позволить себе их использовать, ограничиваясь временным размещением дампа в виртуальной памяти. Кстати, если имеющегося объема виртуальной памяти окажется недостаточно (Дополнительно -> Параметры быстродействия -> Виртуальная память), сброс дампа окажется невозможным.

Если же система от загрузки отказывается, упорно забрасывая вас голубыми экранами смерти, вспомните о существовании клавиши <F8> и выберите пункт "Загрузка последней удачной конфигурации " ("Last Known Good Configuration"). Более радикальной мерой является запуск системы в безопасном (safe) режиме с минимумом загружаемых служб и драйверов. Переустановка системы - это крайняя мера и без особой нужны к ней лучше не прибегать. Лучше войдите в "консоль восстановления" и перетащите файл дампа на другую машину для его исследования.

Подключение дампа памяти

Для подключения дампа памяти к отладчику Windows Debugger (windbg.exe) в меню "File" выберите пункт "Crash Dump" или воспользуйтесь горячей комбинацией <Ctrl-D>. В отладчике i386kd.exe для той же цели служит ключ "-z" командной строки, за которым следует полный путь к файлу дампа, отделенный от ключа одним или несколькими пробелами, при этом переменная окружения _NT_SYMBOL_PATH должна быть определена и содержать полный путь к файлам символьных идентификаторов, в противном случае отладчик аварийно завершит свою работу. Как один из вариантов, можно указать к командной строке ключ "-y" и тогда экран консоли будет выглядеть так: "i386kd -z C:\WINNT\memory.dmp -y C:\WINNT\Symbols", причем отладчик следует вызывать из Checked Build Environment/Free Build Environment консоли, находящейся в папке Windows 2000 DDK, иначе у вас ничего не получится.

Хорошая идея - ассоциировать dmp-файлы с отладчиком i386kd, запуская их одним ударом Enter'а из FAR'a. Впрочем, выбор средства анализа - дело вкуса. Кому-то нравится KAnalyze, а кому-то достаточно и простенького DumpChk. Выбор аналитических инструментов чрезвычайно велик (один лишь DDK содержит четыре из них!) и чтобы хоть как-то определиться с выбором, мы остановимся на i386kd.exe, также называемом Kernel Debugger'ом.

Как только консоль отладчика появится на экране (а Kernel Debugger - это консольное приложение, горячо любимое всеми, кто провел свою молодость за текстовыми терминалами), курсор наскоро дизассемблирует текущую машинную инструкцию и своим тревожным мерцанием затягивает нас в пучину машинного кода. Ну что, глазки строить будем или все-таки дизассемблировать? - незлобно ворчим мы, выбивая на клавиатуре команду "u", заставляющую отладчик продолжить дизассемблирование.

Судя по символьным идентификаторам PspUnhandledExceptionInSystemThread и KeBugCheckEx мы находимся глубоко в ядре, а точнее - в окрестностях того кода, что выводит BSOD на экран:

8045249c 6a01          push     0x1
kd>u
_PspUnhandledExceptionInSystemThread@4:
80452484 8B442404      mov      eax, dword ptr [esp+4]
80452488 8B00          mov      eax, dword ptr [eax]
8045248A FF7018        push     dword ptr [eax+18h]
8045248D FF7014        push     dword ptr [eax+14h]
80452490 FF700C        push     dword ptr [eax+0Ch]
80452493 FF30          push     dword ptr [eax]
80452495 6A1E          push     1Eh
80452497 E8789AFDFF    call     _KeBugCheckEx@20
8045249C 6A01          push     1
8045249E 58            pop      eax
8045249F C20400        ret      4

Листинг 22. Результат дизассемблирования подключенного дампа памяти с текущего адреса.

В стеке ничего интересного также не содержится, вот судите сами (просмотр содержимого стека осуществляется командной "kb"):

kd> kb
ChildEBP RetAddr  Args to Child
f403f71c 8045251c f403f744 8045cc77 f403f74c ntoskrnl!PspUnhandledExceptionInSystemThread+0x18
f403fddc 80465b62 80418ada 00000001 00000000 ntoskrnl!PspSystemThreadStartup+0x5e
00000000 00000000 00000000 00000000 00000000 ntoskrnl!KiThreadStartup+0x16

Листинг 23. Содержимое стека не дает никаких намеков на природу истинного виновника.

Такой поворот событий ставит нас в тупик. Сколько бы мы ни дизассемблировали ядро, это ни на йоту не приблизит нас к источнику критической ошибки. Что ж, все вполне логично. Текущий адрес (8045249Сh) лежит далеко за пределами драйвера-убийцы (0BE80A00h). Хорошо, давайте развернемся и пойдем другим путем. Помните тот адрес, что высвечивал голубой экран смерти? Не помните - не беда! Если это только не запрещено настройками, копии всех голубых экранов сохраняются в Журнале системы. Откроем его ("Панель управления" а "Администрирование" а "Просмотр событий"):

Компьютер был перезагружен после критической ошибки:
0x0000001e (0xc0000005, 0xbe80b000, 0x00000000, 0x00000000).
Microsoft Windows 2000 [v15.2195]
Копия памяти сохранена: C:\WINNT\MEMORY.DMP.

Листинг 24. Копия голубого экрана смерти, сохраненная в системном журнале.

Отталкиваясь от категории критической ошибки (0x1E), мы без труда сможем определить адрес инструкции-убийцы - 0xBE80B000 (в приведенном выше листинге он выделен жирным шрифтом). Даем команду "u BE80B000" для просмотра его содержимого и видим:

kd>u 0xBE80B000
be80b000 a100000000    mov   eax,[00000000]
be80b005 c20800        ret   0x8
be80b008 90            nop
be80b009 90            nop
be80b00a 90            nop
be80b00b 90            nop
be80b00c 90            nop
be80b00d 90            nop

Листинг 25. Результат дизассемблирования дампа памяти по адресу, сообщенному голубым экраном смерти.

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

Хорошо, а как быть, если копии экрана смерти в нашем распоряжении нет? На самом деле, синий экран всегда с нами, надо только знать где искать! Попробуйте открыть файл дампа в любом hex-редакторе и вы обнаружите следующие строки:

00000000: 50 41 47 45 44 55 4D 50 ¦ 0F 00 00 00 93 08 00 00 PAGEDUMP0
00000010: 00 00 03 00 00 80 8B 81 ¦ C0 A4 46 80 80 A1 46 80 ¦ АЛБLдFААбFА
00000020: 4C 01 00 00 01 00 00 00 ¦ 1E 00 00 00 05 00 00 C0 LO O ^ ¦ L
00000030: 00 B0 80 BE 00 00 00 00 ¦ 00 00 00 00 00 41 47 45 -А- AGE

Листинг 26. Копия голубого экрана в заголовке дампа программы.

С первого же взгляда удается опознать все основные Bug Check параметры: 1E 00 00 00 - это код категории сбоя 0x1E (на x86-процессорах наименее значимый байт располагается по меньшему адресу, то есть все числа записываются задом наперед); 05 00 00 C0 - код исключения ACCESS VIOLATION; а 00 B0 80 BE - и есть адрес машинной команды, породившей это исключение. В комбинации же 0F 00 00 00 93 08 легко узнается номер билда системы, стоит только записать его в десятичной нотации.

Для просмотра Bug Check параметров в более удобочитаемом виде можно воспользоваться следующей командой отладчика: "dd KiBugCheckData":

kd> dd KiBugCheckData
dd KiBugCheckData
8047e6c0  0000001e c0000005 be80b000 00000000
8047e6d0  00000000 00000000 00000001 00000000
8047e6e0  00000000 00000000 00000000 00000000
8047e6f0  00000000 00000000 00000000 00000000
8047e700  00000000 00000000 00000000 00000000
8047e710  00000000 00000000 00000000 00000000
8047e720  00000000 00000000 00000000 00000000
8047e730  00000000 e0ffffff edffffff 00020000

Листинг 27. Bug Check параметры, отображаемые в удобочитаемом виде.

Другие полезные команды: "!drivers" - выводящая список драйверов, загруженных на момент сбоя, "!arbiter" - показывающая всех арбитров вместе с диапазонами арбитража, "!filecache" - отображающая информацию о кэше файловой системы и PT, "!vm" - отчитывающаяся об использовании виртуальной памяти и т.д. и т.п. - всех не перечислишь! (Полный перечень команд вы найдете в руководстве по своему любимому отладчику).

Конечно, в реальной жизни определить истинного виновника краха системы намного сложнее, поскольку всякий нормальный драйвер состоит из множества сложновзаимодействующих функций, образующих запутанные иерархические комплексы, местами пересеченные туннелями глобальных переменных, превращающих драйвер в самый настоящий лабиринт. Приведем только один пример. Конструкция вида "mov eax, [ebx]", где ebx = 0, работает вполне нормально, послушно возбуждая исключение, и пытаться поговорить с ней по-мужски бессмысленно! Нужно найти тот код, который записывает в EBX нулевое значение, и сделать это непросто. Можно, конечно, просто прокрутить экран вверх, надеясь, что на данном участке программный код выполнялся линейно, но никаких гарантий, что это действительно так, у нас нет, равно как нет и возможности обратной трассировки (back trace). Грубо говоря, адрес предшествующей машинной инструкции нам неизвестен и закладываться на прокрутку экрана нельзя!

Загрузив подопытный драйвер в любой интеллектуальный дизассемблер, автоматически восстанавливающий перекрестные ссылки (например IDA PRO), мы получим более или менее полное представление о топологии управляющих ветвей программы. Конечно, дизассемблирование в силу своей статической природы, не гарантирует, что управление не перекинулось откуда-то еще, но по крайней мере сужает круг поиска. Вообще же, о дизассемблировании написано множество хороших книг (и "Фундаментальные основы хакерства" Криса Касперски в том числе), поэтому не будем останавливаться на этом вопросе, а просто пожелаем всем читателям удачи.

Отладчик i386kd за работой

Рисунок 6. Отладчик i386kd за работой (пиво, бутерброды прилагаются); несмотря на свою отталкивающую внешность это чрезвычайно мощный и удобный в работе инструмент, позволяющих проворачивать умопомрачительные пассажи нажатием всего пары-тройки клавиш (одна из которых вызывает ваш собственный скрипт).

Windbg c загруженным дампом

Рисунок 7. Windbg c загруженным дампом памяти. Обратите внимание - отладчик самостоятельно высвечивает Bug Check коды, не ожидая, пока мы об этом его попросим, а при попытке дизассемблирования инструкции, возбудившей исключение, на экране выскакивает "Module Load: W2K_KILL.SYS", сообщая нам имя драйвера-убийцы. Вроде бы мелочь, а как приятно!