Реферат: Хуки и DLL
Название: Хуки и DLL Раздел: Рефераты по информатике, программированию Тип: реферат | |||||||||||
Хуки и DLL Dr. Joseph M. Newcomer Перевод: Алексей Остапенко Существует большая неразбериха по поводу установки и использования глобальных хуков.
Заметим, что ни одна из описанных ниже проблем не возникает, если вы просто отлавливаете операции в своем собственном процессе. Они возникают только в том случае, когда вы хотите получать события на системном уровне. Основной проблемой здесь является адресное пространство. Когда глобальная 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 пытается отображать библиотеки в те же самые виртуальные области, что и у других экземпляров одной и той же библиотеки, всякий раз, когда это возможно. Она пытается. Ей может не удаться это сделать.
У перемещения есть и другие последствия. Если в 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, определяющей сегмент данных, будут размещены в этом сегменте данных, при условии, что они инициализированы. Если вы не укажете инициализатор, переменные будут размещены в сегменте данных по умолчанию, и #pragma не имеет силы.
Директива #pragma comment вызывает добавление указанного ключа к командной строке компоновщика на этапе связывания. Вы могли бы использовать Project | Settings в VC++ и изменить командную строку компоновщика, однако трудно помнить про необходимость такого действия, когда вы перемещаете код с места на место (и обычная ошибка - забыть выбрать All Configurations при изменении установок и, таким образом, успешно отлаживать, но получить сбой в конфигурации Release). Итак, я обнаружил, что лучше всего помещать команду непосредственно в исходном файле. Заметим, что используемый текст должен соответствовать синтаксису командного ключа компоновщика. Это означает, что вы не должны включать в указанный текст пробелы, иначе компоновщик не обработает его должным образом. Обычно вы предоставляете некоторый механизм для установки дескриптора окна. Например,
хотя эта операция, как я покажу далее, часто совмещена с собственно установкой хука. заголовочный файл (myhook.h) Здесь должны быть объявлены функции setMyHook и clearMyHook, но это требование разъяснено в моем очерке The Ultimate DLL Header File.
исходный файл (myhook.cpp)
В заголовочном файле добавьте следующее в секцию protected класса:
В фале приложения добавьте это где-нибудь в начале файла:
Добавьте следующее в MESSAGE_MAP вне специальных комментариев //{AFX_MSG:
В файл приложения добавьте следующую функцию:
Я написал небольшой пример для демонстрации, но поскольку я утомился создавать функцию глобального хука в n+1 раз, я сделал ему отличный пользовательский интерфейс. Кот смотрит из окна и следит за мышью. Но будьте осторожны! Подойдите достаточно близко к коту, и он схватит мышь! Вы можете скачать этот проект и собрать его. Ключевое значение имеет подпроект DLL; остальное - это использующая ее декоративная мишура. В этом примере показаны несколько других приемов, включая различные приемы рисования, использование ClipCursor и SetCapture, выбор региона, обновление экрана, и т.д. Таким образом, помимо демонстрации использования перехватывающей функции, для начинающих программистов этот пример имеет ценность в различных аспектах программирования под Windows. |