API-интерфейсы отладчика

Создание расширения Debugging Tools for Windows

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

Загрузка примера кода

Диагностика проблем в производственных средах может оказаться одной из самых неприятных задач для любого инженера.В то же время она может приносить наибольшее удовлетворение. Работая в Microsoft Support, я сталкиваюсь с этим каждый день. Почему приложение рухнуло? Почему оно зависло? Почему в нем появились проблемы с производительностью?

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

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

Windows автоматически создает мини-дамп через Windows Error Reporting (WER), когда в любом приложении генерируется необработанное им исключение. Кроме того, вы можете вручную создать файл дампа с помощью утилиты Userdump.exe. Утилита ProcDump от Sysinternals (technet.microsoft.com/sysinternals/dd996900) становится основным инструментом создания дампов процессов в Microsoft Support, так как она умеет захватывать дамп на основе большого количества триггеров и способна генерировать дампы разных размеров. Но вот вы получили дамп, и что же с ним делать дальше?

Многие версии Visual Studio поддерживают открытие файлов дампов (.dmp), но лучший инструмент — отладчик из Debugging Tools for Windows. Эти отладочные инструменты основаны на едином ядре, которое поддерживает два API расширения отладчика. В данной статье я рассмотрю основы создания собственного расширения отладчика, чтобы вы могли легко анализировать файлы дампов (а также работающие системы).

Подготовка инструментария

Debugging Tools for Windows (microsoft.com/whdc/devtools/debugging) — устанавливаемый и разрешенный к распространению компонент Windows SDK и Windows Driver Kit (WDK). На момент написания этой статьи текущей версией была 6.12, и она доступна в Windows SDK или WDK версии 7.1. Советую использовать самую новую версию, так как в ядро отладки введено много ценных добавлений, в том числе усовершенствованный проход по стеку (stack walking).

В руководстве по Debugging Tools for Windows говорится, что компилировать расширения отладчика нужно с применением сборочной среды WDK. Я использую последний выпуск WDK (версию 7.1.0, сборка 7600.16385.1), но подойдет любая версия WDK или ее предыдущая инкарнация — Driver Development Kit (DDK). При создании расширения с помощью WDK вы используете среды x64 и x86 Free Build.

Приложив немного усилий, вы также сможете адаптировать мои проекты для компиляции в сборочной среде Windows SDK или Visual Studio.

Одно предупреждение: WDK не любит пробелы в путях. Убедитесь, что компиляция выполняется из верного пути. Поэтому, например, вместо пути C:\Users\Andrew Richards\Documents\Projects используйте нечто вроде C:\Projects.

Независимо от того, как вы компилируете расширение, вам понадобятся заголовочные и библиотечные файлы из Debugging Tools SDK, который является компонентом Debugging Tools for Windows. В примерах в этой статье я использую свой x86-путь (C:\debuggers_x86\sdk), когда ссылаюсь на заголовочные и библиотечные файлы. Если вы предпочтете установить отладчик в любой другой каталог, не забудьте соответственно изменить путь и добавить кавычки там, где в именах каталогов, образующих пути, есть пробелы.

Использование Debugging Tools

Отладчики в Debugging Tools for Windows безразличны к процессорной архитектуре. Любая редакция отладчика позволяет вести отладку на любой целевой архитектуре. Частый пример:использование x64-отладчика для отладки x86-приложения. Отладчик выпускается в редакциях для x86, x64 (amd64) и IA64, но может отлаживать приложения, ориентированные на архитектуры x86, x64, IA64, ARM, EBC и PowerPC (Xbox). Вы можете установить все редакции отладчиков, и они будут работать независимо.

Такую гибкость, однако, не следует трактовать как универсальность. В отличие от ядра отладчика не все расширения могут корректно работать с целевой архитектурой. Некоторые расширения отладчика предполагают, что размер указателей в целевой архитектуре совпадает с таковым у отладчика. Аналогично они используют «зашитые» в код имена регистров, которые могут оказаться неправильными (например, esp вместо rsp), тогда как следует оперировать псевдорегистрами вроде $csp.

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

