Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6269
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

Глава 21. Локальная память потока.docx 687

Статическая локальная память потока

Статическая локальная память потока основана па той же концепции, что и динамическая, — она предназначена для того, чтобы с потоком можно было сопоставить те или иные данные. Однако статическую TLS использовать гораздо проще, так как при этом не нужно обращаться к каким-либо функциям.

Возьмем такой пример: вы хотите сопоставлять стартовое время с каждым потоком, создаваемым программой. В этом случае нужно лить объявить переменную для хранения стартового времени:

_declspec(thread) DWORD gt_dwStartTime = 0;

Префикс __declspec(thread) — модификатор, поддерживаемый компилятором Microsoft Visual С++. Он сообщает компилятору, что соответствующую переменную следует поместить в отдельный раздел EXEили DLL-файла. Переменная, указываемая за __declspec(thread), должна быть либо глобальной, либо статической внутри (или вне) функции. Локальную переменную с модификатором __declspec(thread) объявить нельзя. Но это не должно вас беспокоить, ведь локальные переменные и так связаны с конкретным потоком. Кстати, глобальные TLS-переменные я помечаю префиксом gt_, а статические — st_.

Обрабатывая программу, компилятор выносит все TLS-переменные в отдельный раздел, и вы вряд ли удивитесь, что этому разделу присваивается имя .tls. Компоновщик объединяет эти разделы из разных объектных модулей и создает в итоге один большой раздел .tls, помещаемый в конечный EXEили DLL-файл.

Работа статической TLS строится на тесном взаимодействии с операционной системой. Загружая приложение в память, система отыскивает в EXE-файле раздел .tls и динамически выделяет блок памяти для хранения всех статических TLSпеременных. Всякий раз, когда ваша программа ссылается на одну из таких переменных, ссылка переадресуется к участку, расположенному в выделенном блоке памяти. В итоге компилятору приходится генерировать дополнительный код для ссылок на статические TLS-переменные, что увеличивает размер приложения и замедляет скорость его работы. В частности, на процессорах x86 каждая ссылка на статическую TLS-переменную заставляет генерировать три дополнительные машинные команды.

Если в процессе создается другой поток, система выделяет еще один блок памяти для хранения статических переменных нового потока. Только что созданный поток имеет доступ лишь к своим статическим TLS-переменным, и не может обратиться к TLS-переменным любого другого потока.

Вот так в общих чертах и работает статическая TLS-память. Теперь посмотрим, что происходит при участии DLL. Ведь скорее всего ваша программма, использующая статические TLS-переменные, связывается с какой-ни- будь DLL, в которой тоже применяются переменные этого типа. Загружая такую программу, система сначала определяет объем ее раздела .tls, а затем

688 Часть IV. Динамически подключаемые библиотеки

добавляет эту величину к сумме размеров всех разделов .tls, содержащихся в DLL, которые связаны с вашей программой. При создании потоков система автоматически выделяет блок памяти, достаточно большой, чтобы в нем уместились все TLS-переменные, необходимые как приложению, так и неявно связываемым с ней DLL. Все так хорошо, что даже не верится!

И не верьте! Подумайте, что будет, если приложение вызовет LoadLibrary и подключит DLL, тоже содержащую статические TLS-переменные. Системе придется проверить потоки, уже существующие в процессе, и увеличить их блоки TLS-памяти, чтобы подогнать эти блоки под дополнительные требования, предъявляемые новой DLL. Ну а если вы вызовете FreeLibrary для выгрузки DLL со статическими TLS-переменными, системе придется ужать блоки памяти, сопоставленные с потоками в данном процессе. К счастью, Windows Vista прекрасно со всем этим справляется.

Оглавление

 

Г Л А В А 2 2 Внедрение DLL и перехват API-вызовов ................................................

689

Пример внедрения DLL .......................................................................................................

690

Внедрение DLL с использованием реестра ..................................................................

692

Внедрение DLL с помощью ловушек ..............................................................................

694

Утилита для сохранения позиций элементов на рабочем столе........................

695

Внедрение DLL с помощью удаленных потоков .........................................................

707

Программа-пример InjLib ................................................................................................

