Хуки и DLL

Хуки и DLL

Dr. Joseph M. Newcomer 

Перевод: Алексей Остапенко 

Существует большая неразбериха по поводу установки и использования глобальных хуков. 

ПРИМЕЧАНИЕ 

Возможно стоит упомянуть, что Камбалы (прим. переводчика: Flounder - псевдоним автора) в целом не одобряют использование крючков (hooks), но такие крючки (хуки) кажутся допустимыми. 

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

Основной проблемой здесь является адресное пространство. Когда глобальная DLL исполняется, она исполняется в контексте того процесса, чье событие перехватывается. Это означает, что адреса, которые видит DLL, даже для своих собственных переменных, являются адресами в контексте целевого процесса. Поскольку это DLL, она имеет отдельную копию своих данных для каждого использующего ее процесса. И это означает, что любые значения, которые вы устанавливаете в глобальных для DLL переменных (таких, как объявленные на уровне файла), являются приватными и не будут наследовать ничего из исходного контекста библиотеки. Они будут инициализироваться заново, т.е., обычно, они будут равны нулю. 

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

Концепция разделенных адресных пространств трудна для понимания. Позвольте мне продемонстрировать ее на картинке. 

 



Здесь мы имеем три процесса. Ваш Процесс показан слева (Your Process). У DLL есть сегменты кода (Code), данных (Data) и разделяемый сегмент (Shared), как его создать мы обсудим позже. Теперь, если перехватывающая DLL вызывается для перехвата события в Процессе A (Process A), она отображается в адресное пространство Процесса A, как указано. Код является разделяемым, поэтому адреса в Процессе A ссылаются на те же страницы памяти, что и адреса в Вашем Процессе. По совпадению страницы памяти оказались отображенными в Процесс A по тем же самым виртуальным адресам, т.е. адресам, которые видит Процесс A. Процесс A также получает свою собственную копию сегмента данных, поэтому все что видит Процесс A в секции "Data", полностью принадлежит ему и не может повлиять на любой другой процесс (или быть измененным любым другим процессом!). Однако, фокус который заставляет все это работать заключается в разделяемом сегменте данных, показанном здесь красным цветом. Страницы, адресуемые Вашим Процессом в точности те же страницы памяти, что и адресуемые в Процессе A. Заметим, что по совпадению эти страницы оказались в адресном пространстве Процесса A в точности на тех же виртуальных адресах, что и в Вашем Процессе. Если бы вы сидели за отладкой Вашего Процесса и Процесса A одновременно (что вы можете делать, запустив две копии VC++!) и смотрели бы по адресу &something, находившемуся в разделяемом сегменте данных, и смотрели бы по нему в Вашем Процессе и затем по тому же адресу &something в Процессе A, вы бы увидели в точности одни и те же данные и даже по тому же самому адресу. Если бы вы использовали отладчик для изменения или отслеживали изменения программой значения something, то вы могли бы перейти к другому процессу, исследовать его и увидеть, что новое значение появилось также и здесь. 

Но вот облом: одинаковый адрес - это совпадение. Это совпадение абсолютно и однозначно не гарантируется. Посмотрите на Процесс B. Когда событие перехвачено в Процессе B, в него отображается DLL. Но адреса, занимаемые ею в Вашем Процессе и Процессе A, не доступны в адресном пространстве Процесса B. Поэтому происходит перемещение кода на другой адрес в Процессе B. Код в порядке; ему действительно безразлично по какому адресу он исполняется. Адреса данных подправлены так, чтобы ссылаться на новое положение данных, и даже разделяемые данные отображены в другое множество адресов, таким образом к ним обращаются по-другому. Если бы вы использовали отладчик с Процессом B и посмотрели бы на адрес &something в разделяемой области, вы бы обнаружили, что адрес something был бы другим, но содержимое something было бы тем же самым; выполнение изменения содержимого в Вашем Процессе или в Процессе A немедленно сделало бы это изменение видимым в Процессе B, хотя Процесс B и видит его по другому адресу. Это то же самое место физической памяти. Виртуальная память - это отображение между адресами, видимыми вами, как программистом, и физическими страницами памяти, которые в действительности содержит ваш компьютер. 

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

ПРИМЕЧАНИЕ 