Каждый тип сборки приложения и соответствующая процессорная архитектура создают свой набор проблем с отладкой. Ассемблерный код, генерируемый для отладочной сборки сравнительно линеен, но тот же код, созданный для выпускной сборки (release build), оптимизирован и может напоминать миску со спагетти. В архитектурах x86 Frame Pointer Omission (FPO) сеет хаос при реконструкции стека вызовов (новейший отладчик хорошо справляется с этим). В архитектурах x64 параметры функций и локальные переменные хранятся в регистрах. В момент захвата дампа они могут быть перемещены в стек или вообще исчезнуть из-за повторного использования соответствующих регистров.

И здесь ключевую роль играет опыт. Точнее, опыт экспертов. Нужно просто превращать их «ноу-хау» в расширение отладчика для использования всеми нами. Для меня достаточно нескольких повторений аналогичных последовательностей в отладке, после чего я автоматизирую их как расширение отладчика. Я так привык к некоторым из своих расширений, что даже не помню, как я делал то же самое, используя непосредственно команды отладчика.

Использование API-интерфейсов отладчика

Существует два API расширения отладчика: устаревший WdbgExts API (wdbgexts.h) и текущий DbgEng API (dbgeng.h).

Расширения WdbgExts основаны на глобальном вызове, который конфигурируется при инициализации (WinDbgExtensionDllInit):

WINDBG_EXTENSION_APIS ExtensionApis;

Глобальный (экспортируемый) вызов обеспечивает функциональность, необходимую для запуска таких функций, как dprintf("\n") и GetExpression("@$csp"), без указания пространства имен. Этот тип расширения напоминает код, который вы пишете при программировании под Win32 API.

Расширения DbgEng основаны на интерфейсах отладчика. Интерфейс IDebugClient передается вам ядром отладки как параметр каждого вызова. Эти интерфейсы поддерживают QueryInterface для доступа ко все более широкому набору интерфейсов отладчика. Этот тип расширения напоминает код, который вы пишете при программировании под Win32 API.

Можно использовать гибридный вариант этих двух типов расширения. Вы открываете доступ к расширению как DbgEng, но в период выполнения добавляете функциональность WdbgExts API через вызов IDebugControl::GetWindbgExtensionApis64. В качестве примера я написал классическую «Hello World» как расширение DbgEng на C. Если вы предпочитаете C++, ссылайтесь на класс ExtException в Debugging Tools SDK (.\inc\engextcpp .cpp).

Скомпилируйте расширение как MyExt.dll (TARGETNAME — в файле Sources на рис. 1). Оно предоставляет команду !helloworld. Это расширение динамически связывается с исполняющей библиотекой Microsoft Visual C (MSVCRT). Если вы хотите использовать статическое связывание, замените выражение USE_MSVCRT=1 на USE_LIBCMT=1 в файле Sources.

Рис. 1. Файл Sources

TARGETNAME=MyExt
TARGETTYPE=DYNLINK

_NT_TARGET_VERSION=$(_NT_TARGET_VERSION_WINXP)

DLLENTRY=_DllMainCRTStartup

!if "$(DBGSDK_INC_PATH)" != ""
INCLUDES = $(DBGSDK_INC_PATH);$(INCLUDES)
!endif
!if "$(DBGSDK_LIB_PATH)" == ""
DBGSDK_LIB_PATH = $(SDK_LIB_PATH)
!else
DBGSDK_LIB_PATH = $(DBGSDK_LIB_PATH)\$(TARGET_DIRECTORY)
!endif

TARGETLIBS=$(SDK_LIB_PATH)\kernel32.lib \
           $(DBGSDK_LIB_PATH)\dbgeng.lib

USE_MSVCRT=1

UMTYPE=windows

MSC_WARNING_LEVEL = /W4 /WX

