Смекни!
smekni.com

Хукии 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, то студенты полагают, что имя имеет значение. А оно не имеет значения.