Майстри delphi, hooks - аспекти реалізації

Hooks - аспекти реалізації.

Відразу хочу зробити кілька застережень: мова в подальшому піде тільки про 32-х розрядної Windows і про глобальні пастках, тому що саме при їх програмуванні виникає більшість помилок; всі приклади будуть даватися на Delphi, тому що прикладів і описів для любителів С ++ досить.

Подальше оповідання припускає, що читач знайомий з основними принципами роботи з DLL і хоча б в загальних рисах уявляє механізм їх написання.

Що ж відбувається в системі коли ми "ставимо" пастку і що це взагалі таке - пастка.

Пастка (hook) - це механізм Windows, що дозволяє перехоплювати події, призначені деякого додатком, до того як ці події до цього додатка дійдуть.

Функції-фільтри - це функції, які отримують повідомлення про подію, що відбулася від пастки.

Залежно від типу пастки функції-фільтри можуть змінювати події, скасовувати їх або просто реагувати на них. Таким чином, коли ми говоримо "встановив пастку" ми маємо на увазі процес прикріплення функції-фільтра до вибраного нами типу пастки. Отже, коли ми в своїй програмі використовуємо функцію SetWindowsHookEx ми прикріплюємо функцію-фільтр, покажчик на яку ми і передаємо другим параметром, приклад:
SetWindowsHookEx (WH_SHELL, @ShellHook. HInstance, 0); в даному випадку ShellHook - це і є функція-фільтр. Надалі, під словосполученням "встановили пастку" будемо розуміти приєднання функції-фільтра до пастки.

Що ж відбувається після того, як ми встановили глобальну пастку. Розуміння наступного параграфа є ключем для розуміння механізму роботи пасток Windows, розташованих в DLL. Якщо ви не зрозумієте його, поверніться і перечитайте заново і так до тих пір, поки все не стане ясним.

Про те, що б повідомлення дійшло до n-1 пастки (hook n-1) повинен подбає сам програміст. Ось на цьому етапі дуже часто виникають помилки.

Для виклику наступної пастки в ланцюжку пасток в Windows використовується функція CallNextHookEx, першим параметром якої є дескриптор поточної пастки, одержуваний функцією SetWindowsHookEx. Тепер увага: ми встановили пастку в Process1, тобто функція SetWindowsHookEx виконувалася в DLL, що знаходиться в АП Process1 (див. рис.1) і, відповідно, дескриптор встановленої пастки повертається функцією SetWindowsHookEx належить даними DLL, що знаходяться в АП Process1. Нехай в Process2 виникає подія на яке поставлена ​​пастка, тоді Dll з першого процесу проектується на АП Process2, а дані DLL в Process2 инициализируются заново, і виходить, що в Process2 в змінної, в якій "лежав" дескриптор поставленої пастки в Process1, буде дорівнює 0. Функція-фільтр Process2, відпрацювавши, повинна буде передати повідомлення далі по ланцюжку пасток, тобто виконати функцію CallNextHookEx, першим параметром якої повинен бути дескриптор поточної пастки, але в даних DLL, що знаходиться в Process2 немає цього дескриптора (змінна, яка повинна містити його містить нуль). "Як же бути в такому випадку. Як же нам дізнатися дескриптор пастки, поставленої в іншому процесі, якщо самі процеси нічого не знають один про одного?" - запитаєте ви. На це питання я відповім трохи пізніше, а поки давайте поверхнево пробіжить по типам пасток, хоча інформація про типи повністю приведена в SDK.

Як ми вже знаємо, пастка встановлюється за допомогою Win32 API функції SetWindowsHookEx ():

function SetWindowsHookEx (idHook: integer; lpfn: TFNHookProc; hmod: HINST; dwThreadID: DWORD): HHOOK; stdcall;
idHook. описує тип установлюваної пастки. Даний параметр може приймати одне з наступних значень:

Фільтр додатки оболонки. Функція-фільтр пастки викликається, коли створюються і руйнуються вікна верхнього рівня або коли з додатком-оболонці потрібно стати активним.

Значення кожного з параметрів функції-фільтра пастки змінюється в залежності від типу встановлюється пастки. За більш детальними роз'ясненнями значень параметрів звертайтеся до довідки по Win32 API.

hmod. даний параметр повинен мати значення hInstance в EXE або DLL-файлах, в яких міститься функція-фільтр пастки (нагадаю, що це функція зворотного виклику). Якщо мова йде про глобальні пастках, то даний параметр може приймати тільки дескриптор DLL, з якої встановлюється пастка. Причина очевидна - EXE-файл не може бути відображений на АП іншого процесу, тоді як DLL-фали спеціально створені для цього. Підкреслю ця обставина ще раз: глобальні пастки можуть розташовуватися тільки в DLL, але ніяк не в EXE файлах.

dwThreadID. даний параметр ідентифікує потік, з яким буде пов'язана пастка. Ми ведемо мову про глобальні пастках, а тому цей параметр буде завжди дорівнює 0, що означає, що пастка буде пов'язана з усіма потоками в системі.