711

Библиотека ImgWalk.dll....................................................................................................

718

Внедрение троянской DLL ..................................................................................................

720

Внедрение DLL как отладчика ...........................................................................................

720

Внедрение кода через функцию CreateProcess...........................................................

721

Перехват API-вызовов: пример ........................................................................................

722

Перехват API-вызовов подменой кода.......................................................................

723

Перехват API-вызовов с использованием раздела импорта ..............................

723

Программа-пример LastMsgBoxInfo.............................................................................

728

Г Л А В А 2 2

Внедрение DLL и перехват APIвызовов

В среде Windows каждый процесс получает свое адресное пространство. Указатели, используемые вами для ссылки на определенные участки памяти, — это адреса в адресном пространстве вашего процесса, и в нем нельзя создать указатель, ссылающийся на память, принадлежапгую другому процессу. Так, если в вашей программе есть «жучок», из-за которого происходит запись по случайному адресу, он не разрушит содержимое памяти, отведенной другим процессам.

Раздельные адресные пространства очень выгодны и разработчикам, и пользователям. Первым важно, что Windows перехватывает обращения к памяти по случайным адресам, вторым — что операционная система более устойчива и сбой одного приложения не приведет к краху другого или самой системы. Но, конечно, за надежность приходится платить: написать программу, способную взаимодействовать с другими программами или манипулировать другими процессами, теперь гораздо сложнее.

Вот ситуации, в которых требуется прорыв за границы процессов и доступ к адресному пространству другого процесса:

создание подкласса окна, порожденного другим процессом;

получение информации для отладки (например, чтобы определить, какие DLL используются другим процессом);

установка ловушек (hooks) в других процессах.

Вэтой главе я расскажу о нескольких механизмах, позволяющих внедрить (inject) какую-либо DLL в адресное пространство другого процесса, ваш код, попав в чужое адресное пространство, может устроить в нем настоящий хаос, поэтому хорошенько взвесьте, так ли вам необходимо это внедрение.

690 Часть IV. Динамически подключаемые библиотеки

Пример внедрения DLL

Допустим, вы хотите создать подкласс от экземпляра окна, порожденного другим процессом. Это, как вы помните, позволит изменять поведение окна. Все, что от вас для этого требуется, — вызвать функцию SetWindowLongPtr, чтобы заменить адрес оконной процедуры в блоке памяти, принадлежащем окну, новым — указывающим на вашу функцию WndProc. В документации Platform SDK утверждается, что приложение не может создать подкласс окна другого Процесса. Это не совсем верно. Проблема создания подкласса окна из другого процесса на самом деле сводится к преодолению границ адресного пространства.

Вызывая SetWindowLongPtr для создания подкласса окна (как показано ниже), вы говорите системе, что все сообщения окну, на которое указывает hwnd, следует направлять не обычной оконной процедуре, а функции MySubclassProc.

SetWindowLongPtr(hWnd, GWLP_WNDPROC, MySubclassProc);

Иными словами, когда системе надо передать сообщение процедуре WndProc указанного окна, она находит ее адрес и вызывает напрямую. В нашем примере система видит, что с окном сопоставлен адрес функции MySubclassProc, и поэтому вызывает именно ее, а не исходную оконную процедуру.

Проблема с созданием подкласса окна, принадлежащего другому процессу, состоит в том, что процедура подкласса находится в чужом адресном пространстве. Упрощенная схема приема сообщений оконной процедурой представлена на рис. 22-1. Процесс A создает окно. На адресное пространство этого процесса проецируется файл User32.dll. Эта проекция User32.dll отвечает за прием и диспетчеризацию сообщений (синхронных и асинхронных), направляемых любому из окон, созданных потоками процесса А. Обнаружив какое-то сообщение, она определяет адрес процедуры WndProc окна и вызывает ее, передавая описатель окна, сообщение и параметры wParam и lParam. Когда WndProc обработает сообщение, User32.dll вернется в начало цикла и будет ждать следующее оконное сообщение.