SOURCES= dbgexts.rc      \
         dbgexts.cpp     \
         myext.cpp

Функция DebugExtensionInitialize (рис. 2) вызывается при загрузке расширения. Чтобы задать параметр Version, просто используйте макрос DEBUG_EXTENSION_VERSION с директивами #define EXT_MAJOR_VER и EXT_MINOR_VER, которые я добавил в заголовочный файл:

// dbgexts.h

#include <windows.h>
#include <dbgeng.h>

#define EXT_MAJOR_VER  1
#define EXT_MINOR_VER  0

Рис. 2. Файл dbgexts.cpp

// dbgexts.cpp

#include "dbgexts.h"

extern "C" HRESULT CALLBACK
DebugExtensionInitialize(PULONG Version, PULONG Flags) {
  *Version = DEBUG_EXTENSION_VERSION(EXT_MAJOR_VER, EXT_MINOR_VER);
  *Flags = 0;  // Reserved for future use.
  return S_OK;
}

extern "C" void CALLBACK
DebugExtensionNotify(ULONG Notify, ULONG64 Argument) {
  UNREFERENCED_PARAMETER(Argument);
  switch (Notify) {
    // A debugging session is active. The session may not necessarily be suspended.
    case DEBUG_NOTIFY_SESSION_ACTIVE:
      break;
    // No debugging session is active.
    case DEBUG_NOTIFY_SESSION_INACTIVE:
      break;
    // The debugging session has suspended and is now accessible.
    case DEBUG_NOTIFY_SESSION_ACCESSIBLE:
      break;
    // The debugging session has started running and is now inaccessible.
    case DEBUG_NOTIFY_SESSION_INACCESSIBLE:
      break;
  }
  return;
}

extern "C" void CALLBACK
DebugExtensionUninitialize(void) {
  return;
}

Значение Version сообщается как версия API в команде отладчика .chain. Чтобы изменить File Version, File Description, Copyright и другие значения, вам нужно отредактировать файл dbgexts.rc:

myext.dll: image 6.1.7600.16385, API 1.0.0, built Wed Oct 13 20:25:10 2010
  [path: C:\Debuggers_x86\myext.dll]

Параметр Flags зарезервирован и должен быть равен 0. Функция должна возвращать S_OK.

Функция DebugExtensionNotify вызывается, когда у сеанса меняется его состояние. Параметр Argument обертывается макросом UNREFERENCED_PARAMETER, чтобы исключить предупреждения компилятора о неиспользуемом параметре.

Для полноты картины я добавил выражение switch для параметра Notify, но не включил в эту часть никакого функционального кода. Выражение switch обрабатывает смену четырех состояний сеанса:

  • DEBUG_NOTIFY_SESSION_ACTIVE — подключение к мишени;
  • DEBUG_NOTIFY_SESSION_INACTIVE — отключение от мишени (через .detach или qd);
  • если выполнение мишени приостанавливается (например, в точке прерывания), функции будет передано DEBUG_NOTIFY_SESSION_ACCESSIBLE;
  • если выполнение мишени возобновляется, функции будет передано DEBUG_NOTIFY_SESSION_INACCESSIBLE.

Функция DebugExtensionUninitialize вызывается при выгрузке расширения.

Каждая предоставляемая команда расширения объявляется как функция типа PDEBUG_EXTENSION_CALL. Имя этой функции является именем команды расширения. Так как я пишу «Hello World», я присвоил функции имя helloworld (рис. 3).

Рис. 3. MyExt.cpp

// MyExt.cpp

#include "dbgexts.h"

HRESULT CALLBACK 
helloworld(PDEBUG_CLIENT pDebugClient, PCSTR args) {
  UNREFERENCED_PARAMETER(args);

  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl))) {
    pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "Hello World!\n");
    pDebugControl->Release();
  }
  return S_OK;
}

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

;-------------
;   MyExt.def
;-------------
EXPORTS
  helloworld
  DebugExtensionNotify
  DebugExtensionInitialize
  DebugExtensionUninitialize

