Sysinternals ProcDump v4.0

Написание плагина для Sysinternals ProcDump v4.0

Эндрю Ричардс

Продукты и технологии:

Sysinternals ProcDump v4.0, Sysinternals VMMap

В статье рассматриваются:

• терминология, связанная с набором дампов;

• функция MiniDumpWriteDump;

• записи контекста исключения;

• исключения периода выполнения и посмертный отладчик (post mortem debugger);

• функция MiniDumpCallback;

• применение Sysinternals VMMap для анализа памяти приложения.

Исходный код можно скачать по ссылке code.msdn.microsoft.com/mag201112ProcDump.

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

Получение дампа памяти приложения в такие моменты — стандартная тактика анализа проблем, будь то зависание, крах или падение производительности. Большинство утилит для получения дампов памяти следуют принципу «все или ничего», т. е. дают либо всю информацию (полные дампы), либо очень мало (мини-дампы). Мини-дампы обычно настолько малы, что плодотворный анализ невозможен из-за отсутствия динамически распределяемой памяти (куч). Полные дампы всегда предпочтительнее, но в настоящее время этот вариант используют редко. Постоянно растущие объемы памяти означают, что получение полного дампа может занимать и 15, и 30, и даже 60 минут. Более того, файлы дампов становятся настолько большими, что их не так-то легко передавать для анализа даже в сжатом виде.

В прошлом году в утилите Sysinternals ProcDump v3.0 был введен ключ MiniPlus (–mp), ориентированный на решение этой проблемы для неуправляемых приложений. При указании этого ключа создается дамп, размер которого находится где-то посередине между размерами мини-дампа и полного дампа. Решения о включении тех или иных областей памяти при использовании ключа MiniPlus основываются на множестве эвристических алгоритмов, учитывающих тип памяти, атрибуты ее защиты, размер выделяемой памяти (allocation size), размер региона и содержимое стека. В зависимости от структуры целевого приложения файл этого дампа может быть на 50–95% меньше полного дампа. Еще важнее, что для большинства задач анализа этот дамп столь же функционален, что и полный дамп. Когда ключ MiniPlus применяется к Microsoft Exchange 2010 Information Store, занимающему 48 Гб памяти, на выходе получается файл дампа размером 1–2 Гб (сокращение на 95%).

Марк Руссинович и я работали над новым выпуском утилиты ProcDump, которая теперь позволяет принимать  решения о включении той или иной памяти вам. Sysinternals ProcDump v4.0 предоставляет через ключ –d тот же API, который MiniPlus использует на внутреннем уровне как внешний плагин на основе DLL.

В этой статье я подробно расскажу о том, как работает Sysinternals ProcDump v4.0, создав серию приложений-примеров, которые расширяют друг друга и реализуют все больше и больше функциональности ProcDump. Углубившись во внутреннее устройство ProcDump, мы рассмотрим, как написать плагин, способный взаимодействовать с ProcDump и нижележащим DbgHelp API.

В пакете исходного кода содержатся приложения-примеры, а также набор приложений, которые «падают» по разным вариантам, чтобы вы могли протестировать свой код. В примере MiniDump05 все API-функции реализованы как автономное приложение. Пример MiniDump06 реализует MiniDump05 как плагин для Sysinternals ProcDump v4.0.

Терминология

В терминах, связанных с иерархией дампов, можно очень легко запутаться из-за слишком частого употребления слова «мини». Существуют файловый формат MiniDump, дампы Mini и MiniPlus, а также функции MiniDumpWriteDump и MiniDumpCallback.

Windows поддерживает файловый формат MiniDump через DbgHelp.dll. Файл дампа MiniDump (*.dmp) — это контейнер, содержащий частичный или полный снимок памяти мишени пользовательского режима или режима ядра. Этот формат файлов поддерживает использование «потоков» для хранения дополнительных метаданных (комментариев, статистики процессов и т. д.). Название этого формата файлов прямо связано с требованием поддержки захвата минимального объема данных. API-функции MiniDumpWriteDump и MiniDumpCallback из DbgHelp имеют префиксы «MiniDump», соответствующие формату файлов, которыми они оперируют.