Если вы знаете немного больше (достаточно, чтобы это представляло опасность), вы можете сказать: "Ага! Я могу переместить (rebase) мою DLL так, что она загружается по не конфликтующему адресу, и я смогу проигнорировать эту особенность". Это отличный пример того, как малые знания могут представлять серьезную опасность. Вы не можете гарантировать что такая DLL будет работать с любым возможным исполняемым модулем, который может быть когда-либо запущен на вашей машине! Поскольку это DLL глобального хука, она может быть вызвана из Word, Excel, Visio, VC++ и шести тысяч приложений, о которых вы никогда не слышали, но можете когда-либо запустить или может запустить ваш клиент. Поэтому забудьте об этом. Не пытайтесь перемещать. В конце концов, вы проиграете. Обычно, в самое неподходящее время с самым важным вашим клиентом (например, с обозревателем вашего продукта из журнала или с вашим очень богатым заказчиком, который уже обеспокоен другими ошибками, которые у вас могут быть...). Считайте, что разделяемый сегмент данных "перемещаем". Если вы не понимаете этот параграф, значит вы знаете недостаточно много, чтобы это представляло опасность. И вы можете спокойно его проигнорировать. 

У перемещения есть и другие последствия. Если в DLL вы сохранили указатель на callback-функцию в контексте Вашего Процесса, то для DLL бессмысленно вызывать ее в Процессе A или Процессе B. Этот адрес приведет к передаче управления в указываемую им область, что нормально, но эта передача произойдет в адресное пространство Процесса A или Процесса B, что совершенно бесполезно, не говоря уже о том, что почти наверняка фатально. 

Это также означает, что вы не можете использовать в своей DLL ничего из MFC. Она не может быть ни MFC DLL, ни MFC Extension DLL. Почему? Потому, что она будет вызывать функции MFC. А где они? Ну, они в вашем адресном пространстве. А не в адресном пространстве Процесса A, написанного на Visual Basic, или Процесса B, написанного на Java. Таким образом, вы должны написать DLL на чистом C, и я бы рекомендовал совсем не использовать библиотеку времени исполнения C (CRT). Вы должны использовать только API. Используйте lstrcpy вместо strcpy или tcscpy, lstrcmp вместо strcmp или tcscmp, и т.д. 

Существует множество решений для организации взаимодействия вашей DLL и ее управляющего сервера. Одно из решений заключается в использовании ::PostMessage или ::SendMessage (заметим, что здесь я ссылаюсь на вызовы чистого API, а не вызовы MFC!). Там, где возможно использовать вызов ::PostMessage, лучше используйте его, а не ::SendMessage, т.к. иначе вы можете получить опасные тупиковые ситуации. Если Ваш Процесс в итоге останавливается, все остальные процессы в системе остановятся, т.к. все заблокированы на вызове ::SendMessage, который никогда не возвратится, и вы просто вывели всю систему из строя с возможностью серьезной потери данных в важных для пользователя приложениях. Это Совершенно Однозначно Не Хорошая Ситуация. 

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

Вы не можете возвратить указатель из вызовов ::SendMessage и ::PostMessage (мы забудем про возможность передавать обратно относительные указатели в разделяемую область памяти; это также выходит за рамки этой статьи). Это из-за того, что любой указатель, который вы можете создать, будет ссылаться либо на адрес в DLL (перемещенной в перехваченный процесс), либо на адрес в перехваченном процессе (Процессе A или Процессе B), и, следовательно, он будет абсолютно бесполезен в Вашем Процессе. Вы можете возвращать лишь адресно-независимую информацию в WPARAM или LPARAM. 

Я сильно рекомендую использовать для таких целей Зарегистрированные Оконные Сообщения (смотрите мой обзор по Управлению Сообщениями ). Вы можете использовать макрос ON_REGISTERED_MESSAGE в MESSAGE_MAP окна, которому вы отсылаете сообщение. 

Основным требованием теперь является получение HWND этого окна. К счастью, это несложно. 

Первое, что вы должны сделать - это создать разделяемый сегмент данных. Это делается при помощи объявления #pragma data_seg. Выберите какое-либо хорошее мнемоническое имя для сегмента данных (оно должно быть не длиннее 8 символов). Просто чтобы подчеркнуть произвольность имени, я использовал здесь свое собственное имя. Во время преподавания я обнаружил, что если я использую имена вида .SHARE или .SHR, или .SHRDATA, то студенты полагают, что имя имеет значение. А оно не имеет значения. 