Параметр args содержит строку аргументов для команды. Этот параметр передается как ANSI-строка, завершаемая нулем (CP_ACP).

Параметр pDebugClient — это указатель на интерфейс IDebugClient, который позволяет расширению взаимодействовать с ядром отладки. Хотя указатель на интерфейс выглядит как указатель на COM-интерфейс, он не подлежит маршалингу, и к нему нельзя обратиться в более позднее время. Кроме того, его нельзя использовать из любого другого потока. Чтобы выполнить работу в другом потоке, нужно создать в том потоке новый клиент отладчика (новый указатель на интерфейс IDebugClient) с помощью IDebugClient::CreateClient. Это единственная функция, которую можно выполнять в другом потоке.

Интерфейс IDebugClient (как и все интерфейсы) наследует от IUnknown. Вы используете QueryInterface для доступа к другим интерфейсам DbgEng — будь то более поздние версии интерфейса IDebugClient (IDebugClient4) или другие интерфейсы (IDebugControl, IDebugRegisters, IDebugSymbols, IDebugSystemObjects и т. д.). Для вывода текста в отладчике вам понадобится интерфейс IDebugControl.

В моей папке, кроме файлов из SDK, находятся еще два файла. Командный файл make.cmd добавляет в сборочную среду WDK пути inc и lib в Debugger SDK, а затем выполняет соответствующую команду компиляции:

@echo off
set DBGSDK_INC_PATH=C:\Debuggers_x86\sdk\inc
set DBGSDK_LIB_PATH=C:\Debuggers_x86\sdk\lib
set DBGLIB_LIB_PATH=C:\Debuggers_x86\sdk\lib
build -cZMg %1 %2

Заметьте, что сборочная среда WDK сама определяет, какой двоичный файл будет генерироваться — x86 или x64. Если вам нужна компиляция под несколько архитектур, откройте несколько окон командной строки и в каждом из них запустите make.cmd. Компиляция может осуществляться параллельно.

После компиляции я использую скрипт (x86) test.cmd для копирования скомпилированных двоичных файлов i386 в папку x86-отладчика (c:\Debuggers_x86), а затем открываю экземпляр Notepad с присоединенным отладчиком и загруженным расширением:

@echo off
copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86
copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86
\Debuggers_x86\windbg.exe -a myext.dll -x notepad

Если все проходит, как планировалось, я могу набрать «!helloworld» в командной строке отладчика и увидеть в ответ «Hello World!»:

0:000> !helloworld
Hello World!

Разрешение и чтение символов

Программа «Hello World» может быть просто восхитительной, но вы можете сделать кое-что получше. Сейчас я задействую эту инфраструктуру, чтобы добавить команду, которая действительно взаимодействует с мишенью и может помочь в анализе. В простом приложении test01 имеется глобальный указатель, которому присваивается некое значение:

// test01.cpp

#include <windows.h>

void* g_ptr;
int main(int argc, char* argv[]) {
  g_ptr = "This is a global string";
  Sleep(10000);
  return 0;
}

Новая команда !gptr в MyExt.cpp (рис. 4) будет разрешать глобальный test01!g_ptr, считывать указатель, а потом выводить значения, найденные в том же формате, что и «x test01!g_ptr»:

0:000> x test01!g_ptr
012f3370 Test01!g_ptr = 0x012f20e4

0:000> !gptr
012f3370 test01!g_ptr = 0x012f20e4
<string>

Рис. 4. Переработанный MyExt.cpp