Mini, MiniPlus и Full используются для описания разных объемов контента в файлах дампов. Mini — самый малый (минимальный) и включает блок переменных окружения процесса (process environment block, PEB), блоки переменных окружения потоков (thread environment blocks, TEB), частичные стеки, загруженные модули и сегменты данных. Марк и я придумали название «MiniPlus», чтобы описать содержимое памяти, захватываемое Sysinternals ProcDump с ключом –mp; оно включает содержимое мини-дампа (Mini) плюс память, которая была определена как важная с помощью эвристических методов. Ну а полный дамп (Full) (procdump.exe –ma) включает все виртуальное адресное пространство процесса независимо от того, была ли ему передана физическая память из RAM.

Функция MiniDumpWriteDump

Чтобы получить мини-дамп памяти процесса в файловом формате MiniDump и записать его в файл, вы вызываете DbgHelp-функцию MiniDumpWriteDump. Этой функции нужен описатель целевого процесса (с правами доступа PROCESS_QUERY_INFORMATION и PROCESS_VM_READ), PID целевого процесса, описатель файла (с правами доступа FILE_GENERIC_WRITE), битовая маска флагов MINIDUMP_TYPE и три необязательных параметра: структура Exception Information (используется для включения записи контекста исключения), структура User Stream Information (с ее помощью обычно включают комментарий в дамп через типы CommentStreamA/W MINIDUMP_STREAM_TYPE) и структура Callback Information (позволяет модифицировать то, что захватывается при вызове):

Приложение-пример MiniDump01 (рис. 1) показывает, как вызвать MiniDumpWriteDump, чтобы получить мини-дамп безо всяких необязательных параметров. Оно начинает с проверки аргументов командной строки в поисках PID, а потом вызывает OpenProcess, чтобы получить описатель целевого процесса. Затем оно вызывает CreateFile для получения описателя файла. (Заметьте, что MiniDumpWriteDump поддерживает любую мишень ввода-вывода.) Файлу присваивается имя на основе даты и времени в формате ISO для уникальности и сортировки в хронологическом порядке: C:\dumps\minidump_YYYY-MM-DD_HH-MM-SS-MS.dmp. Каталог C:\dumps «зашит» в код, чтобы гарантировать все необходимые права доступа для записи. Это необходимо при выполнении посмертной отладки, так как текущий каталог (например, System32) может оказаться недоступным для записи.

Рис. 1. MiniDump01.cpp

Параметр DumpType — битовая маска на основе MINIDUMP_TYPE, которая определяет включение (или исключение) конкретных типов памяти. Флаги MINIDUMP_TYPE обеспечивают весьма широкие возможности и позволяют указывать захват многих регионов памяти без дополнительного кодирования с применением обратного вызова. Параметры, используемые в примере MiniDump01, идентичны применяемым ProcDump. Они создают (Mini) дамп, который можно использовать для общего анализа процесса.

В DumpType всегда присутствует флаг MiniDumpNormal, так как его значение равно 0x00000000. Здесь был использован DumpType, который включает все стеки (MiniDumpNormal), всю информацию PEB и TEB (MiniDumpWithProcessThreadData), все сведения о загруженных модулях и любых глобальных переменных (MiniDumpWithDataSegs), все сведения об описателях (MiniDumpWithHandleData), информацию обо всех регионах памяти (MiniDumpWithFullMemoryInfo) и данные о времени выполнения и привязках к процессорам всех потоков (MiniDumpWithThreadInfo). При установке этих флагов создаваемый дамп является более полной версией мини-дампа, но все еще остается довольно небольшим (менее 30 Мб даже для самого большого приложения). Команды отладчика, поддерживаемые этими флагами MINIDUMP_TYPE, перечислены в табл. 1.

Табл. 1. Команды отладчика

MINIDUMP_TYPE Команды отладчика
MiniDumpNormal knL99
MiniDumpWithProcessThreadData !peb, !teb
MiniDumpWithDataSegs lm, dt <global>
MiniDumpWithHandleData !handle, !cs
MiniDumpWithFullMemoryInfo !address
MiniDumpWithThreadInfo !runaway