Теперь допустим, что процесс B хочет создать подкласс окна, порожденного одним из потоков процесса А. Сначала код процесса В должен определить описатель этого окна, что можно сделать самыми разными способами. В примере на рис. 22-1 поток процесса B просто вызывает FindWindow, затем — Set WindowLongPtr, пытаясь изменить адрес процедуры WndProc окна. Обратите внимание: пытаясь. Этот вызов не даст ничего, кроме NULL. Функция SetWindowLongPtr просто проверяет, не хочет ли процесс изменить адрес WndProc окна, созданного другим процессом, и, если да, игнорирует вызов.

А если бы функция SetWindowLongPtr могла изменить адрес WndProc? Система тогда связала бы адрес процедуры MySubclassProc с указанным окном. Затем при посылке сообщения этому окну код User32 в процессе A из-

Глава 22. Внедрение DLL и перехват API-вызовов.docx 691

лек бы данное сообщение, получил адрес MySubclassProc и попытался бы вызвать процедуру по этому адресу. Но это привело бы к крупным неприятностям, так как MySubclassProc находится в адресном пространстве процесса B, а активен — процесс А. Очевидно, если бы User32 обратился по данному адресу, то на самом деле он обратился бы к какому-то участку памяти в адресном пространстве процесса А, что, естественно, привело бы к нарушению доступа к памяти.

Рис. 22-1. Поток процесса B пытается создать подкласс окна, сформированного потоком процесса А

Чтобы избежать этого, было бы неплохо сообщить системе, что MySubclassProc находится в адресном пространстве процесса B, и тогда она переключила бы контекст перед вызовом процедуры подкласса. Увы, по ряду причин такая функциональность в системе не реализована.

Подклассы окон, созданных потоками других процессов, порождаются весьма редко. Большинство приложений делает это лишь применительно к собственным окнам, и архитектура памяти в Windows этому не препятствует.

Переключение активных процессов отнимает слишком много процессорного времени.

Код MySubclassProc должен был бы выполняться потоком процесса B, но каким именно — новым или одним из существующих?

Как User32.dll узнает, с каким процессом связан адрес оконной процедуры?

Поскольку удачных решений этих проблем нет, Майкрософт предпочла запретить функции SetWindowLongPtr замену процедуры окна, созданного другим процессом.

692 Часть IV. Динамически подключаемые библиотеки

Тем не менее порождение подкласса окна, созданного чужим процессом, возможно: нужно просто пойти другим путем. Ведь на самом деле проблема не столько в создании подкласса, сколько в закрытости адресного пространства процесса. Если бы вы могли как-то поместить код своей оконной процедуры в адресное пространство процесса A, это позволило бы вызвать SetWindowLongPtr и передать ей адрес MySubclassProc в процессе А. Я называю такой прием внедрением (injecting) DLL в адресное пространство процесса. Мне известно несколько способов подобного внедрения. Рассмотрим их по порядку, начиная с простейшего.

Примечание. Если вы планируете порождать подклассы окон, созданных одним и тем же процессом, воспользуйтесь функциями SetWindowSubclass,

