Assembler для Windows


Глава 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.

Дескриптор сегмента содержал, в частности, поле доступа, которое определяло тип индексируемого сегмента (сегмент кода, сегмент данных, системный сегмент и т.д.). Здесь же можно, например, указать, что данный сегмент доступен только для чтения. Учитывалась также возможность, что сегмент может отсутствовать в памяти, т.е. временно находиться на диске. Тем самым закладывалась возможность виртуальной памяти.

Подытожим, что же давал нам защищенный режим.

  1. Возможность для каждой задачи иметь свою систему сегментов. В микропроцессоре закладывалась возможность быстрого переключения между задачами. Кроме того, предполагалось, что в системе будут существовать сегменты, принадлежащие операционной системе.
  2. Предполагалась, что сегменты могут быть защищены от записи.
  3. В поле доступа можно также указать уровень доступа. Всего возможно четыре уровня доступа. Смысл уровня доступа заключался в том, что задача не может получить доступ к сегменту, у которого уровень доступа выше, чем у данной задачи.
  4. Наконец, в данной схеме была сразу заложена возможность виртуальной памяти, т.е. памяти, формируемой с учетом возможности того, что сегмент может временно храниться на диске. С учетом такой возможности логическое адресное пространство может составлять весьма внушительные размеры.

Обратимся опять к Рис. 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.OBJ
TASM32:
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. В первом случае блок (или его часть) перестает быть отображаемым. Во втором случае весь блок перестает быть зарезервированным. При этом значении второй параметр обязательно должен быть равен нулю.

IV

Фильтры (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.res
TASM32:
Динамическая библиотека
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 можно перевести как ловушка, да и по смыслу это ближе к понятию ловушка.