Значення, що повертається: функція SetWindowsHookEx повертає дескриптор встановленої пастки, саме цей дескриптор нам і треба буде зробити доступним ВСІМ екземплярів відображається DLL. Як це зробити я розповім після невеликого приклад, що показує на практиці необхідність зберігати дескриптор пастки для того, що б зуміти викликати попередню пастку в ланцюжку.

Зауваження. при установці двох пасток різного типу, система створить два ланцюжки пасток. Тобто кожному типу пастки відповідає своя ланцюжок. Так при установці пастки типу WH_MOUSE і WH_KEYBOARD обидві ці пастки будуть перебувати в різних ланцюжках і, відповідно, будуть оброблятися незалежно один від одного.

Щоб видалити функцію-фільтра з черги необхідно викликати функцію UnhookWindowsHookEx. Ця функція приймає дескриптор пастки, отриманий функцією SetWindowsHookEx. Якщо видалення не вдалося, то функція повертає нуль, інакше не нульове значення. Надалі, під виразом "зняти пастку" будемо мати на увазі видалення функції-фільтра.

Тепер, коли вам відомо як встановлювати пастку і як її знімати, розглянемо пару прикладів, які дадуть наочне уявлення про ізольованості АП процесів і вкажуть на одну з найпоширеніших помилок.

Думаю, тепер, розібравшись у вихідних кодах бібліотек з першого прикладу, ви зрозуміли, як НЕ треба писати DLL, з яких ви встановлюєте глобальні пастки. Уявіть, що користувач, який використовує вашу програму, в якій задіяні глобальні пастки, запустить іншу програму, яка так само встановить той же вид пастки, що і ваша, але встановить її в кінець черги, в такому випадку, якщо та, друга програма, буде написана неправильно - ваша програма перестане працювати тому що вашій пастці не надсилатимуться повідомлення з стоять попереду. Це приклад того, як неякісна робота одного програміста може зіпсувати прекрасно виконану роботу іншого.

Тепер пару слів про програмну реалізації всього вищесказаного.

Створює об'єкт файлового відображення. Ця функція повертає покажчик (handle) на об'єкт файлового відображення.

Ця функція закриває відображений в пам'ять файл і звільняє його дескриптор. При вдалому закриття функція повертає нульове значення і 0 в разі невдачі.

За детальною інформацією про параметри вищеописаних функцій звертайтеся до SDK, а так само розберіться в прикладі, який буде розібраний нижче.

Зауваження. першим параметром функції CreateFileMapping () повинен бути переданий дескриптор файлу, якого ми збираємося відобразити. Оскільки ми збираємося відображати дані в файл підкачки, то слід передавати значення $ FFFFFFFF або DWORD (-1), що відповідає тому ж значенню; але тому що гряде ера 64-розрядних систем, варто використовувати значення INVALID_HANDLE_VALUE, яке буде в 64 розрядної системі дорівнює $ FFFFFFFFFFFFFFFF відповідно. Для тих, хто переходив з ранніх версій Delphi на більш пізні (наприклад з Delphi2 на Delphi4) ті, можливо, стикалися з такого роду проблемами в своїх програмах.
Так як ми будемо створювати іменований об'єкт файлового відображення, то останнім параметром функції CreateFileMapping () передамо ім'я об'єкта, яке згодом будуть використовувати інші процеси для посилання на ту ж область пам'яті. Слід згадати про те, що створюваний таким чином об'єкт повинен мати фіксований розмір, тобто не може його змінювати по ходу програми.

Тепер ми володіємо всіма необхідними знаннями для розгляду другого прикладу. Відкрийте каталог Example2 і виконайте ті ж дії, що і в першому прикладі, попередньо уважно розібравшись у вихідних кодах. Після того як ви запустите обидва додатки і встановіть з них дві функції-фільтра одного типу, спробуйте натиснути правою кнопкою миші на будь-якому з вікон і ви побачите, що тепер відпрацьовують обидві встановлені пастки, незалежно від того, на якому з вікон відбулося натискання кнопки миші (тобто не дивлячись на те, з якого примірника DLL виконується виклик функції CallNextHookEx ()). Таким чином, коли який-небудь додаток буде відображати на своє АП DLL, в якій знаходиться функція-фільтр, цей екземпляр DLL буде мати доступ до даних, відображених в пам'ять з Process1 або Process2, в залежності від DLL. Думаю, після настільки докладних пояснень все повинно бути зрозуміло.

Дякую Юрія Зотова за надану підтримку.

Архів з прикладами до статті: example.zip

Список використаної літератури:

  1. Microsoft Win32 Software Development Kit.
  2. Стів Тейксейра і Ксав'є Пачеко, "Delphi5. Керівництво розробника. Том 1. Основні методи і технології".
  3. Kyle Marsh, "Hooks in Win32" (in the original).
  4. Dr. Joseph M. Newcomer, "Hooks and DLLs" (in the original).

Moscow Power Engineering Institute (Technical University)
Faculty of Nuclear Power Plants
27.02.02