При использовании MiniDumpWriteDump получаемый дамп соответствует архитектуре программы захвата, а не мишени, поэтому для 32-разрядного процесса используйте 32-разрядную версию своей программы захвата дампа, а для 64-разрядного процесса — 64-разрядную версию. Если вам нужно отлаживать «32-разрядную Windows в 64-разрядной Windows» (WOW64), следует использовать 64-разрядный дамп 32-разрядного процесса.

Если архитектуры не совпадают (случайно или намеренно), вы должны сменить рабочую машину (effective machine) (.effmach x86) в отладчике, чтобы получить доступ к 32-разрядным стекам в 64-разрядном дампе. Учтите, что в этом варианте многие расширения отладчика не работают.

Запись контекста исключения

Инженеры техподдержки Microsoft используют термины «дамп зависания» (hang dump) и «дамп краха» (crash dump). Запрашивая дамп краха, они хотят получить дамп с записью контекста исключения (exception context record). А запрашивая дамп зависания, они (как правило) имеют в виду серию дампов без такой записи. Однако дамп с информацией об исключении не всегда относится ко времени краха. Информация об исключении — просто способ записи дополнительной информации в дамп. В этом отношении информация пользовательского потока данных (user stream infor­mation) аналогична информации об исключении.

Запись контекста исключения — это комбинация структур CONTEXT (содержимое регистров процессора) и EXCEPTION_RECORD (код исключения, адрес инструкции и др.). Если вы включаете эту запись в дамп и выполняете .ecxr, то текущему контексту отладчика (состоянию потока и регистров процессора) присваивается инструкция, которая вызвала исключение (рис. 2).

Рис. 2. Смена контекста на запись контекста исключения

Для поддержки .ecxr в MiniDumpWriteDump нужно передать необязательную структуру MINIDUMP_EXCEPTION_­INFORMATION. Вы можете получить информацию об исключении в период выполнения или посмертно.

Исключения периода выполнения

Если вы реализуете цикл обработки событий отладчика, при возникновении исключения вам передается информация об этом исключении. Этот цикл будет принимать структуру EXCEPTION_DEBUG_EVENT для точек прерывания, исключений первого шанса (first chance exceptions) и исключений второго шанса (second chance exceptions).

Приложение-пример MiniDump02 показывает, как вызывать MiniDumpWriteDump из цикла обработки событий отладчика, чтобы в дамп включалась запись контекста исключения второго шанса (эквивалент «procdump.exe –e»). Эта функциональность работает, когда вы используете ключ –e. Поскольку весь код довольно длинный, на рис. 3 представлен псевдокод этого приложения. Полный исходный код см. в пакете для скачивания к этой статье.

Рис. 3. Псевдокод MiniDump02

Приложение начинает с поиска PID в аргументах командной строки. Затем вызывает OpenProcess, чтобы получить описатель целевого процесса, потом вызывает CreateFile, чтобы получить описатель файла. Если ключа –e нет, приложение создает дамп зависания, как и раньше. Если же ключ –e присутствует, приложение подключается к мишени (как отладчик), используя DebugActiveProcess. В цикле while приложение ждет возврата структуры DEBUG_EVENT от WaitForDebugEvent. Выражение switch использует член dwDebugEventCode структуры DEBUG_EVENT. После получения дампа или завершения процесса вызывается DebugActiveProcessStop для отключения от мишени.

