bogdan_017,
Сообщения, которые Windows посылает приложению, на самом деле получает процедура окна, определенная для данного класса. Чаще всего она представляет собой конструкцию «выбора», которую в языке C/C++ называют SWITCH, в Pascal/Delphi — CASE, в Basic — SELECT CASE, а в абстрактном языке программирования — мультивлетвением. Задача конструкции CASE заключается в проверке на равенство значения переменной var элементу из списка.
Код
CASE <var> OF
<case A> (выполнить, если var = A)
<case B> (выполнить, если var = B)
. . .
<case N> (выполнить, если var = N)
<default> (выполнить, если нет соответствия)
END_CASE
Данная конструкция направляет сообщения функциям обработчикам. На ассемблере конструкция CASE может быть реализована, по крайней мере, семью способами:
- тупо, через повторяющиеся команды cmp и je.
- через макрос
- через высокоуровневые директивы .IF .ELSEIF .ELSE
- через косвенные переходы или косвенный вызов процедур
- через команду SCASD
- через последовательное приближение
- через имитацию команды XLAT
Рассмотрим подпрограмму, обрабатывающую только четыре сообщения: WM_CREATE=1, WM_DESTROY=2, WM_PAINT=0Fh и WM_TIMER=113h. Пусть сообщение WM_XX обрабатывается по адресу @@WM_XX. Учтем так же, что частота появлений различных сообщений неодинакова (спасибо Y_Mur), поэтому проверяем сперва наиболее часто посылаемые WM_PAINT и WM_TIMER, затем однократно используемую WM_CREATE, и наконец последнее сообщение в жизни нашего приложения WM_DESTROY.
Итак,
первый способ очевиден.
Assembler |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| cmp dword ptr Msg,WM_TIMER
je @@WM_TIMER
cmp dword ptr Msg,WM_PAINT
je @@WM_PAINT
cmp dword ptr Msg,WM_CREATE
je @@WM_CREATE
cmp dword ptr Msg,WM_DESTROY
je @@WM_DESTROY
leave
jmp DefWindowProcA; обработка по умолчанию
@@WM_DESTROY:
. . .
@@WM_CREATE:
. . .
@@WM_PAINT:
. . .
@@WM_TIMER:
. . . |
|
один из вариантов первого способа — заменить команду cmp на команду sub
Assembler |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| mov eax,Msg
dec eax
jz @@WM_CREATE ;WM_CREATE=1
dec eax
jz @@WM_DESTROY ;WM_CREATE+1=WM_DESTROY=2
sub eax,0Dh
jz @@WM_PAINT ;WM_DESTROY+0Dh=WM_PAINT=0Fh
sub eax,104h
jz @@WM_TIMER ;WM_PAINT+104h=WM_TIMER=113h
mov esp,ebp ;все сообщения, не обрабатываемые в функции
pop ebp ;WndProc, направляются на обработку по умолчанию
jmp DefWindowProcA
@@WM_DESTROY:
. . . |
|
Способ второй. Используем макрос с IRP-блоком (Вы, конечно, можете, если захотите, написать другой макрос). Блоки повторения IRP имеют следующий вид:
Код
IRP p,<v1,…,vn>
<тело блока>
ENDM
Запись в уголках <v
1,…,v
n> это явно указываемые фактические параметры, p — некоторое имя, оно играет роль формального (фиктивного) параметра и используется в предложениях тела блока. Фактические параметры v
i перечисляются через запятую, а вся их совокупность обязательно заключается в угловые скобки. Встретив IRP-блок, макрогенератор заменит блок на
n копий тела блока (по одной копии на фактический параметр), причем в
i-той копии все вхождения имени p заменятся на v
i. Знак
& указывает границу формального параметра, выделяет его из окружающего текста, но в окончательный текст программы не попадает. Если в теле блока повторения или макроса имеются комментарии (в конце каких-то предложений или как самостоятельные предложения), то они переносятся во все копии блока. Если комментарий полезен при описании самого блока повторения, но совершенно не нужен в его копиях — тогда комментарий начинают с двух точек с запятой — такие комментарии не копируются.
Assembler |
1
2
3
4
5
6
7
8
| IRP CASE,<WM_TIMER,WM_PAINT,WM_CREATE,WM_DESTROY>;;последовательность вариантов
cmp dword ptr Msg, CASE;;вариант найден?
je @@&CASE
ENDM;;закончить макроописание
leave ; обработка по умолчанию
jmp DefWindowProcA
@@WM_DESTROY:
. . . |
|
Способ третий. Пример использования директив ассемблера .IF .ELSEIF .ELSE .ENDIF:
Assembler |
1
2
3
4
5
6
7
8
9
10
11
12
| .IF (Msg==WM_TIMER)
. . . ;здесь обрабатывается сообщение WM_TIMER
.ELSEIF (Msg==WM_PAINT)
. . . ;здесь обрабатывается сообщение WM_PAINT
.ELSEIF (Msg==WM_CREATE)
. . . ;здесь обрабатывается сообщение WM_CREATE
.ELSEIF (Msg==WM_DESTROY)
. . . ; здесь обрабатывается сообщение WM_DESTROY
.ELSE
leave
jmp DefWindowProcA; обработка по умолчанию
.ENDIF |
|
И вариант с макросом, и вариант с .IF .ELSEIF .ELSE .ENDIF все равно будут оттранслированы в повторяющиеся команды CMP и JE
Способ четвертый. Налицо в программе очень много повторяющихся почти одинаковых фрагментов. Нельзя ли написать программу как-нибудь иначе? Чем различаются эти фрагменты программы? Разными операндами в команде CMP, разными адресами переходов или разными вызываемыми процедурами. Уберем операнды команды CMP в какой-нибудь регистр и используем для разных адресов переходов косвенный переход, а для разных вызываемых процедур — косвенный вызов процедуры.
Assembler |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| ;Таблица сообщений, которые обрабатывает процедура окна
MsgTable dd WM_PAINT, @@WM_PAINT, WM_TIMER, @@WM_TIMER, WM_CREATE, @@WM_CREATE, WM_DESTROY, @@WM_DESTROY, 0; Конец списка обязательно 0
. . .
;Вызов соответствующего обработчика
mov esi,offset MsgTable ;устанавливаем в ESI адрес таблицы-диспетчера
a1: mov eax,[esi] ;устанавливаем в eax тип сообщения
test eax,eax ;это конец таблицы? Если да, то данное сообщение не
jz @@default; обрабатывается приложением - передаем управление Windows
cmp eax,Msg
jne a2 ;для данного сообщения не найден обработчик
add esi,4 ;устанавливаем в esi адрес обработки сообщения
jmp [esi] ;передаем управление обработчику сообщения
a2: add esi,8 ;перейдем к следующему сообщению
jmp short a1
@@default: leave ;Обработчик не найден, возвращается значение,
jmp DefWindowProcA ; которое дает стандартный обработчик
@@WM_DESTROY: |
|
Способ пятый. Используем команду SCASD с префиксом REPNE. Внесем в конец таблицы сообщений, которые обрабатывает процедура окна, адрес обработки по умолчанию, а в регистр ECX количество элементов в таблице сообщений минус один (компилятор подсчитает это число автоматически). Напомню, что команда SCASD сравнивает значение в регистре EAX с ячейкой памяти, локализуемой регистром EDI. Префикс REPNE не только заставляет циклически выполняться команду сканирования, пока ECX не станет равным 0, но и отслеживает состояние флага ZF. Как только содержимое в ячейке памяти совпадет со значением в регистре EAX, сканирование остановится. Регистр EDI будет указывать не на ячейку памяти, значение которой совпадает с регистром EAX, а на следующую ячейку. Сканирование также остановится, если содержимое регистра ECX станет равным 0. При этом в регистре EDI будет адрес метки @@default (адрес обработки по умолчанию)
Assembler |
1
2
3
4
5
6
7
8
9
10
11
12
13
| ;Таблица сообщений, которые обрабатывает процедура окна
MsgTable dd WM_PAINT, @@WM_PAINT, WM_TIMER, @@WM_TIMER, WM_CREATE, \
@@WM_CREATE, WM_DESTROY, @@WM_DESTROY, @@default
MsgCount = ($ - MSGTable - 4)/4
. . .
mov edi,offset MsgTable;устанавливаем в EDI адрес таблицы-диспетчера
mov eax,Msg
mov ecx,MsgCount;удвоенное количество обрабатываемых сообщений (4*2=8)
repne scasd ;ищем сообщение и если найдем — передадим
jmp [edi] ; управление обработчику сообщения
@@default: leave ; если обработчик не найден, вызывается
jmp DefWindowProcA ; обработка по умолчанию
@@WM_DESTROY: |
|
По величине это, по-моему, самый короткий код функции обработки сообщений. Кстати, читатель может спросить: «а что произойдет, если адрес обработчика сообщения совпадет с номером сообщения?» Этого не может произойти, так как максимальный номер стандартных сообщений равен 400h, а 32-разрядная операционная система Windows отводит каждой программе свое адресное пространство размером около двух гигабайт, начиная с адреса 100000h. За границы своего адресного пространства задача выйти не может, так же, как никакая другая задача не может работать с данным пространством - этим и определяется автономность программы.
Способ шестой — последовательное приближение. Пусть у нас уже не 4 обрабатываемых сообщения, а 100. Тогда при использовании с первого по пятый способ мы должны будем последовательно сравнить до 100 значений, до тех пор пока не найдем искомое. Увеличим скорость обработки сообщений, которые Windows посылает Вашему приложению. Рассортируем номера обрабатываемых нашей программой сообщений по возрастанию, теперь разделим номера сообщений на две равные группы — 50 сообщений с меньшими номерами в одной группе, 50 сообщений с большими номерами в другой. После определения, к какой из групп сообщений принадлежит вновь поступившее сообщение, нам потребуется уже не 100, а только 50+1 сравнение. Разобьем каждую из образовавшихся групп с номерами сообщений еще на две подгруппы, и потребуется уже 25+2 сравнений, вместо 100. Продолжаем делить подгруппы с номерами сообщений, и получаем 13+3 вместо 100, затем 7+4, далее 4+5, и, наконец, 2+6, больше разбивать подгруппы не имеет смысла (дальше было бы 1+7, а это одно и то же, что и 2+6). То есть, в общем случае, если 2
(n-1) < N <= 2
n, то определить N можно не более чем за n+1 приближение. Заметьте, увеличение скорости обработки будет сопровождаться разрастанием кода программы.
Assembler |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| mas dd WM_CREATE, WM_DESTROY... WM_PAINT ;массив сообщений
n = ($ - mas)/4
mas1 dd @@WM_CREATE, @@WM_DESTROY,... @@WM_PAINT;массив адресов
...
xor ecx,ecx ;в ecx индекс первого элемента
mov edx,n-1 ;в edx индекс последнего элемента
mov eax,Msg
cmp eax,WM_CREATE;сравниваем с минимальным элементом таблицы
jb short @@default;если меньше, то обработка по умолчанию
mov ebx,edx ;создаем индекс центрального элемента
a1: cmp ecx,edx ;проверка ecx>edx на окончание поиска
ja short @@default;проверены все элементы, обработка по умолчанию
shr ebx,1 ;индекс центрального элемента равен (edx+ecx)/2
cmp mas[ebx*4],eax ;сравниваем с искомым значением
jb short a2 ;mas[ebx*4]<Msg
je short a3 ;mas[ebx*4]=Msg
inc ebx ;учтем только что проверенное значение
mov ecx,ebx ;изменяем нижнюю границу поиска
add ebx,edx ;создаем индекс центрального элемента
jmp short a1 ;переходим к следующему элементу
a2: dec ebx ;учтем только что проверенное значение
mov edx,ebx ;изменяем верхнюю границу поиска
add ebx,ecx ;создаем индекс центрального элемента
jmp short a1 ;переходим к следующему элементу
@@default: leave ;искомое значение не найдено,
jmp DefWindowProc ;обработка по умолчанию
a3: jmp mas1[ebx*4] ;искомое значение найдено
@@WM_CREATE: ... |
|
А это еще один, более короткий вариант последовательного приближения. Но для него требуется, чтобы количество сообщений должно быть равным 2
n. Допустим, мы обрабатываем 16 сообщений,
Количество элементов 2
(n-1) < N <= 2
n
ebx (индекс элемента) 2
(n-1)
ecx (счетчик приближений) n+1
так как N = 16, то в соответствии с таблицей: n = 4, ebx = 8, ecx = 5
Assembler |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| mas dd WM_CREATE, WM_DESTROY, ... WM_PAINT;массив сообщений
mas1 dd @@WM_CREATE, @@WM_DESTROY,... @@WM_PAINT ;массив адресов
n equ 4
...
mov ecx,n+1 ;ecx=5
mov ebx,1
shl ebx,n-1 ;ebx=8 индекс центрального элемента
xor edx,edx ;нижняя граница поиска
mov eax,Msg
cmp eax,WM_CREATE;сравниваем с минимальным элементом таблицы
jb short @@default;если меньше, то обработка по умолчанию
a1: cmp mas[edx+ebx*4],eax ;сравниваем с искомым значением
ja short a2 ;mas[ebx*4]<Msg
je short a3 ;mas[ebx*4]=Msg
lea edx,[edx+ebx*4] ;изменяем нижнюю границу поиска
a2: shr ebx,1 ;изменяем индекс центрального элемента
loop a1 ;переходим к следующему элементу
@@default: leave ;искомое значение не найдено
jmp DefWindowProc ;обработка по умолчанию
a3: jmp mas1[edx+ebx*4] ;искомое значение найдено
@@WM_CREATE: ... |
|
Если Вы и дальше хотите экспериментировать с обработкой сообщений — попробуйте заменить команду cmp на команды sub, dec, test, or или and.
Способ седьмой — имитация команды XLAT. При программировании на ассемблере стремятся либо получить максимально компактный код, либо достичь максимального быстродействия, хотя иногда добиваются и того, и другого. Способы четыре и пять, реализуемые через косвенный переход или косвенный вызов, хотя и имеют наименьший размер кода, но по быстродействию остаются таким же, как и способы с прямым переходом. То есть, всегда возможен вариант, когда будут перебраны все варианты сообщений, перед тем как запустится обработка сообщения по умолчанию. Реализуем мультиветвление при помощи косвенного перехода по таблице. Для увеличения скорости просмотра очереди сообщений строим процедуру подобную команде XLAT (сама команда XLAT, к сожалению, дает возможность работать только с 256 вызовами), строим таблицу MsgTable, где номер элемента таблицы соответствует номеру сообщения. Сами табличные элементы представляют собой адреса (процедур), где эти сообщения обрабатываются. Чтобы не перечислять все 400h (1024) сообщений, можно определить номер максимального сообщения Max_Msg и если номер сообщения больше максимального, сразу отправлять на обработку сообщения по умолчанию (метка @@default). Сообщения, не обрабатываемые в процедуре WinProc, также имеют адрес обработки соответствующий адресу метки @@default. При этом время выполнения перехода всегда одинаково для всех вариантов мультиветвления.
Assembler |
1
2
3
4
| mov eax,Msg
cmp eax,Max_Msg
ja @@default;MsgTable - таблица с адресами обработки
jmp MsgTable[eax*4];каждый адрес обработки расположен через 4 байта |
|
Но у этого способа есть и оборотная сторона. К сожалению, большая скорость обработки сообщений будет компенсирована раздувшейся таблицей MsgTable.