Уже достаточно давно для расширений к WinDbg используются механизмы Debugger Engine. Это достаточно мощный и дружелюбный набор COM-интерфейсов для работы с отладочными средствами в операционной системе Windows. Но для того, что бы использовать эти инструменты в расширении WinDbg, необходимо собрать отдельный PE-модуль (DLL). У такого модуля должны быть определены, в числе прочих, функции обратного вызова:
- DebugExtensionInitialize(...): обратный вызов инициализации расширения отладчика
- DebugExtensionUninitialize(): обратный вызов де-инициализации расширения отладчика
В комментариях к функции DebugExtensionInitialize(...) довольно недвусмысленно сказано, что:
То есть для расширения 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):
Соответственно, при вызове !test получаем следующий вывод:
Затем, грузим расширение вручную с использованием команды "!load DExtSmpl". И видим, что DebugExtensionInitialize(...) вызывается повторно:
Как было написано ранее, точки останова стоят и на DebugExtensionInitialize(...) и на DebugExtensionUninitialize(), но здесь мы наблюдаем повторный вызов DebugExtensionInitialize(...) без соответствующего вызова DebugExtensionUninitialize(). В этом легко убедиться, снова вызвав команду !test:
При следующем вызове "!load DExtSmpl" бибилотека не инициализируется очередной раз. Но как только будет введена команда "!unload DExtSmpl" мы увидим первый вызов DebugExtensionUninitialize(). Команда !test дает ожидаемый результат:
Если после выгрузки снова выполнить пару команд "!load DExtSmpl" - "!unload DExtSmpl", то мы снова увидим парный вызов DebugExtensionInitialize(...) - DebugExtensionUninitialize().
А теперь усложним схему загрузки и использования DExtSmpl.dll:
Естественно, что все это время в процесс WinDbg была загружена только одна копия DExtSmpl.dll. Судя по всему, библиотеки расширения контролируются по имени, которое было явно или неявно указано при загрузке, а не адресу отображенного модуля (что кажется самым очевидным и логичным).
Следовательно, если в расширении есть глобальные данные, которые нужно инициализировать только один раз, то при реализации не обойтись без внутреннего счетчика, который бы точно указывал на то, что вызов DebugExtensionInitialize(...) (или DebugExtensionUninitialize()) вызывается первый (или последний) раз соответственно.
Описанное выше тестовое расширение можно скачать одним архивом
ΞρεΤΙκ