EXCEPTION_DEBUG_EVENT в структуре DEBUG_EVENT содержит запись исключения внутри исключения. Если запись исключения является точкой прерывания, она обрабатывается локально вызовом ContinueDebugEvent сDBG_CONTINUE. Если это исключение первого шанса, оно не обрабатывается, чтобы его можно было преобразовать в исключение второго шанса (если в мишени нет обработчика). Для этого вызывается ContinueDebugEvent сDBG_EXCEPTION_NOT_HANDLED. Последний вариант — исключение второго шанса. Используя dwThreadId структуры DEBUG_EVENT, вызывается OpenThread, чтобы получить описатель потока с исключением. Описатель потока используется с GetThreadContext для заполнения обязательной структуры CONTEXT. (Здесь будьте осторожны: размер структуры CONTEXT периодически растет из-за добавления в процессоры дополнительных регистров. Если в ОС более поздней версии размер структуры CONTEXT увеличился, вам придется перекомпилировать этот код.) Полученные структура CONTEXT и EXCEPTION_RECORD из DEBUG_EVENT используются для заполнения структуры EXCEPTION_POINTERS, а она применяется для заполнения структуры MINIDUMP_EXCEPTION_INFORMATION. Эта структура передается функции WriteDump приложения для использования с MiniDumpWriteDump.

EXIT_PROCESS_DEBUG_EVENT обрабатывается специфическим образом в ситуации, когда мишень завершается до возникновения исключения. Чтобы принять это событие, вызывается ContinueDebugEvent с DBG_CONTINUE и происходит выход из цикла while.

События CREATE_PROCESS_DEBUG_EVENT и LOAD_DLL_DEBUG_EVENT обрабатывается специфическим образом, когда нужно закрыть HANDLE. В этом случае вызывается ContinueDebugEvent с DBG_CONTINUE.

Блок Default в выражении switch обрабатывает все события, вызывая Continue­DebugEvent с DBG_CONTINUE для продолжения выполнения и закрытия переданного описателя.

Посмертные исключения

В Windows Vista был введен третий параметр в командную строку посмертного отладчика (post mortem debugger) для поддержки передачи информации об исключении. Чтобы принять третий параметр, у вас должен быть параметр Debugger (в разделах реестра AeDebug), который включает три подстановки %ld. Эти три значения таковы: Process ID, Event ID и JIT Address. Последнее значение — JIT Address — это адрес структуры JIT_DEBUG_INFO в адресном пространстве мишени. Windows Error Reporting (WER) выделяет соответствующую область памяти в адресном пространстве мишени, когда WER вызывается в результате появления необработанного исключения. Оно помещается в структуру JIT_DEBUG_INFO, вызывая посмертный отладчик (с передачей адреса этой области памяти); при завершении посмертного отладчика эта память освобождается.

Чтобы определить запись контекста исключения, посмертное приложение считывает структуру JIT_DEBUG_INFO из адресного пространства мишени. В этой структуре содержатся адреса структур CONTEXT и EXCEPTION_RECORD в адресном пространстве мишени. Вместо чтения структур CONTEXT и EXCEPTION_RECORD из адресного пространства мишени я просто заполняю структуру EXCEPTION_POINTERS этими адресами, а затем присваиваю TRUE члену ClientPointers структуры MINIDUMP_EXCEPTION_INFORMATION. Тем самым всю черновую работу я перекладываю на отладчик. Он считывает данные из адресного пространства мишени (и автоматически учитывает архитектурные различия, чтобы можно было создавать 64-разрядный дамп 32-разрядного процесса).

Приложение-пример MiniDump03 демонстрирует, как реализовать поддержку JIT_DEBUG_INFO(рис. 4).

Рис. 4. MiniDump03: обработчик JIT_DEBUG_INFO

Когда приложение запускается как посмертный отладчик, оно отвечает за принудительное завершение процесса вызовом TerminateProcess. В примереMiniDump03 функция TerminateProcess вызывается после создания дампа:

Для замены посмертного отладчика своим приложением вы указываете в параметре Debugger в разделах AeDebug на посмертное приложение с соответствующей архитектурой. Используя подходящее по архитектуре приложение, вы избавляетесь от необходимости настройки рабочей машины (.effmach) в отладчике:

Функция MiniDumpCallback

До сих пор создаваемые дампы содержали память, на включение которой указывал параметр DumpType (и дополнительно включалась запись контекста исключения). Реализовав прототип функции MiniDumpCallback, мы можем добавлять не только дополнительные регионы памяти, но и кое-какую обработку ошибок. Немного позже я опишу, как можно реализовать прототип функции MiniDumpCallback исключительно для использования с Sysinternals ProcDump v4.0.

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

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