#pragma data_seg(".JOE")

HANDLE hWnd = NULL;

#pragma dta_seg()

#pragma comment(linker, "/section:.JOE,rws")

Любые переменные, объявленные вами в области действия #pragma, определяющей сегмент данных, будут размещены в этом сегменте данных, при условии, что они инициализированы. Если вы не укажете инициализатор, переменные будут размещены в сегменте данных по умолчанию, и #pragma не имеет силы. 

ПРИМЕЧАНИЕ 

В тот же момент оказывается, что эта особенность не позволяет использовать массивы объектов C++ в разделяемом сегменте данных, т.к. в C++ вы не можете инициализировать массив пользовательских объектов (предполагается, что этим должны заниматься их конструкторы по умолчанию). Это пересечение формальных требований C++ и расширений Microsoft, требующих наличия инициализаторов, оказывается фундаментальным ограничением. 

Директива #pragma comment вызывает добавление указанного ключа к командной строке компоновщика на этапе связывания. Вы могли бы использовать Project | Settings в VC++ и изменить командную строку компоновщика, однако трудно помнить про необходимость такого действия, когда вы перемещаете код с места на место (и обычная ошибка - забыть выбрать All Configurations при изменении установок и, таким образом, успешно отлаживать, но получить сбой в конфигурации Release). Итак, я обнаружил, что лучше всего помещать команду непосредственно в исходном файле. Заметим, что используемый текст должен соответствовать синтаксису командного ключа компоновщика. Это означает, что вы не должны включать в указанный текст пробелы, иначе компоновщик не обработает его должным образом. 

Обычно вы предоставляете некоторый механизм для установки дескриптора окна. Например, 

void SetWindow(HWND w)

 {

 hWnd = w;

 }

хотя эта операция, как я покажу далее, часто совмещена с собственно установкой хука. 

Пример: Мышиный Хук

заголовочный файл (myhook.h)

Здесь должны быть объявлены функции setMyHook и clearMyHook, но это требование разъяснено в моем очерке The Ultimate DLL Header File. 

#define UWM_MOUSEHOOK_MSG \

 _T("UMW_MOUSEHOOK-" \

 "{B30856F0-D3DD-11d4-A00B-006067718D04}")

исходный файл (myhook.cpp)

#include "stdafx.h"

#include "myhook.h"



#pragma data_seg(".JOE")

HWND hWndServer = NULL;

#pragma data_seg()

#pragma comment("linker, /section:.JOE,rws")



HINSTANCE hInstance;

UINT HWM_MOUSEHOOK;

HHOOK hook;



// опережающее объявление

static LRESULT CALLBACK msghook(int nCode, WPARAM wParam, LPARAM lParam);



/****************************************************************

* DllMain

* Вход:

* HINSTANCE hInst: Дескриптор экземпляра DLL

* DWORD Reason: причина вызова

* LPVOID reserved: зарезервировано

* Выход: BOOL

* TRUE при успешном завершении

* FALSE при наличии ошибок (не возвращается никогда)

* Действие:

* инициализация DLL.

****************************************************************/



BOOL DllMain(HINSTANCE hInst, DWORD Reason, LPVOID reserved)

{

 switch(Reason)

 { /* причина */

 //**********************************************

 // PROCESS_ATTACH

 //**********************************************

 case DLL_PROCESS_ATTACH:

 // Сохраним дескриптор экземпляра, т.к. он понадобится нам позднее для установки хука

 hInstance = hInst;

 // Данный код инициализирует сообщение уведомления хука

 UWM_MOUSEHOOK = RegisterWindowMessage(UWM_MOUSEHOOK_MSG);

 return TRUE;



 //**********************************************

 // PROCESS_DETACH

 //**********************************************

 case DLL_PROCESS_DETACH:

 // Если сервер не снял хук, снимем его, т.к. мы выгружаемся

 if(hWndServer != NULL)

 clearMyHook(hWndServer);

 return TRUE;

 } /* причина */

}