HRESULT CALLBACK 
gptr(PDEBUG_CLIENT pDebugClient, PCSTR args) {
  UNREFERENCED_PARAMETER(args);

  IDebugSymbols* pDebugSymbols;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugSymbols), 
    (void **)&pDebugSymbols))) {  
    // Resolve the symbol.
    ULONG64 ulAddress = 0;
    if (SUCCEEDED(pDebugSymbols->GetOffsetByName("test01!g_ptr", &ulAddress))) {
      IDebugDataSpaces* pDebugDataSpaces;
      if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugDataSpaces),
        (void **)&pDebugDataSpaces))) {  
        // Read the value of the pointer from the target address space.
        ULONG64 ulPtr = 0;
        if (SUCCEEDED(pDebugDataSpaces->ReadPointersVirtual(1, ulAddress, &ulPtr))) {
          PDEBUG_CONTROL pDebugControl;
          if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
            (void **)&pDebugControl))) {  
            // Output the values.
            pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
              "%p test01!g_ptr = 0x%p\n", ulAddress, ulPtr);
            pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "%ma\n", ulPtr);
            pDebugControl->Release();
          }
        }
        pDebugDataSpaces->Release();
      }
      pDebugSymbols->Release();
    }
  }
  return S_OK;
}

Первый шаг — определить местонахождение указателя test01!g_ptr. Этот указатель при каждом запуске будет находиться в другом месте, так как механизм Address Space Layout Randomization (ASLR) будет менять адрес загрузки модуля. Чтобы получить адрес, я использую QueryInterface и получаю интерфейс IDebugSymbols, потом вызываю GetOffsetByName. Функция GetOffsetByName принимает имя символа и возвращает адрес как 64-разрядный указатель. Функции отладчика всегда возвращают 64-разрядные указатели (ULONG64), благодаря чему 64-разрядные мишени можно отлаживать с помощью 32-разрядного отладчика.

Помните:это адрес указателя в адресном пространстве мишени, а не в вашем. Его нельзя просто-так считать и определить значение. Чтобы получить значение этого указателя, я вновь использую QueryInterface для получения интерфейса IDebugDataSpaces, а затем вызываю ReadPointersVirtual. Эта функция считывает указатель из адресного пространства целевого процесса. ReadPointersVirtual автоматически подстраивается под размер указателя и различия в порядке следования байтов. Вам не нужно манипулировать возвращенным указателем.

IDebugControl::Output принимает ту же форматирующую строку, что и printf, но ее средства форматирования позволяют ссылаться на адресное пространство мишени (целевого процесса). Я использую формат %ma для вывода ANSI-строки, на которую указывает глобальный указатель в адресном пространстве мишени. Формат %p поддерживает указатели разных размеров, и его следует применять для вывода указателей (вы должны передавать ULONG64).

Я изменил тестовый скрипт для загрузки файла дампа x86-версии test01 вместо запуска Notepad:

@echo off
copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86
copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86
\Debuggers_x86\windbg.exe -a myext.dll -y "..\Test01\x86;SRV*c:\symbols*http://msdl.microsoft.com/download/symbols" -z ..\Test01\x86\Test01.dmp

Я также задал символьный путь к папке test01 x86 и Microsoft Public Symbol Server для корректного разрешения символов. Кроме того, я создал тестовый скрипт для x64, который делает то же самое, что и тестовый крипт для x86, но работает с файлом дампа x64-версии тестового приложения:

@echo off
copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86
copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86
\Debuggers_x64\windbg.exe -a myext.dll -y "..\Test01\x64;SRV*c:\symbols*http://msdl.microsoft.com/download/symbols" -z ..\Test01\x64\Test01.dmp

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

И вновь, если все идет по плану, я могу ввести «x test01!g_ptr» и !gptr в командной строке отладчика и увидеть соответствующие ответы:

// x86 Target
0:000> x test01!g_ptr
012f3370 Test01!g_ptr = 0x012f20e4

0:000> !gptr
012f3370 test01!g_ptr = 0x012f20e4
This is a global string

// x64 Target
0:000> x test01!g_ptr
00000001`3fda35d0 Test01!g_ptr = 0x00000001`3fda21a0

0:000> !gptr
000000013fda35d0 test01!g_ptr = 0x000000013fda21a0
This is a global string