GetWindowSubclass, RemoveWindowSubclass и DefSubclassProc (см. статью

«Subclassing Controls» по адресу http://msdn2.microsoft.com/enus/library/ms649784.aspx).

Внедрение DLL с использованием реестра

Если вы уже работали с Windows, то знаете, что такое реестр. В нем хранится конфигурация всей системы, и, модифицируя в реестре те или иные параметры, можно изменить поведение системы. Я намерен поговорить о параметре реестра:

HKEY_LOCAL_MACHINE\Softwaге\Мicrosoft

\Windows NT\CurrrentVersion\Windows\

Ниже показано, как он выгляди в окне Registry Editor:

Список параметров в разделе реестра, где находится AppInit_DLLs, можно просмотреть с помощью программы Registry Editor (Редактор реестра). Значением параметра AppInitJDLLs может быть как имя одной DLL (с указанием пути доступа), так и имена нескольких DLL, разделенных пробелами

или

запятыми.

Поскольку

пробел

используется здесь в качестве разделите-

ля,

в именах

файлов не

должно

быть пробелов. Система считывает, путь

Глава 22. Внедрение DLL и перехват API-вызовов.docx 693

только первой DLL в списке — пути остальных DLL игнорируются, поэтому лучше размещать свои DLL в системном каталоге Windows, чтобы не указывать пути. Как видите, я указал в параметре AppInit_DLLs только одну DLL и задал путь к ней: C:\MyLib.dll.

При следующей перезагрузке компьютера Windows сохранит значение этого параметра. Далее, когда User32.dll будет спроецирован на адресное пространство процесса, этот модуль получит уведомление DLL_PROCESS_ ATTACH и после его обработки вызовет LoadLibrary для всех DLL, указанных в параметре AppInit_DLLs. В момент загрузки каждая DLL инициализируется вызовом ее функции DllMain с параметром fwdReason, равным DLL_PROCESS_ATTACH. Поскольку внедряемая DLL загружается на такой ранней стадии создания процесса, будьте особенно осторожны при вызове функций. Проблем с вызовом функций Kernel32.dll не должно быть, но в случае других DLL они вполне вероятны — User32.dll не проверяет, успешно ли загружены и инициализированы эти DLL.

Это простейший способ внедрения DLL. Все, что от вас требуется, — добавить значение в уже существующий параметр реестра. Однако он не лишен недостатков.

Ваша DLL проецируется на адресные пространства только тех процессов, на которые спроецирован и модуль User32.dll. Его используют все GUIприложения, но большинство программ консольного типа — нет. Поэтому такой метод не годится для внедрения DLL, например, в компилятор или компоновщик.

Ваша DLL проецируется на адресные пространства всех GUI-процессов. Но вам-то почти наверняка надо внедрить DLL только в один или несколько определенных процессов. Чем больше процессов попадет «под тень» такой DLL, тем выше вероятность аварийной ситуации. Ведь теперь ваш код выполняется потоками этих процессов, и, если он зациклится или некорректно обратится к памяти, вы повлияете на поведение и устойчивость соответствующих процессов. Поэтому лучше внедрять свою DLL в как можно меньшее число процессов.

Ваша DLL проецируется на адресное пространство каждого GUI-процесса в течение всей его жизни. Тут есть некоторое сходство с предыдущей проблемой. Желательно не только внедрять DLL в минимальное число процессов, но и проецировать ее на эти процессы как можно меньшее время. Допустим, вы хотите создать подкласс главного окна Word Pad в тот момент, когда пользователь запускает ваше приложение. Естественно, пока пользователь не откроет ваше приложение, внедрять DLL в адресное пространство Word Pad не требуется. Когда пользователь закроет ваше приложение, целесообразно отменить переопределение оконной процедуры Word Pad. И в этом случае DLL тоже незачем «держать» в адресном пространстве Word Pad. Так что лучшее решение — внедрять DLL только на то время, в течение которого она действительно нужна конкретной программе.

694 Часть IV. Динамически подключаемые библиотеки

Внедрение DLL с помощью ловушек

Внедрение DLL в адресное пространство процесса возможно и с применением ловушек. Чтобы они работали так же, как и в 16-разрядной Windows, Майкрософт пришлось создать механизм, позволяющий внедрять DLL в адресное пространство другого процесса. Рассмотрим его на примере.

Процесс A (вроде утилиты Spy++) устанавливает ловушку WH_ GETMESSAGE и наблюдает за сообщениями, которые обрабатываются окнами в системе, Ловушка устанавливается вызовом SetWindowsHookEx:

ННООК hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstDll, 0);

Аргумент WH_GETMESSAGE определяет тип ловушки, а параметр GetMsgProc — адрес функции (в адресном пространстве вашего процесса), которую система должна вызывать всякий раз, когда окно собирается обработать сообщение. Параметр hinstDll идентифицирует DLL, содержащую функцию GetMsgProc. В Windows значение hinstDll для DLL фактически задает адрес в виртуальной памяти, по которому DLL спроецирована на адресное пространство процесса. И, наконец, последний аргумент, 0, указывает поток, для которого предназначена ловушка. Поток может вызвать SetWindowsHookEx и передать ей идентификатор другого потока в системе. Передавая 0, мы сообщаем системе, что ставим ловушку для всех существующих в ней GUT-потоков.

