Глава 6. Некоторые вопросы системного программирования в Windows
Большая часть материала этой главы будет посвящена управлению памятью Windows 9x. Данный материал требует от читателя некоторой подготовки в области так называемого защищенного режима микропроцессоров Intel. Более подробно о защищенном режиме можно узнать в книгах [1,5].
I
О страничной и сегментной адресации. Начну изложение с некоторого исторического экскурса. Семейство микропроцессоров Intel47 ведет свое начало с микропроцессора Intel 8086. В настоящее время во всю работает уже седьмое поколение. Каждое новое поколение отличалось от предыдущего в программном отношении, главным образом, расширением набора команд. Но были в этой восходящей лестнице и две ступени, сыгравшие огромную роль в развитии компьютеров на базе микропроцессоров Intel. Это микропроцессор 80286 (защищенный режим) и микропроцессор 80386 (страничная адресация).
До появления микропроцессора 80286 микропроцессоры использовались в так называемом реальном режиме адресации. Кратко изложу, в чем заключался этот режим. Для программирования использовался так называемый логический адрес, состоящий из двух 16-битных компонент: сегмента и смещения. Сегментный адрес мог храниться в одном из трех сегментных регистров CS,DS, SS,ES. Смещение хранилось в одном из индексных регистров DI, SI,BX,BP,SP48. При обращении к памяти логический адрес подвергался преобразованию, заключающемуся в том, что к смещению прибавлялся сегментный адрес, сдвинутый на четыре бита влево. В результате получался 20-битный адрес, который, как легко заметить, мог охватывать всего около 1 Мб памяти49. Операционная система MS DOS и была изначально рассчитана для работы в таком адресном пространстве. Получаемый 20-битный адрес назывался линейным, и при этом фактически совпадал с физическим адресом ячейки памяти. Разумеется, с точки зрения развития операционных систем это был тупик. Должна быть, по крайней мере, возможность расширять память, и не просто расширять, а сделать все адресное пространство равноправным. Выход был найден с введением так называемого защищенного режима.
Гениальность подхода заключалась в том, что на первый взгляд ничего не изменилось. По-прежнему логический адрес формировался при помощи сегментных регистров и регистров, где хранилось смещение. Однако сегментные регистры хранили теперь не сегментный адрес, а так называемый селектор, часть которого (13 бит) представляла собой индекс в некоторой таблице, называемой дескрипторной. Индекс указывал на дескриптор, в котором хранилась полная информация о сегменте. Размер дескриптора был достаточен для адресации уже гораздо большего объема памяти.
Рис. 3.6.1. Схема преобразования логического адреса в линейный адрес.
На Рис. 3.6.1 схематически представлен алгоритм преобразования логического адреса в линейный адрес. Правда, за основу мы взяли уже 32-битный микропроцессор. Таблица дескрипторов или таблица базовых адресов могла быть двух типов: глобальная (GDT) и локальная (LDT). Тип таблицы определялся вторым битом содержимого сегментного регистра. На расположение глобальной таблицы и ее размер указывал регистр GDTR. Предполагалось, что содержимое этого регистра после его загрузки не должно меняться. В глобальной дескрипторной таблице должны храниться дескрипторы сегментов, занятых операционной системой. Адрес локальной таблицы дескрипторов хранился в регистре LDTR. Предполагалось, что локальных дескрипторных таблиц может быть несколько — одна для каждой запущенной задачи. Тем самым уже на уровне микропроцессора закладывалась поддержка многозадачности. Размер регистра GDTR составляет 48 бит. 32 бита - адрес глобальной таблицы, 16 бит - размер.
Кроме глобальной дескрипторной таблицы, предусматривалась еще одна общесистемная таблица - дескрипторная таблица прерываний (IDT). Она содержит дескрипторы специальных системных объектов, которые называются шлюзами и определяют точки входа процедур обработки прерываний и особых случаев. Положение дескрипторной таблицы прерываний определяется содержимым регистра IDTR, структура которого аналогична регистру GDTR.
Размер регистра LDTR составляет всего 10 байт50. Первые 2 байта адресуют локальную дескрипторную таблицу не напрямую, а посредством глобальной дескрип-торной таблицы, т.е. играют роль селектора для каждой вновь создаваемой задачи. Т.о. в глобальную дескрипторную таблицу должен быть добавлен элемент, определяющий сегмент, где будет храниться локальная дескрипторная таблица данной задачи. Переключение же между задачами может происходить всего лишь сменой содержимого регистра LDTR. Отсюда, кстати, вытекает то, что, если задача одна собирается работать в защищенном режиме, ей незачем использовать локальные дескрипторные таблицы и регистр LDTR.
Дескриптор сегмента содержал, в частности, поле доступа, которое определяло тип индексируемого сегмента (сегмент кода, сегмент данных, системный сегмент и т.д.). Здесь же можно, например, указать, что данный сегмент доступен только для чтения. Учитывалась также возможность, что сегмент может отсутствовать в памяти, т.е. временно находиться на диске. Тем самым закладывалась возможность виртуальной памяти.
Подытожим, что же давал нам защищенный режим.
- Возможность для каждой задачи иметь свою систему сегментов. В микропроцессоре закладывалась возможность быстрого переключения между задачами. Кроме того, предполагалось, что в системе будут существовать сегменты, принадлежащие операционной системе.
- Предполагалась, что сегменты могут быть защищены от записи.
- В поле доступа можно также указать уровень доступа. Всего возможно четыре уровня доступа. Смысл уровня доступа заключался в том, что задача не может получить доступ к сегменту, у которого уровень доступа выше, чем у данной задачи.
- Наконец, в данной схеме была сразу заложена возможность виртуальной памяти, т.е. памяти, формируемой с учетом возможности того, что сегмент может временно храниться на диске. С учетом такой возможности логическое адресное пространство может составлять весьма внушительные размеры.
Обратимся опять к Рис. 3.6.1. Из схемы видно, что результатом преобразования является линейный адрес. Но если для микропроцессора 80286 линейный адрес можно отождествить с физическим адресом, для микропроцессора 80386 это уже не так.
Начиная с микропроцессора 80386 появился еще один механизм преобразования адресов - это страничная адресация. Чтобы механизм страничной адресации заработал, старший бит системного регистра CR0 должен быть равен 1.
Обратимся к Рис. 3.6.2. Линейный адрес, получаемый путем дескрипторного преобразования, делится на три части. Старшие 10 бит адреса используются как индекс в таблице, которая называется каталог таблиц страниц. Расположение каталога страниц определяется содержимым регистра CR3. Каталог состоит из дескрипторов. Максимальное количество дескрипторов 1024. Самих же каталогов может быть бесчисленное множество, но в данный момент работает каталог, на который указывает регистр CR3.
Рис. 3.6.2. Преобразование линейного адреса в физический адрес.
Средние 10 бит линейного адреса предназначены для индексации таблицы страниц, которая содержит 1024 дескриптора страниц, которые, в свою очередь, определяют физический адрес страниц. Размер страницы составляет 4 Кб. Легко сосчитать, какое адресное пространство может быть охвачено одним каталогом таблиц страниц. Это составляет 1024*1024*1024*4 байт, т.е. порядка четырех гигабайт.
Младшие 12 бит определяют смещение внутри страницы. Как легко заметить, это как раз составляет 4 Кб (4095 байта)51. Конечно, читатель уже догадался, что для каждого процесса должен существовать свой каталог таблиц страниц. Переключение же между процессами можно осуществлять посредством изменения содержимого регистра CR3. Однако это не совсем рационально, так как требует большого объема памяти. В реальной ситуации для переключения между процессами производится изменение каталога таблиц страниц.
Обратимся теперь к структуре дескрипторов страниц (дескриптор таблицы страниц имеет ту же самую структуру).
- Биты 12-31 - адрес страницы, который в дальнейшем складывается со смещением, предварительно сдвигаясь на двенадцать бит.
- Биты 9-11 - для использования операционной системой.
- Биты 7-8 - зарезервированы и должны быть равны нулю.
- Бит 6 - устанавливается, если была осуществлена запись в каталог или страницу.
- Бит 5 - устанавливается перед чтением и записью на страницу.
- Бит 4 - запрещение кэширования.
- Бит 3 - бит сквозной записи.
- Бит 2 - если значение этого бита равно 0, то страница относится к супервизору, если 1, то страница относится к рабочему процессу. Этим устанавливается два уровня доступа.
- Бит 1 - если бит установлен, то запись на страницу разрешена.
- Бит 0. Если бит установлен, то страница присутствует в памяти. Страницы, содержащие данные сбрасываются на диск и считываются, когда происходит обращение к ним. Страницы, содержащие код, на диск не сбрасываются, но могут подкачиваться из соответствующих модулей на диске. Поэтому память, занятая этими страницами, также может рационально использоваться.
47 Я имею в виду и микропроцессоры совместимые с Intel, выпускаемые другими фирмами.
48 В узком смысле слова индексными регистрами называются DI и SI.
49 Когда-то казалось, что один мегабайт памяти это много.
50 В старых моделях регистр содержал всего 2 байта.
51 Размер страницы в операционной системе Windows NT может
отличаться от 4 Кб, что, впрочем, почти никогда не сказывается на программировании.
II
Адресное пространство процесса. В предыдущем разделе мы говорили о страничной и сегментной адресации. Как же эти две адресации уживаются в Windows? Оказывается, все очень просто. В сегментные регистры загружаются селекторы, базовые адреса которых равны нулю, а размер сегмента составляет 4 гигабайта. После этого о существовании сегментов и селекторов можно забыть, хотя для микропроцессора этот механизм по-прежнему работает. Основным же механизмом формирования адреса становятся страничные преобразования. Такая модель памяти и называется плоской (FLAT). Логическая адресация в такой модели определяется всего одним 32-битным смещением. До сих пор все наши программы писались именно в плоской модели памяти. При этом мы представляли, что вся область памяти, адресуемая 32-битным адресом, находится в нашем распоряжении. Разумеется, мы были правы, только адрес этот является логическим адресом, который, в свою очередь, подвергается страничному преобразованию, а вот в какую физическую ячейку памяти он попадает, ответить уже весьма затруднительно.
На Рис. 3.6.3 представлено логическое адресное пространство процесса. Особо обратите внимание на разделенные области памяти. Что это значит? А значит это только одно: эти области памяти проецируются на одно и то же физическое пространство.
Самая нижняя область адресного пространства отводится под образ операционной системы MS DOS, которая как видите, еще вполне зримо присутствует в операционной системе Windows. Кроме того, эта область используется для выделения динамической памяти 16-битным процессам.
Следующая область адресного пространства, между 4 Мб и 2 Гб, является адресным пространством процесса. Процесс занимает эту область пространства под код, данные, а также специфичные для него динамические библиотеки. Это не разделяемая область. Есть, однако, исключения, с которым мы уже встречались. Можно определить отдельные разделяемые секции. Это значит, что некоторые страницы из этого логического пространства будут отображаться в одну физическую область у разных процессов. Следующая область содержит в себе файлы, отображаемые в память, системные динамические библиотеки, а также динамическую память для 16-битных приложений. Последняя часть адресного пространства отведена под системные компоненты. Удивительно, но в Windows 9х эта область не защищена от доступа обычных программ.
Рис. 3.6.3. Адресное пространство процесса. Области 1,3,4 являются разделяемыми.
III
Управление памятью. В этом разделе мы разберем несколько функций, позволяющих динамически выделять и удалять блоки памяти.
Начнем с функции GlobalAlloc. Другая функция, LocalAlloc, полностью эквивалентна первой и сохранена только для совместимости со старыми приложениями. Функция имеет два аргумента. Первым аргументом является флаг, о значении которого будем говорить ниже. Вторым аргументом является число необходимых байтов. Если функция выполнена успешно, то она возвращает адрес начала блока, который можно использовать в дальнейших операциях. Если же система не может выделить достаточно памяти, то функция возвращает 0.
Обычно значение флага принимают равным константе GMEM_FIXED, которая равна нулю. Это означает, что блок памяти неперемещаем. Неперемещаемость следует понимать в том смысле, что не будет меняться виртуальный адрес блока, тогда как физическая память, куда проецируется данный блок, может, разумеется, меняться системой. Комбинация данного флага с флагом GMEM_ZEROINIT приводит к автоматическому заполнению выделенного блока нулями, что часто бывает весьма удобно. Изменить размер выделенного блока можно при помощи функции GlobalReAlloc. Первым аргументом данной функции является указатель на изменяемый блок, второй аргумент - размер нового блока, третий аргумент - флаг. Заметим, что данная функция может изменить свойства блока памяти, т.е., например, сделать его перемещаемым.
Обратимся теперь снова к флагам функции GlobalAlloc. Дело в том, что если ваша программа интенсивно работает с памятью, т.е. многократно выделяет и освобождает память, память может оказаться фрагментированной. Действительно - Вы же запрещаете перемещать блоки. В этом случае можно использовать флаг GMEM_MOVEABLE. Выделив блок, Вы можете в любой момент зафиксировать его при помощи функции GlobalLock, после этого спокойно работая с ним. С помощью функции GlobalUnlock можно в любой момент снять фиксацию, т.е. разрешить системе упорядочивать блоки. Надо иметь в виду, что при использовании флага GMEM_MOVEABLE возвращается не адрес, а дескриптор. Но как раз аргументом функции GlobalLock и является дескриптор. Сама же функция GlobalLock возвращает адрес.
Возможен и еще более экзотический подход с использованием флага GMEM_DISCARDABLE. Этот флаг используется совместно с GMEM_MOVEABLE. В этом случае блок может быть удален из памяти системой, если только вы его предварительно не зафиксировали. Если блок был удален системой, то функция GlobalLock возвратит 0, и Вам придется снова выделять блок и загружать, если необходимо, данные.
Для удаления блока памяти используется функция GlobalFree. Причем в случае выделения фиксированного блока памяти, аргументом функции является адрес блока памяти, а в случае перемещаемого блока памяти - дескриптор. Для освобождения удаляемого блока памяти используйте функцию GlobalDiscard.
Особо хочу отметить функцию GlobalMemoryStatus, с помощью которой можно определить количество свободной памяти. Единственным параметром данной функции является указатель на структуру, содержащую информацию о памяти. Вот эта структура.
MEM STRUC dwLength DW ? dwMemoryLoad DW ? dwTotalPhys DW ? dwAvailPhys DW ? dwTotalPageFile DW ? dwAvailPageFile DW ? dwTotalVirtual DW ? dwAvailVirtual DW ? MEM ENDS
dwLength - размер структуры в байтах.
dwMemoryLoad - процент использованной памяти.
dwTotalPhys - полный объем физической памяти в байтах.
dwAvailPhys - объем доступной физической памяти в байтах.
dwTotalPageFile - количество сохраненных байт физической памяти на диске.
dwAvailPageFile - количество доступных байт памяти, сохраненных на диске.
dwTotalVirtual - объем виртуальной памяти.
dwAvailVirtual - объем доступной виртуальной памяти.
Ниже на Рис. 3.6.4 показано простейшее применение функции GlobalAlloc.
; файл MEM.ASM .386P ; плоская модель .MODEL FLAT, stdcall ; константы ; для вывода в консоль STD_OUTPUT_HANDLE equ -11 GENERIC_READ equ 80000000h OPEN_EXISTING equ 3 IFDEF MASM ; MASM ; прототипы внешних процедур EXTERN GlobalFree@4:NEAR EXTERN GlobalAlloc@8:NEAR EXTERN GetFileSize@8:NEAR EXTERN CloseHandle@4:NEAR EXTERN CreateFileA@28:NEAR EXTERN ReadFile@20:NEAR EXTERN GetStdHandle@4:NEAR EXTERN WriteConsoleA@20:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetCommandLineA@0:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; TASM LOCALS ; прототипы внешних процедур EXTERN GlobalFree:NEAR EXTERN GlobalAlloc:NEAR EXTERN GetFileSize:NEAR EXTERN CloseHandle:NEAR EXTERN CreateFileA:NEAR EXTERN ReadFile:NEAR EXTERN GetStdHandle:NEAR EXTERN WriteConsoleA:NEAR EXTERN ExitProcess:NEAR EXTERN GetCommandLineA:NEAR GlobalFree@4 = GlobalFree GlobalAlloc@8 = GlobalAlloc GetFileSize@8 = GetFileSize CloseHandle@4 = CloseHandle CreateFileA@28 = CreateFileA ReadFile@20 = ReadFile GetStdHandle@4 = GetStdHandle WriteConsoleA@20 = WriteConsoleA ExitProcess@4 = ExitProcess GetCommandLineA@0 = GetCommandLineA ; директивы компоновщику для подключения библиотек includelib c:\tasm32\lib\import32.lib ENDIF ;------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' LENS DWORD ? HANDL DWORD ? ; дескриптор консоли HF DWORD ? ; дескриптор файла SIZEH DWORD ? ; старшая часть длины файла SIZEL DWORD ? ; младшая часть длины файла GH DWORD ? ; указатель на блок памяти NUMB DWORD ? BUF DB 10 DUP (0) _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить HANDLE вывода PUSH STD_OUTPUT_HANDLE CALL GetStdHandle@4 MOV HANDL,EAX ; получить количество параметров CALL NUMPAR CMP EAX,2 JB _EXIT ;------------------------------------------------- ; получить параметр номером EDI MOV EDI,2 LEA EBX,BUF CALL GETPAR ; теперь работаем с файлом ; открыть только для чтения PUSH 0 PUSH 0 PUSH OPEN_EXISTING PUSH 0 PUSH 0 PUSH GENERIC_READ PUSH OFFSET BUF CALL CreateFileA@28 CMP EAX,-1 JE _EXIT ; запомнить дескриптор файла MOV HF,EAX ; определить размер файла PUSH OFFSET SIZEH PUSH EAX CALL GetFileSize@8 ; запомнить размер, предполагаем, что размер не превосходит 4 Гб MOV SIZEL,EAX ; запросить память для считывания туда файла PUSH EAX PUSH 0 CALL GlobalAlloc@8 CMP EAX,0 JE _CLOSE ; запомнить адрес выделенного блока MOV GH,EAX ; читать файл в выделенную память PUSH 0 PUSH OFFSET NUMB PUSH SIZEL PUSH GH PUSH HF CALL ReadFile@20 CMP EAX,0 JE _FREE ; вывести прочитанное PUSH 0 PUSH OFFSET LENS PUSH SIZEL PUSH GH PUSH HANDL CALL WriteConsoleA@20 _FREE: ; освободить память PUSH GH CALL GlobalFree@4 ; закрыть файлы _CLOSE: PUSH HF CALL CloseHandle@4 _EXIT: ; конец работы программы PUSH 0 CALL ExitProcess@4 ;--------------------------------------------- ; область процедур ; процедура определения количества параметров в строке ; определить количество параметров (->EAX) NUMPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак @@L1: CMP BYTE PTR [ESI],0 JE @@L4 CMP BYTE PTR [ESI],32 JE @@L3 ADD ECX,EDX ; номер параметра MOV EDX,0 JMP @@L2 @@L3: OR EDX,1 @@L2: INC ESI JMP @@L1 @@L4: MOV EAX,ECX RET NUMPAR ENDP ; получить параметр из командной строки ; EBX - указывает на буфер, куда будет помещен параметр ; в буфер помещается строка с нулем на конце ; EDI - номер параметра GETPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ; указатель на строку XOR ECX,ECX ; счетчик MOV EDX,1 ; признак @@L1: CMP BYTE PTR [ESI],0 JE @@L4 CMP BYTE PTR [ESI],32 JE @@L3 ADD ECX,EDX ; номер параметра MOV EDX,0 JMP @@L2 @@L3: OR EDX,1 @@L2: CMP ECX,EDI JNE @@L5 MOV AL,BYTE PTR [ESI] MOV BYTE PTR [EBX],AL INC EBX @@L5: INC ESI JMP @@L1 @@L4: MOV BYTE PTR [EBX],0 RET GETPAR ENDP _TEXT ENDS END START
Puc. 3.6.4. Пример программы с выделением динамической памяти.
Трансляция программы на Рис. 3.6.4.
MASM32:
ML /С /coff /DMASM MEM.ASM LINK /SUBSYSTEM:CONSOLE MEM.OBJTASM32:
TASM32 /ml MEM.ASM TLINK32 -ap MEM.OBJ
Операционная система Windows предоставляет также группу функций, осуществляющих управление виртуальной памятью. Основной функцией этой группы является функция VirtualAlloc. Вот параметры этой функции:
- 1-й параметр. Адрес блока памяти для резервирования или передачи ему физической памяти.
- 2-й параметр. Размер блока.
- 3-й параметр. Может быть равен MEM_RESERVE - для резервирования блока, или MEM_COMMIT - для резервирования и передачи ему физической памяти.
- 4-й параметр. Определяет уровень защиты блока. Он может быть, например, равен PAGE_READONLY или PAGE_READWRITE, или другой константе, определенной в документации Windows.
- Возвращает функция виртуальный адрес блока памяти.
Суть данной функции заключается в том, что Вы можете зарезервировать блок памяти, который не спроецирован на физическую память, а затем сделать так, чтобы этот блок (или часть его) был спроецирован на физическую память. После чего этот блок памяти можно уже использовать.
Другая функция, VirtualFree, может освобождать блоки, задействованные функцией VirtualAlloc. Первым параметром этой функции является адрес блока. Вторым параметром функции является размер освобождаемого блока. Третий параметр функции может принимать значение МЕМ_DЕСОММIТ либо значение MEM_RELEASE. В первом случае блок (или его часть) перестает быть отображаемым. Во втором случае весь блок перестает быть зарезервированным. При этом значении второй параметр обязательно должен быть равен нулю.
Фильтры (HOOKS). Мы рассмотрим весьма действенное средство, чаще всего используемое для отладки программ. Средство это называют фильтрами или ловушками52. Смысл его заключается в том, что Вы при желании можете отслеживать сообщения как в рамках одного приложения, так и в рамках целой системы. В этой связи фильтры делят на глобальные (в рамках всей системы) и локальные (в рамках данного процесса). Работая с фильтрами, надо иметь в виду, что они могут существенным образом затормозить работу всей системы. Особенно это касается глобальных фильтров. С точки зрения программирования мы просто определяем функцию, которая вызывается системой при возникновении некоторого события. Можно также говорить о сообщении, приходящем на функцию фильтра.
Рассмотрим некоторые средства для работы с фильтрами. Ниже перечислены основные типы фильтров или сообщения.
- WH_CALLWNDPROC - фильтр срабатывает, когда вызывается функция SendMessage.
- WH_CALLWNDPROCRET - фильтр срабатывает, когда функция SendMessage возвращает управление.
- WH_CBT - сообщение приходит, когда что-то происходит с окном.
- WH_DEBUG - данное сообщение посылается перед тем, как послать сообщение какому-либо другому фильтру.
- WH_GETMESSAGE - данный фильтр срабатывает, когда функция GetMessage принимает какое-либо сообщение из очереди.
- WH_JOURNALRECORD - данное сообщение приходит на процедуру фильтра, когда система удаляет из очереди какое-либо сообщение.
- WH_JOURNALPLAYBACK - вызывается за предыдущим вызовом (WH_JOURNALRECORD).
- WH_KEYBOARD - сообщение приходит, когда происходят клавиатурные события.
- WH_MOUSE - аналогично предыдущему, но относится к событиям с мышью.
- WH_MSGFILTER - вызывается в случае событий ввода, которые произошли с диалоговым окном, меню, полосой прокрутки, но до того, как эти события были обработаны в пределах данного процесса.
- WH_SHELL - данный фильтр срабатывает, когда что-то происходит с Windows-оболочкой.
- WH_SYSMSGFILTER - аналогично сообщению WH_MSGFILTER, но относится ко всей системе.
Фильтр устанавливается при помощи функции SetWindowsHookEx. Рассмотрим параметры этой функции.
- 1-й параметр. Тип фильтра, из тех, что мы перечислили выше.
- 2-й параметр. Адрес процедуры фильтра. Если Вы создаете для всей системы, то эта процедура должна находиться в динамической библиотеке. Исключение составляют лишь два типа фильтра: WH_JOURNALRECORD и WH_JOURNALPLAYBACK.
- 3-й параметр. Дескриптор динамической библиотеки, если фильтр предназначен для всей системы. Исключение составляют два, уже упомянутых типа фильтра.
- 4-й параметр. Идентификатор потока, если Вы хотите следить за одним из потоков. Если значение этого параметра равно нулю, то создается фильтр для всей системы. Вообще говоря, поток может относиться и к Вашему, и к "чужому" процессу.
- Функция SetWindowsHookEx возвращает дескриптор фильтра.
Функция фильтра получает три параметра. Первый параметр определяет произошедшее событие в зависимости от типа фильтра. Два последующих параметра расшифровывают это событие. Поскольку для каждого типа фильтра может быть несколько событий, я не буду их перечислять. Их можно найти в справочном руководстве.
По окончании работы фильтр обязательно должен быть закрыт с помощью функции UnhookWindowsHookEx, единственным параметром которой является дескриптор фильтра.
Фильтр, вообще говоря, есть лишь некоторое звено в вызываемой системой цепочке, поэтому следует из своей процедуры фильтра вызвать функцию CallNextHookEx, которая передаст нужную информацию по цепочке. Параметры этой функции:
- 1-й параметр. Дескриптор Вашего фильтра.
- 2,3,4-й параметры в точности соответствуют трем параметрам, переданным Вашей процедуре фильтра.
Ниже на Рис. 3.6.5 приводится пример простого фильтра, который отлавливает все произошедшие в системе нажатия клавиши "пробел". Обратите внимание, что поскольку устанавливаемый нами фильтр является глобальным, мы помещаем процедуру фильтра в динамическую библиотеку.
// файл dial.rc для программы DLLEX.ASM // определение констант #define WS_SYSMENU 0x00080000L #define WS_MINIMIZEBOX 0x00020000L #define WS_MAXIMIZEBOX 0x00010000L // определение диалогового окна DIAL1 DIALOG 0, 0, 240, 120 STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX CAPTION "Пример программы с фильтром" FONT 8, "Arial" { } ; основной модуль DLLEX.ASM, ; устанавливающий фильтр в динамической библиотеке .386P ; плоская модель .MODEL FLAT, stdcall ; константы ; сообщение приходит при закрытии окна WM_CLOSE equ 10h WM_INITDIALOG equ 110h WH_KEYBOARD equ 2 ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ; прототипы внешних процедур IFDEF MASM ; MASM EXTERN UnhookWindowsHookEx@4:NEAR EXTERN SetWindowsHookExA@16:NEAR EXTERN EndDialog@8:NEAR EXTERN DialogBoxParamA@20:NEAR EXTERN GetProcAddress@8:NEAR EXTERN LoadLibraryA@4:NEAR EXTERN FreeLibrary@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN MessageBoxA@16:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE EXTERN UnhookWindowsHookEx:NEAR EXTERN SetWindowsHookExA:NEAER EXTERN EndDialog:NEAR EXTERN DialogBoxParamA:NEAR EXTERN GetProcAddress:NEAR EXTERN LoadLibraryA:NEAR EXTERN FreeLibrary:NEAR EXTERN ExitProcess:NEAR EXTERN MessageBoxA:NEAR UnhookWindowsHookEx@4 = UnhookWindowsHookEx SetWindowsHookExA@16 = SetWindowsHookExA EndDialog@8 = EndDialog DialogBoxParamA@20 = DialogBoxParamA GetProcAddress@8 = GetProcAddress LoadLibraryA@4 = LoadLibraryA FreeLibrary@4 = FreeLibrary ExitProcess@4 = ExitProcess MessageBoxA@16 = MessageBoxA ; директивы компоновщику для подключения библиотек includelib c:\tasm32\lib\import32.lib ENDIF ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' MSG MSGSTRUCT <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 LIBR DB 'DLL2.DLL',0 HLIB DD ? APROC DD ? HH DD ? ATOH DD ? IFDEF MASM NAMEPROC DB '_HOOK@0',0 NAMEPROC1 DB '_TOH@0',0 ELSE NAMEPROC1 DB '_TOH',0 NAMEPROC DB 'HOOK',0 ENDIF _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; загрузить библиотеку PUSH OFFSET LIBR CALL LoadLibraryA@4 CMP EAX,0 JE _EXIT MOV HLIB,EAX ; получить адрес процедуры-фильтра PUSH OFFSET NAMEPROC PUSH HLIB CALL GetProcAddress@8 CMP EAX,0 JE _EXIT MOV APROC,EAX ; получить адрес вспомогательной процедуры PUSH OFFSET NAMEPROC1 PUSH HLIB CALL GetProcAddress@8 CMP EAX,0 JE _EXIT MOV ATOH,EAX ; здесь установить HOOK PUSH 0 PUSH HLIB PUSH APROC PUSH WH_KEYBOARD CALL SetWindowsHookExA@16 MOV HH,EAX ; запомним и передадим в библиотеку MOV EAX,ATOH PUSH HH CALL ATOH ; открыть диалоговое окно PUSH 0 PUSH OFFSET WNDPROC PUSH 0 PUSH OFFSET PA PUSH [HINST] CALL DialogBoxParamA@20 ; удалить HOOK PUSH HH CALL UnhookWindowsHookEx@4 ; закрыть библиотеку ; библиотека автоматически закрывается также ; при выходе из программы PUSH OFFSET NAMEPROC PUSH HLIB CALL FreeLibrary@4 ; выход _EXIT: PUSH 0 CALL ExitProcess@4 ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10Н] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI ;------------------ CMP DWORD PTR [EBP+0CH],WM_CLOSE JNE L1 PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog@8 JMP FINISH L1: CMP DWORD PTR [EBP+0CH],WM_INITDIALOG JNE FINISH FINISH: POP EDI POP ESI POP EBX POP EBP MOV EAX,0 RET 16 WNDPROC ENDP _TEXT ENDS END START ; динамическая библиотека DLL2.ASM ; содержащая процедуру-фильтр .386P ; плоская модель IFDEF MASM .MODEL FLAT, stdcall ELSE .MODEL FLAT ENDIF PUBLIC HOOK, TOH ; константы ; сообщения, приходящие при открытии ; динамической библиотеки DLL_PROCESS_DETACH equ 0 DLL_PROCESS_ATTACH equ 1 DLL_THREAD_ATTACH equ 2 DLL_THREAD_DETACH equ 3 IFDEF MASM ; MASM ; прототипы внешних процедур EXTERN CallNextHookEx@16:NEAR EXTERN MessageBoxA@16:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; TASM EXTERN CallNextHookEx:NEAR EXTERN MessageBoxA:NEAR CallNextHookEx@16 = CallNextHookEx MessageBoxA@16 = MessageBoxA includelib c:\tasm32\lib\import32.lib ENDIF ;-------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' HDL DD ? HHOOK DD ? CAP DB "Сообщение фильтра",0 MES DB "Нажат пробел",0 _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; [EBP+10H] ; резервный параметр ; [EBP+0CH] ; причина вызова ; [EBP+8] ; идентификатор DLL-модуля DLLENTRY: MOV EAX,DWORD PTR [EBP+0CH] CMP EAX,0 JNE D1 ; закрытие библиотеки JMP _ЕХIТ D1: CMP EAX,1 JNE _EXIT ; открытие библиотеки ; запомнить идентификатор динамической библиотеки MOV EDX,DWORD PTR [EBP+08H] MOV HDL,EDX _ЕХIТ: MOV EAX,1 RET 12 ;---------------- TOH PROC EXPORT PUSH EBP MOV EBP,ESP MOV EAX,DWORD PTR [EBP+08H] MOV HHOOK,EAX POP EBP RET TOH ENDP ; процедура фильтра HOOK PROC EXPORT PUSH EBP MOV EBP,ESP ; отправить сообщение по цепочке PUSH DWORD PTR [EBP+010H] PUSH DWORD PTR [EBP+0CH] PUSH DWORD PTR [EBP+08H] PUSH HHOOK CALL CallNextHookEx@16 ; проверить, не нажат ли пробел CMP DWORD PTR [EBP+0CH],32 JNE _EX ; нажат - выводим сообщение PUSH 0 ; МВ_ОК PUSH OFFSET CAP PUSH OFFSET MES PUSH 0 ; в окне экрана CALL MessageBoxA@16 _EX: POP EBP RET HOOK ENDP _TEXT ENDS END DLLENTRY
Puc. 3.6.5. Простой пример построения глобального фильтра.
Трансляция программ на Рис. 3.6.5.
MASM32:
Динамическая библиотека
ml /c /coff /DMASM dll2.asm link /subsystem:windows /DLL dll2.objОсновная программа.
ml /c /coff /DMASM dllex.asm rc dial.rc link /subsystem:windows dllex.obj dial.resTASM32:
Динамическая библиотека
TASM32 /ml dll2.asm tlink32 /subsystem:windows -aa -Tpd dll2.objОсновная программа.
TASM32 /ml dllex.asm brcc32 dial.rc tlink32 -aa dllex.obj,,,,,dial.res
При разборе программ на Рис. 3.6.5 обратите внимание на роль, которую играет процедура TOH. Заметьте также, что второй и третий параметр процедуры фильтра в точности соответствует значению аналогичных параметров сообщения WM_KEYDOWN. Кстати, надеюсь, Вы понимаете, почему при нажатии клавиши пробел появляются два сообщения - по одному на нажатие и отпускание.
52 Hook можно перевести как ловушка, да и по смыслу это ближе к понятию ловушка.