Если вы повторите тест с использованием x64-отладчика, версии расширения отладчика, скомпилированного под amd64 и файлов дампов для x86 или x64, вы получите те же результаты. То есть мы создали расширение, независимое от процессорной архитектуры.

Типы процессоров и стеки

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

Рис. 5. Sleepy

HRESULT CALLBACK 
sleepy(PDEBUG_CLIENT4 Client, PCSTR args) {
  UNREFERENCED_PARAMETER(args);
  BOOL bFound = FALSE;

  IDebugControl* pDebugControl;

  if (SUCCEEDED(Client->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl))) {
    IDebugSymbols* pDebugSymbols;

    if (SUCCEEDED(Client->QueryInterface(__uuidof(IDebugSymbols), 
      (void **)&pDebugSymbols))) {
      DEBUG_STACK_FRAME* pDebugStackFrame = 
        (DEBUG_STACK_FRAME*)malloc(
        sizeof(DEBUG_STACK_FRAME) * MAX_STACK_FRAMES);

      if (pDebugStackFrame != NULL) {  
        // Get the Stack Frames.
        memset(pDebugStackFrame, 0, (sizeof(DEBUG_STACK_FRAME) * 
          MAX_STACK_FRAMES));
        ULONG Frames = 0;

        if (SUCCEEDED(pDebugControl->GetStackTrace(0, 0, 0, 
          pDebugStackFrame, MAX_STACK_FRAMES, &Frames)) && 
          (Frames > 0)) {
          ULONG ProcessorType = 0;
          ULONG SymSize = 0;
          char SymName[4096];
          memset(SymName, 0, 4096);
          ULONG64 Displacement = 0;

          if (SUCCEEDED(pDebugControl->GetEffectiveProcessorType(
            &ProcessorType))) {
            for (ULONG n=0; n<Frames; n++) {  

              // Use the Effective Processor Type and the contents 
              // of the frame to determine existence
              if (SUCCEEDED(pDebugSymbols->GetNameByOffset(
                pDebugStackFrame[n].InstructionOffset, SymName, 4096, 
                &SymSize, &Displacement)) && (SymSize > 0)) {

                if ((ProcessorType == IMAGE_FILE_MACHINE_I386) && 
                  (_stricmp(SymName, "KERNELBASE!Sleep") == 0) && 
                  (Displacement == 0xF)) {  
                  // Win7 x86; KERNELBASE!Sleep+0xF is usually in frame 3.
                  IDebugDataSpaces* pDebugDataSpaces;

                  if (SUCCEEDED(Client->QueryInterface(
                    __uuidof(IDebugDataSpaces), 
                    (void **)&pDebugDataSpaces))) {  
                    // The value is pushed immediately prior to 
                    // KERNELBASE!Sleep+0xF
                    DWORD dwMilliseconds = 0;

                    if (SUCCEEDED(pDebugDataSpaces->ReadVirtual(
                      pDebugStackFrame[n].StackOffset, &dwMilliseconds, 
                      sizeof(dwMilliseconds), NULL))) {
                      pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
                        "Sleeping for %ld msec\n", dwMilliseconds);
                      bFound = TRUE;
                    }
                    pDebugDataSpaces->Release();
                  }
                  if (bFound) break;
                }

                else if ((ProcessorType == IMAGE_FILE_MACHINE_AMD64) && 
                  (_stricmp(SymName, "KERNELBASE!SleepEx") == 0) && 
                  (Displacement == 0xAB)) {  
                  // Win7 x64; KERNELBASE!SleepEx+0xAB is usually in frame 1.
                  IDebugRegisters* pDebugRegisters;

                  if (SUCCEEDED(Client->QueryInterface(
                    __uuidof(IDebugRegisters), 
                    (void **)&pDebugRegisters))) {  
                    // The value is in the 'rsi' register.
                    ULONG rsiIndex = 0;
                    if (SUCCEEDED(pDebugRegisters->GetIndexByName(
                      "rsi", &rsiIndex)))
                    {
                      DEBUG_VALUE debugValue;
                      if (SUCCEEDED(pDebugRegisters->GetValue(
                        rsiIndex, &debugValue)) && 
                        (debugValue.Type == DEBUG_VALUE_INT64)) {  
                        // Truncate to 32bits for display.
                        pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
                          "Sleeping for %ld msec\n", debugValue.I32);
                        bFound = TRUE;
                      }
                    }
                    pDebugRegisters->Release();
                  }

                  if (bFound) break;
                }
              }
            }
          }
        }
        free(pDebugStackFrame);
      }
      pDebugSymbols->Release();
    }
    if (!bFound)
      pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
        "Unable to determine if Sleep is present\n");
    pDebugControl->Release();
  }
  return S_OK;
}