Рис. 5. Реализация в шаблоне прототипа функции MiniDumpCallback

Пример MiniDump04 содержит обратный вызов, который делает две вещи: он включает содержимое всего стека и игнорирует ошибки чтения. В этом примере для включения всего стека используются ThreadCallback и MemoryCallback, а для игнорирования ошибок чтения применяется ReadMemoryFailureCallback.

Чтобы запустить этот обратный вызов, в функцию MiniDumpWriteDump передается необязательная структура MINIDUMP_CALLBACK_­INFORMATION. Ее член CallbackRoutine указывает на реализованную функцию MiniDumpCallback (в моем шаблоне и примере — MiniDumpCallbackRoutine). Член CallbackParam является указателем VOID*, который позволяет сохранять контекст между вызовами обратного вызова. Моя функция WriteDump из примера Mini­Dump04 показана на рис. 6. Структура, которую я определил для сохранения контекста между вызовами (MemoryInfoNode), — узел связанного списка, содержащий адрес и размер:

Рис. 6. WriteDump из примера MiniDump04

Полный стек

Когда в параметре DumpType указывается флаг MiniDumpWithProcessThreadData, содержимое каждого стека включается из регистра базы стека в текущий указатель стека. Моя функция MiniDumpCallbackRoutine, реализованная в примере MiniDump04, дополняет его, включая оставшуюся часть стека. За счет охвата всего стека вам, возможно, удастся определить источник, замусоривающий стек.

Замусоривание стека — ситуация, при которой в буфере на основе стека происходит переполнение. При переполнении буфера запись выходит за адрес возврата стека и вызывает выполнение кода операции ret с выталкиванием (POP) содержимого буфера как указателя команд вместо того указателя команд, заталкивание (PUSH) которого было выполнено кодом операции call. Это приводит к выполнению по недопустимому адресу памяти или, что еще хуже, к выполнению случайного блока кода.

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

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

Рис. 7. Сравнение содержимого стеков

Первая часть кода «полного стека» — это обработка обратного вызова типа ThreadCallback (рис. 8). Этот тип обратного вызова вызывается по одному разу для каждого потока в процессе. Ему передается структура MINIDUMP_THREAD_CALLBACK через параметр CallbackInput. Эта структура включает член StackBase, который содержит базу (базовый адрес) стека для потока. Член StackEnd — это текущий указатель стека (esp/rsp для x86/x64 соответственно). Данная структура не включает лимит стека (это часть TEB).

Рис. 8. ThreadCallback используется для получения региона стека каждого потока

В примере используется упрощенный подход и предполагается, что размер стека равен 1 Мб — это значение по умолчанию для большинства приложений. Если это ниже указателя стека, параметр DumpType вызовет включение соответствующей памяти. Еслиже стек больше 1 Мб, произойдет включение части стека. А в случае, когда стек меньше 1 Мб, просто включаются дополнительные данные. Заметьте: если диапазон памяти, запрошенный обратным вызовом, охватывает свободный регион или перекрывается с другим включением, никаких ошибок не происходит.

StackBase и смещение в 1 Мб записываются в новый экземпляр определенной мной структуры MemoryInfoNode. Новый экземпляр добавляется в начало связанного списка, который передается обратному вызову в аргументе CallbackParam. После нескольких вызовов ThreadCallback связанный список содержит несколько узлов дополнительной памяти, которую нужно включить в дамп.

Последняя часть кода «полного стека» — обработка обратного вызова типа MemoryCallback (рис. 9). MemoryCallback вызывается постоянно, пока вы получаете TRUE от этого вызова, и предоставляет ненулевое значение для членов MemoryBase и MemorySize структуры MINIDUMP_CALLBACK_OUTPUT.

Рис. 9. MemoryCallback вызывается постоянно, пока не будет пройден весь регион стека

Этот код присваивает значения параметру CallbackOutput, а затем удаляет узел из связанного списка. После нескольких вызовов MemoryCallback в связанном списке не остается узлов, и возвращаются нулевые значения, на основании чего вызовы MemoryCallback прекращаются. Заметьте, что при передаче члены MemoryBase и MemorySize устанавливаются в 0; вам нужно просто вернуть TRUE.