/****************************************************************

* setMyHook

* Вход:

* HWND hWnd: Окно, чей хук предстоит поставить

* Выход: BOOL

* TRUE если хук успешно поставлен

* FALSE если произошла ошибка, например, если хук

* уже был установлен

* Действие:

* Устанавливает хук для указанного окна

* Сначала устанавливает хук перехватывающий сообщения (WH_GETMESSAGE)

* Если установка прошла успешно, hWnd устанавливается в качестве

* окна сервера.

****************************************************************/



__declspec(dllexport) BOOL WINAPI setMyHook(HWND hWnd)

 {

 if(hWndServer != NULL)

 return FALSE;

 hook = SetWindowsHookEx(

 WH_GETMESSAGE,

 (HOOKPROC)msghook,

 hInstance,

 0);

 if(hook != NULL)

 { /* удача */

 hWndServer = hWnd;

 return TRUE;

 } /* удача */

 return FALSE;

 } // SetMyHook



/****************************************************************

* clearMyHook

* Вход:

* HWND hWnd: Окно, чей хук должен быть снят

* Выход: BOOL

* TRUE если хук успешно снят

* FALSE если вы передали неверный параметр

* Действие:

* Снимает установленный хук.

****************************************************************/

__declspec(dllexport) BOOL clearMyHook(HWND hWnd)

 {

 if(hWnd != hWndServer)

 return FALSE;

 BOOL unhooked = UnhookWindowsHookEx(hook);

 if(unhooked)

 hWndServer = NULL;

 return unhooked;

 }



/****************************************************************

* msghook

* Вход:

* int nCode: Значение кода

* WPARAM wParam: параметр

* LPARAM lParam: параметр

* Выход: LRESULT

*

* Действие:

* Если сообщение является сообщением о перемещении мыши, отправляет его

* окну сервера с координатами мыши

* Замечания:

* Функция должна быть CALLBACK-функцией, или она не будет работать!

****************************************************************/



static LRESULT CALLBACK msghook(int nCode, WPARAM wParam, LPARAM lParam)

 {

 // If the value of nCode is < 0, just pass it on and return 0

 // this is required by the specification of hook handlers

 // Если значение nCode < 0, просто передаем его дальше и возвращаем 0

 // этого требует спецификация обработчиков хуков

 if(nCode < 0)

 { /* передаем дальше */

 CallNextHookEx(hook, nCode,

 wParam, lParam);

 return 0;

 } /* передаем дальше */



 // Прочитайте документацию, чтобы выяснить смысл параметров WPARAM и LPARAM

 // Для хука WH_MESSAGE, LPARAM определяется как указатель на структуру MSG,

 // таким образом следующий код делает эту структуру доступной



 LPMSG msg = (LPMSG)lParam;



 // Если это сообщение о перемещении мыши, либо в клиентской (client), либо

 // в не клиентской (non-client) области, мы хотим уведомить родителя о его

 // возникновении. Заметим, что вместо SendMessage используется PostMessage

 if(msg->message == WM_MOUSEMOVE ||

 msg->message == WM_NCMOUSEMOVE)

 PostMessage(hWndServer,

 UWM_MOUSEMOVE, 

 0, 0); 



 // Передаем сообщение следующему хуку

 return CallNextHookEx(hook, nCode, 

 wParam, lParam);

 } // msghook

Приложение сервера

В заголовочном файле добавьте следующее в секцию protected класса: 

afx_msg LRESULT OnMyMouseMove(WPARAM,LPARAM);

В фале приложения добавьте это где-нибудь в начале файла: 

UINT UWM_MOUSEMOVE = ::RegisterWindowMessage(UWM_MOUSEMOVE_MSG);

Добавьте следующее в MESSAGE_MAP вне специальных комментариев //{AFX_MSG: 

ON_REGISTERED_MESSAGE(UWM_MOUSEMOVE, OnMyMouseMove)

В файл приложения добавьте следующую функцию: 

LRESULT CMyClass::OnMyMouseMove(WPARAM, LPARAM)

 {

 // ...тут что-то делаем

 return 0;

 }

 



Я написал небольшой пример для демонстрации, но поскольку я утомился создавать функцию глобального хука в n+1 раз, я сделал ему отличный пользовательский интерфейс. Кот смотрит из окна и следит за мышью. Но будьте осторожны! Подойдите достаточно близко к коту, и он схватит мышь! 

Вы можете скачать этот проект и собрать его. Ключевое значение имеет подпроект DLL; остальное - это использующая ее декоративная мишура. 

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