Чтобы немного усложнить команду, введем в нее поддержку x86- и x64-версий приложения test01. Так как соглашения о вызовах в x86- и x64-приложениях различаются, команда должна распознавать архитектуру целевого процесса.

Первый шаг — получить фреймы стека. Для этого я использую QueryInterface, получаю интерфейс IDebugControl, а потом с помощью GetStackTrace извлекаю информацию о каждом фрейме стека. GetStackTrace принимает массив структур DEBUG_STACK_FRAME. Я всегда создаю такой массив в куче (динамически распределяемой памяти), чтобы не вызвать переполнения стека. Если вы сталкиваетесь с переполнением стека в потоке целевого процесса, то, вероятно, исчерпали лимит на размер собственного стека, если создавали массив в этом стеке.

Если выполнение GetStackTrace завершается успешно, массив будет заполнен информацией по каждому пройденному фрейму. Успех здесь не обязательно означает, что информация о фрейме корректна. Отладчик предпринимает максимум усилий для прохода по всем фреймам стека, но возможны ошибки, когда символы некорректны (например, не хватает какого-то символа или загружен новый). Если вы загружали символ командой .reload /f /i, не исключено, что вы получили неправильное выравнивание символа.

Для корректного использования содержимого каждой из структур DEBUG_STACK_FRAME нужно знать эффективный тип процессора (effective processor type) мишени. Как уже упоминалось, целевая архитектура может полностью отличаться от архитектуры расширения отладчика. Эффективный тип процессора (.effmach) — это архитектура, используемая мишенью в данный момент.

Тип процессора также может отличаться от того, который использовался хостом мишени. Наиболее распространенный пример этого — мишень является приложением x86, выполняемым в 32-разрядной Windows, которая в свою очередь выполняется в 64-разрядной Windows (WOW64). Эффективный тип процессора будет IMAGE_FILE_MACHINE_I386, а реальный — IMAGE_FILE_MACHINE_AMD64.

То есть вы должны считать приложение x86 именно таковым независимо от того, в какой версии Windows оно выполняется — x86 или x64. (Единственное исключение из этого правила — отладка WOW64-вызовов, окружающих x86-процесс.)

Чтобы получить эффективный тип процессора, я использую уже имеющийся у меня интерфейс IDebugControl и вызываю GetEffectiveProcessorType.

Если эффективный тип процессора — i386, мне нужно найти функцию KERNELBASE!Sleep+0xf. Если все символы разрешены правильно, эта функция должна находиться в третьем фрейме:

0:000> knL4
 # ChildEBP RetAddr  
00 001bf9dc 76fd48b4 ntdll!KiFastSystemCallRet
01 001bf9e0 752c1876 ntdll!NtDelayExecution+0xc
02 001bfa48 752c1818 KERNELBASE!SleepEx+0x65
03 001bfa58 012f1015 KERNELBASE!Sleep+0xf

Если эффективный тип процессора — AMD64, я ищу функцию KERNELBASE!SleepEx+0xab. Если все символы разрешены правильно, эта функция должна находиться в первом фрейме:

0:000> knL2
 # Child-SP          RetAddr           Call Site
