Pull to refresh

Разрегистрация оконного крюка

Reading time3 min
Views46K

Холмс. А скажите, друг мой Ватсон, доводилось ли вам разрегистрировать оконный крюк, в особенности глобальный?

Ватсон. Хм… что же может быть проще, дорогой Холмс.
    ::UnhookWindowsHookEx( hhookMy);

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

В. Вот, ничего себе! Вы случайно не разыгрываете меня, Холмс? Это же User32. Одна из главных частей в Win32, на которую в свою очередь опираются все прочие технологии в Windows. Та же .Net, к примеру. И вдруг такое не вполне детерминированное поведение.

А CreateFile, случайно, не создаёт файл тоже отложенно – например, до момента первой записи в файл? А CreateProcess, случайно, не…

Х. Нет, Ватсон, на этот раз я не шучу. CreateFile и CreateProcess это у нас Kernel32 – совсем другое дело.

Поверьте, дружище. Занимаясь своими расследованиями, я потратил массу времени на изучение внутреннего устройства модуля User32 и с уверенностью утверждаю: авторы написали (и поддерживают) его совершенно не с таким качеством, как Kernel32.

В. Наверное, это просто другая команда разработчиков?

Х. Да, очевидно так.

В. Быть может, такое поведение всё же оправдано. И правда, так ли уж важно, когда выгружается крюковой DLL?

Х. Вообразите, что наше приложение обновило себя через Интернет и перезапускается. После перезапуска окажется, что в некоторые процессы загружена старая версия DLL, а в некоторые новая. И в данном случае старая версия не выгрузится и тогда, когда через очереди пройдут сообщения.

В. Какой ужас!

Но постойте. Если приложение обновило свои файлы, значит оно переименовало старую версию крюкового DLL или переместило её куда-нибудь во временный каталог. Затем запускается новая версия… Она же передаёт в SetWindowsHookEx адрес DLL, файловый путь которого теперь отличается от того DLL, который, как вы говорите, остался загруженным.

Вы хотите сказать, что система создаст крюк, нацеленный не на тот DLL, который передаём в SetWindowsHookEx? Не может такого быть!

Х. В тех процессах, из которых DLL выгрузиться не успел, именно так и произойдёт, клянусь своей тюнингованной клавиатурой.

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

В. Мда… И как со всем этим быть? Что если после UnhookWindowsHookEx просто выждать, скажем, секунду. Оконные сообщения возникают же довольно часто.

Х. Это если пользователь что-нибудь делает с окном. А, например, минимизированные окна ведут себя тихо. Нет никаких гарантий, что в каждой очереди в течение секунды возникнет сообщение. Или в течение часа.

В. Что же делать? Как прикажете гарантированно выгружать DLL из всех процессов?

(Холмс молчит и, покуривая, смотрит на Ватсона.)

В. Будет вам, Холмс. Уверен, вы уже придумали правильный выход из ситуации. Поделитесь, прошу вас.

Х. Пусть наша крюковая функция, обнаружив, что впервые выполняется в некоторой нити, создаст в специальном временном файле файловый поток. И прямо в имени этого потока укажет ид нити, в которой выполняется. Примерно так:
using namespace std;
namespace k = Kernel32;

...
        
if( nullptr == tls_plistiterHookedThreadFileStreamHandle)  // Переменная размещена в локальной памяти нити.
{
	_listHookedThreadFileStreamHandles.push_front( k::CreateFile(
				k::FormatMessage(
							L"%1:%2!4u! %3!4u!",
							sHookedThreadListFilePath,
							k::GetCurrentThreadId(),
							k::GetCurrentProcessId()
					),
				GENERIC_WRITE,
				FILE_SHARE_READ,
				CREATE_ALWAYS,
				FILE_FLAG_DELETE_ON_CLOSE
		));
		
	tls_plistiterHookedThreadFileStreamHandle 
				= new list< HANDLE>::iterator( _listHookedThreadFileStreamHandles.begin());
}

Тогда главный EXE сможет перебрать эти потоки (FindNextStream) и послать сообщение WM_NULL в каждую нить, которую User32 связал с нашим крюком. В результате User32 выгрузит DLL.

DLL же, выгружаясь, должен закрыть все описатели из списка _listHookedThreadFileStreamHandles.

Таким образом получаем ещё и хороший критерий того, что дело сделано, – успех при попытке удалить наш временный файл.

В. Холмс, а к чему такие сложности? Вот же есть SendMessage с аргументом HWND_BROADCAST.

Х. SendMessage не отправляет сообщения абсолютно во все нити, имеющие очередь. Впрочем, пожалуй, так тоже будет работать. Почти всегда.

(Кричит.) Миссис Хадсон, подавайте чай, пожалуйста!

На следующий день

В. Холмс, один джентльмен сообщил, что SendMessage( HWND_BROADCAST) не помогает, по крайней мере, в его случае.

Х. Хм.
Tags:
Hubs:
+91
Comments61

Articles