Хукі b> b> і b> DLL b> p>
Dr. Joseph M.
Newcomer p>
Переклад: Олексій Остапенко p>
Існує велика
плутанина з приводу встановлення і використання глобальних хуков. p>
ПРИМІТКА p>
Можливо варто згадати, що камбали
(прим. перекладача: Flounder - псевдонім автора) в цілому не схвалюють
використання гачків (hooks), але такі гачки (хукі) здаються припустимими. p>
Зауважимо, що ні один із таких проблем не
виникає, якщо ви просто відловлює операції у своєму власному процесі.
Вони виникають тільки в тому випадку, коли ви хочете отримувати події на
системному рівні. p>
Основною проблемою тут є адресне
простір. Коли глобальна DLL виконується, вона виконується в контексті
того процесу, чиє подія перехоплюється. Це означає, що адреси, які
бачить DLL, навіть для своїх власних змінних, що є адресами в контексті
цільового процесу. Оскільки це DLL, вона має окрему копію своїх даних
для кожного використовує її процесу. І це означає, що будь-які значення,
які ви використовуєте для глобальних DLL змінних (таких, як
оголошені на рівні файлу), є приватними і не будуть наслідувати нічого
з початкового контексту бібліотеки. Вони будуть ініціалізувати заново, тобто,
звичайно, вони будуть дорівнювати нулю. p>
Недавнє повідомлення навіть пропонував концепцію
збереження callback-адреси в DLL. Це неможливо. Ну, неможливо не зберегти
його, а неможливо його використовувати. Те, що ви зберегли, - це пачка бітів.
Навіть якщо ви пройдете викладеної нижче інструкції по створенню розділяється
змінної, видимої в усіх примірниках DLL, набір бітів (який ви вважає
адресою) насправді є адресою тільки в контексті процесу,
зберіг цей набір. Для всіх інших процесів це всього лише набір
бітів, і якщо ви спробуєте використовувати його в якості адреси, ви звернетеся
з якого-то адресою в процесі, подія якого було перехоплено, що
нікому не потрібне. У більшості випадків це просто призведе до падіння
додатки. p>
Концепція розділених адресних просторів важка для
розуміння. Дозвольте мені продемонструвати її на зображенні. p>
p>
Тут ми маємо три процеси. Ваш Процес показаний зліва
(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, то ви могли б перейти
до іншого процесу, досліджувати його і побачити, що нове значення з'явилося
також і тут. p>
Але от облом: однакову адресу - це збіг. Це
збіг абсолютно і однозначно не гарантується. Подивіться на Процес B.
Коли подія перехоплено в Процесі B, в нього відображається DLL. Але адреси,
займані нею у Вашому Процесі і Процесі A, не доступні в адресному
просторі Процесу B. Тому відбувається переміщення коду на іншу адресу в
Процесі B. Код в порядку, йому дійсно байдуже за якою адресою він
виповнюється. Адреса даних підправлений так, щоб посилатися на нове положення
даних, і навіть колективні дані відображені в інше безліч адрес, таким
чином до них звертаються по-іншому. Якщо б ви використовували відладчик з
Процесом B і подивилися б на адресу & something в розділяється області, ви
б виявили, що адреса something був би іншим, але вміст something було
б тим же самим; виконання зміни вмісту у Вашому Процесі або в
Процесі A негайно зробило б це зміна видимим у Процесі B, хоча
Процес B і бачить його за іншою адресою. Це те ж саме місце фізичної
пам'яті. Віртуальна пам'ять - це відображення між адресами, видимими вами, як
програмістом, і фізичними сторінками пам'яті, які в дійсності
містить ваш комп'ютер. p>
Хоча я і назвав однакове розташування збігом,
"відповідність" частково навмисне; Windows намагається відображати
бібліотеки в ті ж самі віртуальні області, що й в інших екземплярів однієї
і тієї ж бібліотеки, кожного разу, коли це можливо. Вона намагається. Їй може не
вдасться це зробити. p>
ПРИМІТКА p>
Якщо ви знаєте трохи більше
(достатньо, щоб це представляло небезпеку), ви можете сказати: "Ага!
Я можу перемістити (rebase) мою DLL так, що вона завантажується по не
конфліктуючими адресу, і я зможу проігнорувати цю особливість ". Це
відмінний приклад того, як малі знання можуть стати серйозною
небезпеку. Ви не можете гарантувати що така DLL буде працювати з будь-яким
можливим виконуваним модулем, який може бути будь-коли запущений на вашій
машині! Оскільки це DLL глобального хука, вона може бути викликана з Word,
Excel, Visio, VC + + і шести тисяч додатків, про які ви ніколи не чули,
але можете будь-коли запустити або може запустити ваш клієнт. Тому
забудьте про це. Не намагайтеся переміщувати. В кінці кінців, ви програєте.
Звичайно, в самий невідповідний час з найважливішим вашим клієнтом (наприклад, з
браузером вашого продукту з журналу чи з вашим дуже багатим
замовником, який вже стурбований іншими помилками, які у вас можуть
бути ...). Вважайте, що розділяється сегмент даних "переміщаємо".
Якщо ви не розумієте цей параграф, значить ви знаєте не досить багато,
щоб це представляло небезпеку. І ви можете спокійно його ігнорувати. p>
У переміщення є й інші наслідки. Якщо в DLL ви
зберегли вказівник на callback-функцію в контексті Вашого Процесу, то для DLL
безглуздо викликати її в Процесі A або Процесі B. Ця адреса призведе до
передачу управління в вказує їм область, що нормально, але ця передача
відбудеться в адресний простір Процесу A або Процесу B, що абсолютно
марно, не кажучи вже про те, що майже напевно фатально. p>
Це також означає, що ви не можете використовувати в
своєї DLL нічого з MFC. Вона не може бути ні MFC DLL, ні MFC Extension DLL.
Чому? Тому, що вона буде викликати функції MFC. А де вони? Ну, вони у вашому
адресному просторі. А не в адресному просторі Процесу A, написаного на
Visual Basic, або Процесу B, написаного на Java. Таким чином, ви повинні
написати DLL на чистому C, і я б рекомендував зовсім не використовувати бібліотеку
виконавчі C (CRT). Ви повинні використовувати тільки API. Використовуйте
lstrcpy замість strcpy або tcscpy, lstrcmp замість strcmp або tcscmp, і т.д. p>
Існує безліч рішень для організації
взаємодії вашої DLL і її керуючого сервера. Одне з рішень полягає
у використанні:: PostMessage або:: SendMessage (зауважимо, що тут я посилаються
на виклики чистого API, а не виклики MFC!). Там, де можливо використовувати виклик
:: PostMessage, краще використовуйте його, а не:: SendMessage, тому що інакше ви можете
отримати небезпечні тупикові ситуації. Якщо Ваш Процес у результаті зупиняється,
всі інші процеси в системі зупиняться, тому що всі заблоковані на виклик
:: SendMessage, який ніколи не повернеться, і ви просто вивели всю систему
з ладу з можливістю серйозної втрати даних у важливих для користувача
додатках. Це Абсолютно Однозначно Чи не Гарна Ситуація. p>
Ви також можете використовувати інформаційні черги в
розділяється області пам'яті, але я буду вважати цю тему не включається в рамки
даного огляду. p>
Ви не можете повернути вказівник із викликів
:: SendMessage і:: PostMessage (ми забудемо про можливість передавати назад
відносні покажчики в поділювану область пам'яті; це також виходить за
рамки цієї статті). Це з-за того, що будь-покажчик, який ви можете
створити, буде посилатися або на адресу в DLL (переміщеною в перехоплений процес),
або на адресу в перехопленому процесі (Процесі A або Процесі B), і,
отже, він буде абсолютно не потрібний у Вашому Процесі. Ви можете
повертати лише адресно-незалежну інформацію в WPARAM або LPARAM. p>
Я сильно рекомендую використовувати для таких цілей
Зареєстровані Віконні Повідомлення (дивіться мій огляд по Управлінню
Повідомленнями). Ви можете використати макрос ON_REGISTERED_MESSAGE в
MESSAGE_MAP вікна, якому ви надсилаєте повідомлення. p>
Основною вимогою тепер є отримання HWND
цього вікна. На щастя, це нескладно. p>
Перше, що ви повинні зробити - це створити
розділяється сегмент даних. Це робиться за допомогою оголошення # pragma
data_seg. Виберіть будь-яке гарне мнемонічне ім'я для сегменту даних
(воно має бути не довше 8 символів). Просто щоб підкреслити
довільність імені, я використав тут своє власне ім'я. Під час
викладання я виявив, що якщо я використовую імена виду. SHARE або. SHR, або
. SHRDATA, то студенти вважають, що ім'я має значення. А воно не має
значення. p>
# pragma
data_seg ( ". JOE") p>
HANDLE
hWnd = NULL; p>
# pragma
dta_seg () p>
# pragma
comment (linker, "/ section:. JOE, rws") p>
Будь-які змінні, оголошені вами в області дії
# pragma, що визначає сегмент даних, будуть розміщені в цьому сегменті даних,
за умови, що вони ініціалізований. Якщо ви не вкажете ініціалізатор, змінні
будуть розміщені в сегменті даних за замовчуванням, і # pragma не має сили. p>
ПРИМІТКА p>
У той же момент виявляється, що ця
особливість не дозволяє використовувати масиви об'єктів C + + в поділюваному
сегменті даних, тому що в C + + ви не можете ініціалізувати масив
призначених для користувача об'єктів (передбачається, що цим повинні займатися їх
конструктори за умовчанням). Це перетин формальних вимог C + + і
розширень Microsoft, що вимагають наявності ініціалізаторов, виявляється
фундаментальним обмеженням. p>
Директива # pragma comment викликає додавання
зазначеного ключа до командного рядка компонувальника на етапі зв'язування. Ви могли
б використовувати Project | Settings в VC + + і змінити командний рядок
компонувальника, однак важко пам'ятати про необхідність такої дії, коли ви
переміщаєте код з місця на місце (і звичайна помилка - забути вибрати All
Configurations при зміні установок і, таким чином, успішно налагоджувати, але
отримати збій в конфігурації Release). Отже, я виявив, що краще за все розміщувати
команду безпосередньо у вихідному файлі. Зауважимо, що використовуваний текст
повинен відповідати синтаксису командного ключа Компоновнику. Це означає,
що ви не повинні включати в зазначений текст прогалини, інакше компонувальник НЕ
опрацює його належним чином. p>
Зазвичай ви надаєте певний механізм для
установки дескриптора вікна. Наприклад, p>
void SetWindow (HWND w) p>
( p>
hWnd = w; p>
) p>
хоча ця операція, як я покажу далі, часто поєднана
з власне установкою хука. p>
Приклад: Мишачий Хук
p>
заголовки (myhook.h) p>
Тут повинні бути оголошені функції setMyHook і
clearMyHook, але ця вимога роз'яснено в моєму нарисі The
Ultimate DLL Header File. p>
# define UWM_MOUSEHOOK_MSG p>
_T ( "UMW_MOUSEHOOK-" p>
"(B30856F0-D3DD-11d4-A00B-006067718D04 }") p>
вихідний файл (myhook.cpp) p>
# include "stdafx.h" p>
# include "myhook.h" p>
# pragma data_seg ( ". JOE") p>
HWND hWndServer = NULL; p>
# pragma data_seg () p>
# pragma comment ( "linker,/section:. JOE, rws") p>
HINSTANCE hInstance; p>
UINT HWM_MOUSEHOOK; p>
HHOOK hook; p>
// випереджаючий оголошення p>
static LRESULT CALLBACK msghook (int nCode, WPARAM wParam, LPARAM
lParam); p>
/********************************************** ****************** p>
* DllMain p>
* Вхід: p>
* HINSTANCE hInst: Дескриптор примірника DLL p>
* DWORD Reason: причина виклику p>
* LPVOID reserved: зарезервовано p>
* Вихід: BOOL p>
* TRUE при успішному
завершення p>
* FALSE при наявності помилок
(не повертається ніколи) p>
* Дія: p>
* ініціалізація DLL. p>
*********************************************** *****************/ p>
BOOL DllMain (HINSTANCE hInst, DWORD Reason, LPVOID reserved) p>
( p>
switch (Reason) p>
(/ * причина */ p>
//********************************************* * p>
// PROCESS_ATTACH p>
//********************************************* * p>
case DLL_PROCESS_ATTACH: p>
// Збережемо дескриптор екземпляра, тому що він
знадобиться нам пізніше для встановлення хука p>
hInstance = hInst; p>
// Цей код ініціалізує повідомлення
повідомлення хука p>
UWM_MOUSEHOOK =
RegisterWindowMessage (UWM_MOUSEHOOK_MSG); p>
return TRUE; p>
//********************************************* * p>
// PROCESS_DETACH p>
//********************************************* * p>
case DLL_PROCESS_DETACH: p>
// Якщо сервер не зняв хук, знімемо його, тому що ми вивантажується p>
if (hWndServer! = NULL) p>
clearMyHook (hWndServer); p>
return TRUE; p>
)/* причина */ p>
) p>
/********************************************** ****************** p>
* setMyHook p>
* Вхід: p>
* HWND hWnd: Вікно, чий хук
належить поставити p>
* Вихід: BOOL p>
* TRUE якщо хук успішно
поставлений p>
* FALSE якщо відбулася
помилка, наприклад, якщо хук p>
* вже був встановлений p>
* Дія: p>
* Встановлює хук для
вказаного вікна p>
* Спочатку встановлює хук
перехоплює повідомлення (WH_GETMESSAGE) p>
* Якщо установка пройшла
успішно, hWnd встановлюється як p>
* вікна сервера. p>
*********************************************** *****************/ p>
__declspec (dllexport) BOOL WINAPI setMyHook (HWND hWnd) p>
( p>
if (hWndServer! = NULL) p>
return FALSE; p>
hook = SetWindowsHookEx ( p>
WH_GETMESSAGE, p>
(HOOKPROC) msghook, p>
hInstance, p>
0); p>
if (hook! = NULL) p>
(/ * удача */ p>
hWndServer = hWnd; p>
return TRUE; p>
)/* удача */ p>
return FALSE; p>
)// SetMyHook p>
/********************************************** ****************** p>
* clearMyHook p>
* Вхід: p>
* HWND hWnd: Вікно, чий хук
повинен бути знятий p>
* Вихід: BOOL p>
* TRUE якщо хук успішно
знятий p>
* FALSE якщо ви передали
невірний параметр p>
* Дія: p>
* Знімає встановлений
хук. p>
*********************************************** *****************/ p>
__declspec (dllexport) BOOL
clearMyHook (HWND hWnd) p>
( p>
if (hWnd! = hWndServer) p>
return FALSE; p>
BOOL unhooked =
UnhookWindowsHookEx (hook); p>
if (unhooked) p>
hWndServer = NULL; p>
return unhooked; p>
) p>
/********************************************** ****************** p>
* msghook p>
* Вхід: p>
* int nCode: Значення коду p>
* WPARAM wParam: параметр p>
* LPARAM lParam: параметр p>
* Вихід: LRESULT p>
* p>
* Дія: p>
* Якщо повідомлення є
повідомленням про переміщення миші, відправляє його p>
* вікна сервера з
координатами миші p>
* Зауваження: p>
* Функція повинна бути
CALLBACK-функцією, або вона не буде працювати! P>
*********************************************** *****************/ p>
static LRESULT CALLBACK msghook (int nCode, WPARAM wParam, LPARAM
lParam) p>
( p>
// If the value of nCode is
<0, just pass it on and return 0 p>
// this is required by the
specification of hook handlers p>
// Якщо значення nCode <0, просто передаємо його далі і повертаємо
0 p>
// цього вимагає специфікація обробників
хуков p>
if (nCode <0) p>
(/ * передаємо далі */ p>
CallNextHookEx (hook, nCode, p>
wParam, lParam); p>
return 0; p>
)/* передаємо далі */ p>
// Прочитайте документацію, щоб з'ясувати
сенс параметрів WPARAM і LPARAM p>
// Для хука WH_MESSAGE, LPARAM визначається
як покажчик на структуру MSG, p>
// таким чином наступний код робить цю
структуру доступною p>
LPMSG msg = (LPMSG) lParam; p>
// Якщо це повідомлення про переміщення миші,
або в клієнтської (client), або p>
// в не клієнтської (non-client) області, ми
хочемо повідомити батька про його p>
// виникненні. Зауважимо, що замість
SendMessage використовується PostMessage p>
if (msg-> message == WM_MOUSEMOVE | | p>
msg-> message ==
WM_NCMOUSEMOVE) p>
PostMessage (hWndServer, p>
UWM_MOUSEMOVE, p>
0, 0); p>
// Передаємо повідомлення наступного Хуку p>
return CallNextHookEx (hook, nCode, p>
wParam, lParam); p>
)// msghook p>
Додаток сервера
p>
У заголовки додайте наступне в секцію
protected класу: p>
afx_msg
LRESULT OnMyMouseMove (WPARAM, LPARAM); p>
У Фалєєв програми додайте це десь на початку
файла: p>
UINT
UWM_MOUSEMOVE =:: RegisterWindowMessage (UWM_MOUSEMOVE_MSG); p>
Додайте наступне у MESSAGE_MAP поза спеціальними
коментарів// (AFX_MSG: p>
ON_REGISTERED_MESSAGE (UWM_MOUSEMOVE,
OnMyMouseMove) p>
У файл програми додайте наступну функцію: p>
LRESULT
CMyClass:: OnMyMouseMove (WPARAM, LPARAM) p>
( p>
//
... тут щось робимо p>
return
0; p>
) p>
p>
Я написав невеликий приклад
для демонстрації, але оскільки я втомився створювати функцію глобального хука в
n +1 раз, я зробив йому відмінний інтерфейс користувача. Кіт дивиться з вікна
і стежить за мишею. Але будьте обережні! Підійдіть достатньо близько до кота, і
він схопить миша! p>
Ви можете завантажити цей проект і зібрати його. Ключове
значення має підпроект DLL; інше - це що використовує її декоративна
мішура. p>
У цьому прикладі показано кілька інших прийомів,
включаючи різні прийоми малювання, використання ClipCursor і SetCapture,
вибір регіону, оновлення екрану, і т.д. Таким чином, крім демонстрації
використання перехоплює функції, для початківців програмістів цей приклад
має цінність у різних аспектах програмування під Windows. p>
Список літератури h2>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>