Теперь посмотрим, как все это действует:

1.Поток процесса B собирается направить сообщение какому-либо окну.

2.Система проверяет, не установлена ли для данного потока ловушка

WH_GETMESSAGE.

3.Затем выясняет, спроецирована ли DLL, содержащая функцию GetMsgProc, на адресное пространство процесса В.

4.Если указанная DLL еще не спроецирована, система отображает ее на адресное пространство процесса B и увеличивает счетчик блокировок (lock count) проекции DLL в процессе B на 1.

5.Система проверяет, не совпадают ли значения hinstDll этой DLL, относящиеся к процессам A и B. Если hinstDll в обоих процессах одинаковы, то и адрес GetMsgProc в этих процессах тоже одинаков. Тогда система может просто вызвать GetMsgProc в адресном пространстве процесса B. Если же hinstDll различны, система определяет адрес функции GetMsgProc в адресном пространстве процесса В по формуле:

GetMsgProc В =hInstDll В + (GetMsgProc А - hInstDll А)

Вычитая hinstDll A из GetMsgProc А, вы получаете смещение (в байтах) адреса функции GetMsgProc. Добавляя это смещение к hinstDll B, вы получаете адрес GetMsgPtvc,. соответствующий проекции DLL в адресном пространстве процесса В.

Глава 22. Внедрение DLL и перехват API-вызовов.docx 695

6.Счетчик блокировок проекции DLL в процессе В увеличивается на 1.

7.Вызывается GetMsgProc в адресном пространстве процесса В.

8.После возврата из GetMsgProc счетчик блокировок проекции DLL в адресном пространстве процесса В уменьшается на 1.

Кстати, когда система внедряет или проецирует DLL, содержащую функцию фильтра ловушки, проецируется вся DLL, а не только эта функция. А значит, потокам, выполняемым в контексте процесса В, теперь доступны все функции такой

DLL.

Итак, чтобы создать подкласс окна, сформированного потоком другого процесса, можно сначала установить ловушку WH_GETMESSAGE для этого потока, а затем — когда будет вызвана функция GetMsgProc — обратиться к SetWindowLongPtr и создать подкласс. Разумеется, процедура подкласса должна быть в той же DLL, что и GetMsgProc.

В отличие от внедрения DLL с помощью реестра этот способ позволяет в любой момент отключить DLL от адресного пространства процесса, для чего достаточно вызвать:

BOOL UnhookWindowsHookEx(HHOOK hHook);

Когда поток обращается к этой функции, система просматривает внутренний список процессов, в которые ей пришлось внедрить данную DLL, и уменьшает счетчик ее блокировок на 1. Как только этот счетчик обнуляется, DLL автоматически выгружается. Вспомните: система увеличивает его непосредственно перед вызовом GetMsgProc (см. выше п. 6). Это позволяет избежать нарушения доступа к памяти. Если бы счетчик не увеличивался, то другой поток мог бы вызвать UnhookWindowsHookEx в тот момент1, когда поток процесса В пытается выполнить код GetMsgProc.

Все это означает, что нельзя создать подкласс окна и тут же убрать ловушку — она должна действовать в течение всей жизни подкласса.

Утилита для сохранения позиций элементов на рабочем столе

Эта утилита (DIPS.exe), использует ловушки окон для внедрения DLL в адресное пространство Explorer.exe. Файлы исходного кода и ресурсов этой программы и DLL находятся в каталогах 22-DIPS и 22-DIPSLib внутри архива, доступного на веб-сайте поддержки этой книги.

Компьютер я использую в основном для работы, и, на мой взгляд, самое оптимальное в этом случае разрешение экрана — 1400 х 1050. Иногда я демонстрирую презентации через видеопроектор, рассчитанный на более низкое разрешение. Когда у меня появляется настроение поиграть, приходится открывать апплет Display в Control Panel и устанавливать разрешение, необходимое проектору, а закончив игру, вновь возвращаться в Display и восстанавливать разрешение 1400 х 1050.

Возможность изменять экранное разрешение «на лету» — очень удобная функция Windows. Единственное, что мне не по душе, — при смене экран-

Соседние файлы в предмете Программирование на C++