Чтобы увидеть все регионы памяти в дампе, вы можете использовать область MemoryListStream в выводе команды .dumpdebug (рис. 10) (обратите внимание на то, что смежные блоки могут объединяться).

Рис. 10. Вывод команды .dumpdebug

Ошибка чтения памяти

Последняя часть кода довольно проста (рис. 11). Она устанавливает член Status структуры MINIDUMP_CALLBACK_OUTPUT в S_OK, указывая, что можно пропустить регион памяти, который нельзя считать при создании дампа.

Рис. 11. ReadMemoryFailureCallback вызывается при ошибках чтения

В этом простом варианте обратный вызов реализует ту же функциональность, что и флаг MiniDumpIgnoreInaccessibleMemory. Обратному вызову ReadMemoryFailureCallback передаются смещение, количество байтов и код ошибки через структуру MINIDUMP_READ_MEMORY_FAILURE_CALLBACK в параметре CallbackInput. В более сложном обратном вызове можно было бы использовать эту информацию, чтобы определять, является ли данный регион памяти критически важным для анализа и, если да, отменять создание дампа в случае ошибки чтения.

Разбор памяти

Как же узнать, что можно и чего нельзя удалять из дампа? Sysinternals VMMap — отличный способ увидеть, что представляет собой память приложения. Если вы используете Sysinternals VMMap применительно к управляемому процессу, то заметите, что есть области памяти, связанные с кучей, подлежащей сбору мусора (GC-куча), и области памяти, сопоставленные с образами и проецируемыми файлами приложения. В дампе управляемого процесса вам нужна именно GC-куча, так как расширение отладчика Son of Strike (SOS) требует для интерпретации дампа все структуры данных из GC-кучи.

Чтобы определить адрес GC-кучи, вы могли бы следовать строгому подходу: запустить сеанс отладочной машины (Debugging Engine, DbgEng) применительно к мишени, используя DebugCreate и IDebugClient::AttachProcess. С помощью этого сеанса можно было бы загрузить расширение отладчика SOS и выполнять команды для получения информации о куче (это пример на использование знаний в предметной области).

В качестве альтернативы можно прибегнуть к эвристике. Вы включаете все регионы, тип памяти которых является Private (MEMORY_PRIVATE) или Protection of Read/Write (PAGE_READWRITE или PAGE_EXECUTE_­READWRITE). При этом собирается больше памяти, чем абсолютно необходимо, но все равно достигается значительная экономия за счет исключения самого приложения. В примере MiniDump05 используется именно этот подход (рис. 12); при этом код для стеков потоков из примера MiniDump04 заменяется разовым циклом VirtualQueryEx в обратном вызове ThreadCallback (новая логика по-прежнему вызывается включает полного стека). Затем используется тот же код MemoryCallback, что и в примере MiniDump04, для включения памяти в дамп.

Рис. 12. MiniDump05: ThreadCallback используется один раз для сбора регионов памяти

Проецируемые в память файлы образов

Возможно, вас интересует, как можно отлаживать дамп в отсутствие регионов образа (MEM_IMAGE). Например: как увидеть, какой код выполняется? Ответ несколько неожиданный. Когда отладчику нужно обратиться к отсутствующему региону образа (image region) в неполном дампе, он получает данные из файла образа. Поиск файла образа осуществляется по пути загрузки модуля, по месту размещения исходного файла образа PDB или по путям поиска .sympath/.exepath. Выполнив команду lmvm <модуль>, вы увидите строку «Mapped memory image file» (проецируемый в память файл образа), указывающую, что этот файл был спроецирован в дамп, например:

