EreTIk's Box » Заметки о WinDbg » Особенность инициализации расширения отладчика WinDbg: вызовы DebugExtensionInitialize / DebugExtensionUninitialize


Уже достаточно давно для расширений к WinDbg используются механизмы Debugger Engine. Это достаточно мощный и дружелюбный набор COM-интерфейсов для работы с отладочными средствами в операционной системе Windows. Но для того, что бы использовать эти инструменты в расширении WinDbg, необходимо собрать отдельный PE-модуль (DLL). У такого модуля должны быть определены, в числе прочих, функции обратного вызова:



В комментариях к функции DebugExtensionInitialize(...) довольно недвусмысленно сказано, что:


Implementations of this function should initialize any global variables required by the extension DLL

То есть для расширения WinDbg это можно считать неким DllMain’ом. Но вот только не все так безоблачно. При подготовке очередного релиза PYKD была невнятная ошибка. После достаточно большого количества проведенных тестов, было выявлено, что пары вызовов <DebugExtensionInitialize(...) - DebugExtensionUninitialize()> могут быть вложенными. Для теста я набросал примитивный пример DExtSmpl следующего содержания:


static volatile long g_RefCounter = 0;
extern "C" HRESULT CALLBACK DebugExtensionInitialize(
  OUT PULONG Version,
  OUT PULONG Flags
)
{
  *Version = DEBUG_EXTENSION_VERSION( 1, 0 );
  *Flags = 0;
  InterlockedIncrement(&g_RefCounter);
  return S_OK;
}

extern "C" VOID CALLBACK DebugExtensionUninitialize()
{
  InterlockedDecrement(&g_RefCounter);
}
extern "C" HRESULT CALLBACK test(
  IN  PDEBUG_CLIENT4 pClient,
  IN  PCSTR /* szArguments */
)
{
  TComPtr<IDebugControl> pControl;
  HRESULT hRes= pControl.QueryFrom(pClient);
  if (FAILED(hRes))
    return hRes;
  pControl->Output(
    DEBUG_OUTPUT_NORMAL,
    "initialized %u times\n",
    g_RefCounter);
  return S_OK;
}
                

У WinDbg есть понятие рабочего пространства, которое можно сохранить загрузить для определенной конфигурации отладки. В числе прочих параметров, WinDbg сохраняет в рабочем пространстве список используемых внешних расширений. В этом достаточно просто убедиться, если сразу после старта отладочной сессии набрать команду .chain. Это вполне логично: как правило, человек, сидящий за WinDbg, использует одни и те же внешние расширения. И если поставить точки останова на реализациях DebugExtensionInitialize(...) и DebugExtensionUninitialize(), то при старте отладочной сессии с "закэшированным" расширением, можно увидеть вызов (реальное событие - старт отлаживаемого процесса в WinDbg):


DExtSmpl!DebugExtensionInitialize dbgeng!ExtensionInfo::Load+0x611 dbgeng!ExtensionInfo::CheckAdd+0x78 dbgeng!ParseBangCmd+0x36a dbgeng!ProcessCommands+0x539 dbgeng!ProcessCommandsAndCatch+0x20 dbgeng!Execute+0x28d dbgeng!DebugClient::ExecuteWide+0x8b windbg!ProcessCommand+0x1e9 windbg!ProcessEngineCommands+0xb0 windbg!EventCallbacks::CreateProcessW+0x27 dbgeng!CreateProcessEventApcData::Dispatch+0xb2 dbgeng!ApcDispatch+0x27 dbgeng!SendEvent+0xef dbgeng!NotifyCreateProcessEvent+0x844 dbgeng!LiveUserTargetInfo::ProcessDebugEvent+0x222 dbgeng!LiveUserTargetInfo::WaitForEvent+0x795 dbgeng!WaitForAnyTarget+0x92 dbgeng!RawWaitForEvent+0x351 dbgeng!DebugClient::WaitForEvent+0xcf windbg!EngineLoop+0x167 kernel32!BaseThreadInitThunk+0xd ntdll!RtlUserThreadStart+0x1d

Соответственно, при вызове !test получаем следующий вывод:


initialized 1 times

Затем, грузим расширение вручную с использованием команды "!load DExtSmpl". И видим, что DebugExtensionInitialize(...) вызывается повторно:


DExtSmpl!DebugExtensionInitialize dbgeng!ExtensionInfo::Load+0x611 dbgeng!ExtensionInfo::CheckAdd+0x78 dbgeng!ParseBangCmd+0x36a dbgeng!ProcessCommands+0x563 dbgeng!ProcessCommandsAndCatch+0x20 dbgeng!Execute+0x28d dbgeng!DebugClient::ExecuteWide+0x8b windbg!ProcessCommand+0x189 windbg!ProcessEngineCommands+0xb0 windbg!EngineLoop+0x3d6 kernel32!BaseThreadInitThunk+0xd ntdll!RtlUserThreadStart+0x1d

Как было написано ранее, точки останова стоят и на DebugExtensionInitialize(...) и на DebugExtensionUninitialize(), но здесь мы наблюдаем повторный вызов DebugExtensionInitialize(...) без соответствующего вызова DebugExtensionUninitialize(). В этом легко убедиться, снова вызвав команду !test:


initialized 2 times

При следующем вызове "!load DExtSmpl" бибилотека не инициализируется очередной раз. Но как только будет введена команда "!unload DExtSmpl" мы увидим первый вызов DebugExtensionUninitialize(). Команда !test дает ожидаемый результат:


initialized 1 times

Если после выгрузки снова выполнить пару команд "!load DExtSmpl" - "!unload DExtSmpl", то мы снова увидим парный вызов DebugExtensionInitialize(...) - DebugExtensionUninitialize().


А теперь усложним схему загрузки и использования DExtSmpl.dll:


> !test initialized 1 times > !DExtSmpl.test initialized 2 times > !DExtSmpl.dll.test initialized 3 times > !DExtSmpl.test initialized 3 times > !test initialized 3 times > !load C:\WinDbg\winext\DExtSmpl.dll; !test initialized 4 times > !test initialized 4 times > !unload C:\WinDbg\winext\DExtSmpl.dll; !test Unloading C:\WinDbg\winext\DExtSmpl.dll extension DLL initialized 3 times > !unload DExtSmpl.dll; !test Unloading DExtSmpl.dll extension DLL initialized 2 times

Естественно, что все это время в процесс WinDbg была загружена только одна копия DExtSmpl.dll. Судя по всему, библиотеки расширения контролируются по имени, которое было явно или неявно указано при загрузке, а не адресу отображенного модуля (что кажется самым очевидным и логичным).


Следовательно, если в расширении есть глобальные данные, которые нужно инициализировать только один раз, то при реализации не обойтись без внутреннего счетчика, который бы точно указывал на то, что вызов DebugExtensionInitialize(...) (или DebugExtensionUninitialize()) вызывается первый (или последний) раз соответственно.




Описанное выше тестовое расширение можно скачать одним архивом


ΞρεΤΙκ