00 00000000'001cfc08 000007fe'fd9b1203 ntdll!NtDelayExecution+0xa
01 00000000'001cfc10 00000001'3fda101d KERNELBASE!SleepEx+0xab

Однако в зависимости от доступного уровня разрешения символов символ функции, который я ищу, может отсутствовать в ожидаемом фрейме. Если вы откроете файл дампа test01 x86 и не укажете путь к символу, то можете столкнуться с примером того, о чем я сказал. Вызов KERNELBASE!Sleep будет в первом фрейме, а не в третьем:

0:000> knL4
 # ChildEBP RetAddr  
WARNING: Stack unwind information not available. Following frames may be wrong.
00 001bfa48 752c1818 ntdll!KiFastSystemCallRet
01 001bfa58 012f1015 KERNELBASE!Sleep+0xf
02 001bfaa4 75baf4e8 Test01+0x1015
03 001bfab0 76feaf77 kernel32!BaseThreadInitThunk+0x12

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

Чтобы определить наличие функции Sleep, нужно искать ее символ в каждом фрейме. Если эффективный тип процессора и символ образуют допустимую пару, функция найдена. Заметьте, что эта логика весьма хрупка и используется только для того, чтобы упростить пример. Символ может меняться от сборки к сборке и зависеть от конкретной платформы. Скажем, в Windows Server 2008 — это kernel32!Sleep+0xf, но в Windows 7 — KERNELBASE!Sleep+0xf.

Чтобы получить этот символ, я использую QueryInterface для получения интерфейса IDebugSymbol. Далее вызываю GetNameByOffset, чтобы получить символ с адресом смещения инструкции.

Этот символ состоит из двух частей: имени символа (KERNELBASE!Sleep) и смещения (0xf). Имя символа формируется из имен модуля и функции (<модуль>!<функция>). Смещение считается в байтах от начала функции, и туда вернется поток управления в программе после возврата из вызова.

Если символов нет, о функции будет сообщаться в виде имени модуля с большим смещением (Test01+0x1015).

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

// @$csp is the pseudo-register of @esp
0:000> dps @$csp
<snip>
001bfa4c  752c1818 KERNELBASE!Sleep+0xf
001bfa50  00002710
<snip>

Член StackOffset структуры DEBUG_STACK_FRAME на самом деле уже указывает на этот адрес, поэтому никаких арифметических действий с указателем не требуется. Чтобы извлечь значение, я использую QueryInterface для получения интерфейса IDebugDataSpaces, а потом вызываю ReadVirtual, чтобы считать DWORD из адресного пространства мишени.

Если мишень основана на архитектуре x64, значение задержки находится не в стеке, а в регистре rsi (это тоже хрупкая логика из-за ее зависимости от контекста фрейма):

0:000> r @rsi
rsi=0000000000002710

Чтобы извлечь значение, я использую QueryInterface для получения интерфейса IDebugRegisters. Сначала мне нужно вызвать GetIndexByName, чтобы узнать индекс регистра rsi. Затем я вызываю GetValue для чтения значения регистра из регистров мишени. Так как rsi является 64-битным регистром, значение возвращается как INT64. А поскольку структура DEBUG_VALUE — это объединение (union), вы можете просто сослаться на член I32 вместо I64 и получить усеченную версию, которая представляет DWORD, переданное Sleep.

И вновь в обоих случаях для вывода результата я использую функцию IDebugControl::Output.

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

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

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

Эндрю Ричардс (Andrew Richards) — старший инженер в группе Microsoft Exchange Server. Активно интересуется инструментами технической поддержки и постоянно создает расширения отладчика и приложения, которые упрощают работу инженеров технической поддержки.

Выражаю благодарность за рецензирование статьи экспертам Дрю Блиссу (Drew Bliss), Йен-Лунгу Чиу (Jen-Lung Chiu), Майку Хендриксону (Mike Hendrickson), Кену Джонсону (Ken Johnson), Брунде Нагалингайяа (Brunda Nagalingaiah) и Мэту Уэберу (Matt Weber)