Опора на поддержку отладчиком «Mapped memory image file» — отличный подход, позволяющий создавать дампы небольшого размера. Он особенно хорош для неуправляемых приложений, поскольку используются именно скомпилированные двоичные файлы, а значит, они доступны вашему внутреннему серверу сборки (и на них указывают PDB). В случае управляемых приложений JIT-компиляция на удаленном клиентском компьютере усложняет картину. Если вы хотите отлаживать дамп памяти управляемого приложения с другого компьютера, вам потребуется скопировать двоичные файлы (а также дампы) на свой локальный компьютер. Но это все равно дает экономию, так как можно весьма быстро получить несколько дампов, а затем единый (большой) набор файлов образов приложения можно обрабатывать без спешки. Чтобы упростить набор файлов, вы могли бы задействовать ModuleCallback и написать скрипт, собирающий модули (файлы), на которые есть ссылки в дампе.

Подключи меня!

Модификация автономного приложения под использование Sysinternals ProcDump v4.0 значительно облегчит вам жизнь. Вам больше не придется реализовать весь код, связанный с вызовом MiniDumpWriteDump, и, что важнее, весь код, необходимый для своевременной инициации создания дампа. Вам просто потребуется реализовать функцию MiniDumpCallback и экспортировать ее как MiniDumpCallbackRoutine в DLL.

Пример MiniDump06 (рис. 13) включает код этого обратного вызова из MiniDump05 с несколькими изменениями. В новом проекте MiniDump06 код обратного вызова компилируется как DLL. Проект экспортирует MiniDumpCallbackRoutine (регистр букв имеет значение!) через DEF-файл:

Поскольку ProcDump передает через CallbackParam значение NULL, функция для отслеживания своего прогресса должна использовать глобальную переменную вместо моей структуры MemoryInfoNode. В IncludeThreadCallback появился новый код, который сбрасывает (удаляет) глобальную переменную, если она установлена в ходе предыдущей операции захвата памяти. Это заменяет код, который был реализован после неудачного вызова MiniDumpWriteDump в моей функции WriteDump.

Рис. 13. MiniDumpCallbackRoutine, измененная под использование глобальной переменной вместо CallbackParam

Чтобы использовать свою DLL с ProcDump, вы указываете ключ –d с именем этой DLL (она должна соответствовать архитектуре дампа). Ключ –d доступен при создании мини-дампов (без ключа) и полных дампов (ключ –ma); в случае дампов MiniPlus (ключ –mp) он неприменим:

Заметьте, что обратный вызов запускается с типами обратных вызовов, отличных от тех, которые описывались в моих примерах, при создании полного дампа (–ma) (документацию см. в MSDN Library). Функция MiniDumpWriteDump интерпретирует дамп как полный, когда параметр DumpType содержит MiniDumpWithFullMemory.

Sysinternals ProcDump (procdump.exe) — 32-разрядное приложение, которое при необходимости распаковывает содержащуюся в нем 64-разрядную версию (procdump64.exe). После распаковки и запуска с помощью procdump.exe утилита procdump64.exe загрузит вашу (64-разрядную) DLL. Как таковая, отладка вашей 64-разрядной DLL — дело весьма замысловатое, потому что запущенное приложение не является нужной мишенью. Самое простое, что можно сделать для поддержки отладки вашей 64-разрядной DLL, — скопировать временный файл procdump64.exe в другую папку, а затем вести отладку, используя эту копию. Тогда никакой распаковки не будет, и ваша DLL будет загружаться в приложение, которое вы запускаете из отладчика (например, Visual Studio).

Точка прерывания!

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

Если вы заинтересованы в реализации собственного приложения или DLL для создания дампов, предлагаю вам сначала изучить эффективность работы утилит Sysinternals ProcDump, WER и AdPlus — чтобы заново не изобретать колесо.

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

Если вы разработчик кода режима ядра и заинтересованы в обратных вызовах, имейте в виду, что существует аналогичный механизм режима ядра; читайте документацию по процедуре BugCheckCallback на msdn.com.


ЭндрюРичардс (Andrew Richards) — старший инженер техподдержки в Microsoft для Windows OEM. Активно интересуется инструментами технической поддержки и постоянно создает расширения отладчика и приложения, которые упрощают работу инженеров технической поддержки. С ним можно связаться по адресу andrew.richards@microsoft.com.

Выражаю благодарность за рецензирование статьи экспертам Дрю Блиссу (Drew Bliss) и Марку Руссиновичу (Mark Russinovich).