Поиск и анализ движущихся объектов по серии изображений
Хуки и
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)
{
{ /* причина */
//**********************************************
//
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;
}
/****************************************************************
* Вход:
* 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.
Список литературы
Для подготовки данной работы были использованы
материалы с сайта http://www.rsdn.ru/