Assembler для Windows

             

Глава 2. Основы программирования в операционной системе Windows

В данной главе я намерен рассмотреть два момента, которые крайне важны для начала программирования на ассемблере в среде Windows - это вызов системных функций (API-функций) и возможные структуры программ 15 для Windows. Я полагаю, что можно выделить три типа структуры программ, которые условно можно назвать как классическая16 структура, диалоговая (основное окно — диалоговое), консольная, или безоконная17, структура. В данной главе подробно описывается первая, классическая структура.

Итак, начнем с нескольких общих положений о программировании в Windows. Те, кто уже имеет опыт программирования в среде Windows, могут на этом не останавливаться.



  1. Программирование в Windows основывается на использовании функций API (Application Program Interface, т.е. интерфейс программного приложения). Их количество достигает двух тысяч. Ваша программа в значительной степени будет состоять из таких вызовов. Все взаимодействие с внешними устройствами и ресурсами операционной системы будет происходить посредством таких функций.
  2. Список функций API и их описание лучше всего брать из файла WIN32.HLP, который поставляется, например, с пакетом Borland C++.
  3. Главным элементом программы в среде Windows является окно. Для каждого окна определяется своя процедура18 обработки сообщений (см. ниже).
  4. Окно может содержать элементы управления: кнопки, списки, окна редактирования и др. Эти элементы, по сути, также являются окнами, но обладающими особыми свойствами. События, происходящие с этими элементами (и самим окном), приводят к приходу сообщений в процедуру окна.
  5. Операционная система Windows использует линейную модель памяти. Другими словами, всю память можно рассматривать как один сегмент. Для программиста на языке ассемблера это означает, что адрес любой ячейки памяти будет определяться содержимым одного 32-битного регистра, например EBX.
  6. Следствием пункта 5 является то, что мы фактически не ограничены в объеме данных, кода или стека (объеме локальных переменных). Выделение в тексте программы сегмента кода и сегмента данных является теперь простой формальностью, улучшающей читаемость программы.
  7. Операционная система Windows является многозадачной средой. Каждая задача имеет свое адресное пространство и свою очередь сообщений. Более того, даже в рамках одной программы может быть осуществлена многозадачность - любая процедура может быть запущена как самостоятельная задача.

Итак, после теоретических положений самое время перейти к программным примерам.


15 Не путать со структурой загружаемых модулей.

16 Классификация автора.

17 Как потом станет ясно, консольное приложение вполне может иметь диалоговое окно.

18 Исходя из терминологии, принятой в MS DOS, такую процедуру следует назвать "процедурой прерывания". Для Windows же принята другая терминология. Подобные процедуры, вызываемые самой системой, называются процедурами обратного вызова (CALLBACK).


I

Начнем с того, как можно вызвать функции API. Обратимся к файлу помощи и выберем любую функцию API, например, MessageBox:

int MessageBox ( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType );

Данная функция выводит на экран окно с сообщением и кнопкой (или кнопками) выхода. Смысл параметров: hWnd -дескриптор окна, в котором будет появляться окно-сообщение, lpText - текст, который будет появляться в окне, lpCaption - текст в заголовке окна, uType - тип окна, в частности можно определить количество кнопок выхода. Теперь о типах параметров. Все они в действительности 32-битные целые числа: HWND — 32-битное целое, LPCTSTR — 32-битный указатель на строку, UINT — 32-битное целое. По причине, о которой будет сказано ниже, к имени функций нам придется добавлять суффикс "А", кроме того, при использовании MASM необходимо также в конце имени добавить @16. Таким образом, вызов указанной функции будет выглядеть так: CALL MessageBoxA@16. А как же быть с параметрами? Их следует аккуратно поместить в стек. Запомните правило: СЛЕВА НАПРАВО — СНИЗУ ВВЕРХ. Итак, пусть дескриптор окна расположен по адресу HW, строки — по адресам STR1 и STR2, а тип окна-сообщения — это константа. Самый простой тип имеет значение 0 и называется МВ_ОК. Имеем следующее:

МВ_ОК equ 0
.
.
STR1 DB "Неверный ввод! ",0
STR2 DB "Сообщение об ошибке.",0
HW DWORD ?
.
.
PUSH МВ_ОК
PUSH OFFSET STR1
PUSH OFFSET STR2
PUSH HW
CALL MessageBoxA@16

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

Аналогичным образом в ассемблере легко воспроизвести те или иные Си-структуры. Рассмотрим, например, структуру, определяющую системное сообщение:

typedef struct tagMSG { // msg
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;

Это сообщение будет далее подробно прокомментировано в одном из примеров. На ассемблере эта структура будет иметь вид:

MSGSTRUCT STRUC
MSHWND DD ?
MSMESSAGE DD ?
MSWPARAM DD ?
MSLPARAM DD ?
MSTIME DD ?
MSPT DD ?
MSGSTRUCT ENDS

Как видите, на ассемблере все даже гораздо проще. Вообще трудно не отклониться от темы и еще раз не удивиться фирме Микрософт, все так запутавшей в отношении типов переменных.

Теперь обратимся к структуре всей программы. Как я уже говорил, в данной главе мы будем рассматривать классическую структуру программы под Windows. В такой программе имеется главное окно, а следовательно, и процедура главного окна. В целом, в коде программы можно выделить следующие секции:

  • Регистрация класса окон
  • Создание главного окна
  • Цикл обработки очереди сообщений
  • Процедура главного окна

Конечно, в программе могут быть и другие разделы, но данные разделы образуют основной скелет программы. Разберем эти разделы по порядку.

Регистрация класса окон. Регистрация класса окон осуществляется с помощью функции RegisterClassA, единственным параметром которой является указатель на структуру WNDCLASS, содержащую информацию об окне (см. пример ниже).

Создание окна. На основе зарегистрированного класса с помощью функции CreateWindowExA (или CreateWindowA) можно создать экземпляр окна. Как можно заметить, это весьма напоминает объектную модель программирования.

Цикл обработки очереди сообщений. Вот как выглядит этот цикл на языке Си:

while (GetMessage (&msg, NULL, 0, 0))
{
// разрешить использование клавиатуры,
// путем трансляции сообщений о виртуальных клавишах
// в сообщения о алфавитно-цифровых клавишах
TranslateMessage (&msg);
// вернуть управление Windows и передать сообщение дальше
// процедуре окна
DispatchMessage(&msg);
}

Функция GetMessage() "отлавливает" очередное сообщение из ряда сообщений данного приложения и помещает его в структуру MSG.

Что касается функции TranslateMessage, то ее компетенция касается сообщений WM_KEYDOWN и WM_KEYUP, которые транслируются в WM_CHAR и WM_DEADCHAR, а также WM_SYSKEYDOWN и WM_SYSKEYUP, преобразующиеся в WM_SYSCHAR и WM_SYSDEADCHAR. Смысл трансляции заключается не в замене, а в отправке дополнительных сообщений. Так, например, при нажатии и отпускании алфавитно-цифровой клавиши в окно сначала придет сообщение WM_KEYDOWN, затем WM_KEYUP, а затем уже WM_CHAR.

Как можно видеть, выход из цикла ожиданий имеет место только в том случае, если функция GetMessage возвращает 0. Это происходит только при получении сообщения о выходе (сообщение WM_QUIT, см. ниже). Таким образом, цикл ожидания играет двоякую роль: определенным образом преобразуются сообщения, предназначенные для какого-либо окна, и ожидается сообщение о выходе из программы.

Процедура главного окна.

Вот прототип функции19 окна на языке С:

LRESULT CALLBACK WindowFunc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

Оставив в стороне тип возвращаемого функцией значения20, обратите внимание на передаваемые параметры. Вот смысл этих параметров: hwnd — идентификатор окна, message — идентификатор сообщения, wParam и lParam — параметры, уточняющие смысл сообщения (для каждого сообщения могут играть разные роли или не играть никаких). Все четыре параметра, как вы, наверное, уже догадались, имеют тип DWORD.

А теперь рассмотрим "скелет" этой функции на языке ассемблера.

WNDPROC PROC
PUSH EBP
MOV EBP, ESP ; теперь EBP указывает на вершину стека
PUSH EBX
PUSH ESI
PUSH EDI
PUSH DWORD PTR [EBP+14H]; LPARAM (lParam)
PUSH DWORD PTR [EBP+10H]; WPARAM (wParam)
PUSH DWORD PTR [EBP+0CH]; MES (message)
PUSH DWORD PTR [EBP+08H]; HWND (hwnd)
CALL DefWindowProcA@16
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP

Puc. 1.2.1. "Скелет" оконной процедуры.

Прокомментируем фрагмент на Рис. 1.2.1.

RET 16 — выход с освобождением стека от четырех параметров (16=4*4).
Доступ к параметрам осуществляется через регистр EBP:
DWORD PTR [EBP+14H]; LPARAM (lParam)
DWORD PTR [EBP+10H]; WPARAM (wParam)
DWORD PTR [EBP+0CH]; MES (message) — код сообщения
DWORD PTR [EBP+08H]; HWND (hwnd) — дескриптор окна.

Функция DefWindowProc вызывается для тех сообщений, которые не обрабатываются в функции окна. В данном примере, как Вы понимаете, не обрабатываются все приходящие в функцию окна сообщения.


19 В данной книге термины процедура и функция являются синонимами.

20 Нам это никогда не понадобится.


II

Приступим теперь к разбору конкретных примеров. На Рис. 1.2.2 представлена простая программа. Разберите ее внимательно — она будет фундаментом, на котором мы будем строить дальнейшее рассмотрение.

Прежде всего обратите внимание на директивы INCLUDELIB. В пакете MASM32 довольно много разных библиотек. Для данного примера нам понадобились две: user32.lib и kernel32.lib.

Сначала мы определяем константы и внешние библиотечные процедуры. В действительности все эти определения можно найти в include-файлах, прилагаемых к пакету MASM32. Мы не будем использовать стандартные include-файлы по двум причинам: во-первых, так удобнее понять технологию программирования, во-вторых — так легче перейти от MASM к TASM.

Необходимо четко понимать способ функционирования процедуры окна, так как именно это определяет суть программирования под Windows. Задача данной процедуры — правильная реакция на все приходящие сообщения. Давайте детально разберемся в работе нашего приложения. Сначала обратим внимание на то, что необработанные сообщения должны возвращаться в систему при помощи функции DefWindowProcA.

Мы отслеживаем четыре сообщения: WM_CREATE, WM_DESTROY, WM_LBUTTONDOWN, WM_RBUTTONDOWN. Сообщения WM_CREATE и WM_DESTROY в терминах объектного программирования играют роль конструктора и деструктора: они приходят в функцию окна при создании окна и при уничтожении окна. Если щелкнуть по крестику в правом углу окна, то в функцию окна придет сообщение WM_DESTROY. Далее будет выполнена функция PostQuitMessage и приложению будет послано сообщение WM_QUIT, которое вызовет выход из цикла ожидания и выполнение функции ExitProcess, что в свою очередь приведет к удалению приложения из памяти.

Обращаю Ваше внимание на метку _ERR — переход на нее происходит при возникновении ошибки, и здесь можно поместить соответствующее сообщение.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
; сообщение приходит при закрытии окна
WM_DESTROY equ 2
; сообщение приходит при создании окна
WM_CREATE equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_LBUTTONDOWN equ 201h
; сообщение при щелчке правой кнопкой мыши в области окна
WM_RBUTTONDOWN equ 204h
; свойства окна
CS_VREDRAW equ 1h
CS_HREDRAW equ 2h
CS_GLOBALCLASS equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
style equ CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS
; идентификатор стандартной иконки
IDI_APPLICATION equ 32512
; идентификатор курсора
IDC_CROSS equ 32515
; режим показа окна - нормальный
SW_SHOWNORMAL equ 1
; прототипы внешних процедур
EXTERN MessageBoxA@16: NEAR
EXTERN CreateWindowExA@48:NEAR
EXTERN DefWindowProcA@16:NEAR
EXTERN DispatchMessageA@4:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetMessageA@16:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN LoadCursorA@8:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN PostQuitMessage@4:NEAR
EXTERN RegisterClassA@4:NEAR
EXTERN ShowWindow@8:NEAR
EXTERN TranslateMessage@4:NEAR
EXTERN UpdateWindow@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;--------------------------------------------------
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ? ; идентификатор окна,
; получающего сообщение
MSMESSAGE DD ? ; идентификатор сообщения
MSWPARAM DD ? ; доп. информация о сообщении
MSLPARAM DD ? ; доп. информация о сообщении
MSTIME DD ? ; время посылки сообщения
MSPT DD ? ; положение курсора, во время посылки
; сообщения
MSGSTRUCT ENDS
;---------
WNDCLASS STRUC
CLSSTYLE DD ? ; стиль окна
CLWNDPROC DD ? ; указатель на процедуру окна
CLSCSEXTRA DD ? ; информация о доп. байтах для
; данной структуры
CLWNDEXTRA DD ? ; информация о доп. байтах для окна
CLSHINSTANCE DD ? ; дескриптор приложения
CLSHICON DD ? ; идентификатор иконы окна
CLSHCURSOR DD ? ; идентификатор курсора окна
CLBKGROUND DD ? ; идентификатор кисти окна
CLMENUNAME DD ? ; имя-идентификатор меню
CLNAME DD ? ; специфицирует имя класса окон
WNDCLASS ENDS
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
NEWHWND DD 0
MSG MSGSTRUCT <?>
WC WNDCLASS <?>
HINST DD 0 ; здесь хранится дескриптор приложения
TITLENAME DB 'Простой пример 32-битного приложения',0
CLASSNAME DB 'CLASS32',0
CAP DB 'Сообщение',0
MES1 DB 'Вы нажали левую кнопку мыши',0
MES2 DB 'Выход из программы. Пока!',0
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
REG_CLASS:
; заполнить структуру окна стиль
MOV [WC.CLSSTYLE], style
; процедура обработки сообщений
MOV [WC.CLWNDPROC], OFFSET WNDPROC
MOV [WC.CLSCEXTRA], 0
MOV [WC.CLWNDEXTRA], 0
MOV EAX, [HINST]
MOV [WC.CLSHINSTANCE], EAX
;--------- иконка окна
PUSH IDI_APPLICATION
PUSH 0
CALL LoadIconA@8
MOV [WC.CLSHICON], EAX
;---------- курсор окна
PUSH IDC_CROSS
PUSH 0
CALL LoadCursorA@8
MOV [WC.CLSHCURSOR], EAX
;---------
MOV [WC.CLBKGROUND], 17 ; цвет окна
MOV DWORD PTR [WC.CLMENUNAME], 0
MOV DWORD PTR [WC.CLNAME], OFFSET CLASSNAME
PUSH OFFSET WC
CALL RegisterClassA@4
; создать окно зарегистрированного класса
PUSH 0
PUSH [HINST]
PUSH 0
PUSH 0
PUSH 400 ; DY — высота окна
PUSH 400 ; DX - ширина окна
PUSH 100 ; Y — координата левого верхнего угла
PUSH 100 ; X — координата левого верхнего угла
PUSH WS_OVERLAPPEDWINDOW
PUSH OFFSET TITLENAME ; имя окна
PUSH OFFSET CLASSNAME ; имя класса
PUSH 0
CALL CreateWindowExA@48
; проверка на ошибку
CMP EAX, 0
JZ _ERR
MOV [NEWHWND], EAX ; дескриптор окна
; --------------------------------------------------
PUSH SW_SHOWNORMAL
PUSH [NEWHWND]
CALL ShowWindow@8; показать созданное окно
; --------------------------------------------------
PUSH [NEWHWND]
CALL UpdateWindow@4 ; команда перерисовать видимую
; часть окна, сообщение WM_PAINT
; петля обработки сообщений
MSG_LOOP:
PUSH 0
PUSH 0
PUSH 0
PUSH OFFSET MSG
CALL GetMessageA@16
CMP EAX, 0
JE END_LOOP
PUSH OFFSET MSG
CALL TranslateMessage@4
PUSH OFFSET MSG
CALL DispatchMessageA@4
JMP MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
PUSH [MSG.MSWPARAM]
CALL ExitProcess@4
_ERR:
JMP END_LOOP
; --------------------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014H] LPARAM
; [EBP+10H] 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_DESTROY
JE WMDESTROY
CMP DWORD PTR [EBP+0CH], WM_CREATE
JE WMCREATE
CMP DWORD PTR [EBP+0CH] ,WM_LBUTTONDOWN ;левая кнопка
JE LBUTTON
CMP DWORD PTR [EBP+0CH] ,WM_RBUTTONDOWN ;правая кнопка
JE RBUTTON
JMP DEFWNDPROC
; нажатие правой кнопки приводит к закрытию окна
RBUTTON:
JMP WMDESTROY
; нажатие левой кнопки мыши
LBUTTON:
; выводим сообщение
PUSH 0 ; МВ_ОК
PUSH OFFSET CAP
PUSH OFFSET MES1
PUSH DWORD PTR [EBP+08H]
CALL MessageBoxA@16
MOV EAX, 0
JMP FINISH
WMCREATE:
MOV EAX, 0
JMP FINISH
DEFWNDPROC:
PUSH DWORD PTR [EBP+14H]
PUSH DWORD PTR [EBP+10H]
PUSH DWORD PTR [EBP+0CH]
PUSH DWORD PTR [EBP+08H]
CALL DefWindowProcA@16
JMP FINISH
WMDESTROY:
PUSH 0 ; МВ_ОК
PUSH OFFSET CAP
PUSH OFFSET MES2
PUSH DWORD PTR [EBP+08H] ; дескриптор окна
CALL MessageBoxA@16
PUSH 0
CALL PostQuitMessage@4 ; сообщение WM_QUIT
MOV EAX, 0
FINISH:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Puc. 1.2.2. Простой пример программы для Windows (MASM32).

III

После разбора программы на Рис. 1.2.2 возникает вопрос по поводу ее реализации на ассемблере TASM. В действительности здесь требуются минимальные изменения: вместо библиотек user32.lib и kernel32.lib надо подключить библиотеку import32.lib, удалить во всех именах библиотечных процедур @N и далее выполнить команды tasm32 /ml prog.asm и tlink32 -aa prog.obj.

.386P
; плоская модель
MODEL FLAT, stdcall
; константы
; сообщение приходит при закрытии окна
WM_DESTROY equ 2
; сообщение приходит при создании окна
WM_CREATE equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_LBUTTONDOWN equ 201h
; сообщение при щелчке правой кнопкой мыши в области окна
WM_RBUTTONDOWN equ 204h
; свойства окна
CS_VREDRAW equ 1h
CS_HREDRAW equ 2h
CS_GLOBALCLASS equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
style equ CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS
; идентификатор стандартной иконки
IDI_APPLICATION equ 32512
; идентификатор курсора
IDC_CROSS equ 32515
; режим показа окна — нормальный
SW_SHOWNORMAL equ 1
; прототипы внешних процедур
EXTERN MessageBoxA:NEAR
EXTERN CreateWindowExA:NEAR
EXTERN DefWindowProcA:NEAR
EXTERN DispatchMessageA:NEAR
EXTERN ExitProcess:NEAR
EXTERN GetMessageA:NEAR
EXTERN GetModuleHandleA:NEAR
EXTERN LoadCursorA:NEAR
EXTERN LoadIconA:NEAR
EXTERN PostQuitMessage:NEAR
EXTERN RegisterClassA:NEAR
EXTERN ShowWindow:NEAR
EXTERN TranslateMessage:NEAR
EXTERN UpdateWindow:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\tasm32\lib\import32.lib
;--------------------------------------------------
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ? ; идентификатор окна,
; получающего сообщение
MSMESSAGE DD ? ; идентификатор сообщения
MSWPARAM DD ? ; доп. информация о сообщении
MSLPARAM DD ? ; доп. информация о сообщении
MSTIME DD ? ; время посылки сообщения
MSPT DD ? ; положение курсора, во время посылки сообщения
MSGSTRUCT ENDS
; ---------
WNDCLASS STRUC
CLSSTYLE DD ? ; стиль окна
CLWNDPROC DD ? ; указатель на процедуру окна
CLSCEXTRA DD ? ; информация о доп. байтах для
; данной структуры
CLWNDEXTRA DD ? ; информация о доп. байтах для окна
CLSHINSTANCE DD ? ; дескриптор приложения
CLSHICON DD ? ; идентификатор иконы окна
CLSHCURSOR DD ? ; идентификатор курсора окна
CLBKGROUND DD ? ; идентификатор кисти окна
CLMENUNAME DD ? ; имя-идентификатор меню
CLNAME DD ? ; специфицирует имя класса окон
WNDCLASS ENDS
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
NEWHWND DD 0
MSG MSGSTRUCT <?>
WC WNDCLASS <?>
HINST DD 0 ; здесь хранится дескриптор приложения
TITLENAME DB 'Простой пример 32-битного приложения',0
CLASSNAME DB 'CLASS32',0
CAP DB 'Сообщение',0
MES1 DB 'Вы нажали левую кнопку мыши',0
MES2 DB 'Выход из программы. Пока!',0
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA
MOV [HINST], EAX
REG_CLASS:
; заполнить структуру окна стиль
MOV [WC.CLSSTYLE], style
; процедура обработки сообщений
MOV [WC.CLWNDPROC], OFFSET WNDPROC
MOV [WC.CLSCSEXTRA], 0
MOV [WC.CLWNDEXTRA], 0
MOV EAX, [HINST]
MOV [WC.CLSHINSTANCE], EAX
; ---------- иконка окна
PUSH IDI_APPLICATION
PUSH 0
CALL LoadIconA
MOV [WC.CLSHICON], EAX
;----------- курсор окна
PUSH IDC_CROSS
PUSH 0
CALL LoadCursorA
MOV [WC.CLSHCURSOR], EAX
; ----------
MOV [WC.CLBKGROUND], 17 ; цвет окна
MOV DWORD PTR [WC.CLMENUNAME], 0
MOV DWORD PTR [WC.CLNAME], OFFSET CLASSNAME
PUSH OFFSET WC
CALL RegisterClassA
; создать окно зарегистрированного класса
PUSH 0
PUSH [HINST]
PUSH 0
PUSH 0
PUSH 400 ; DY - высота окна
PUSH 400 ; DX - ширина окна
PUSH 100 ; Y - координата левого верхнего угла
PUSH 100 ; X - координата левого верхнего угла
PUSH WS_OVERLAPPEDWINDOW
PUSH OFFSET TITLENAME ; имя окна
PUSH OFFSET CLASSNAME ; имя класса
PUSH 0
CALL CreateWindowExA
; проверка на ошибку
CMP EAX, 0
JZ _ERR
MOV [NEWHWND], EAX
PUSH SW_SHOWNORMAL
PUSH [NEWHWND]
CALL ShowWindow ; показать созданное окно
PUSH [NEWHWND]
CALL UpdateWindow ; команда перерисовать видимую
; часть окна, сообщение WM_PAINT
; петля обработки сообщений
MSG_LOOP:
PUSH 0
PUSH 0
PUSH 0
PUSH OFFSET MSG
CALL GetMessageA
CMP EAX, 0
JE END_LOOP
PUSH OFFSET MSG
CALL TranslateMessage
PUSH OFFSET MSG
CALL DispatchMessageA
JMP MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
PUSH [MSG.MSWPARAM]
CALL ExitProcess
_ERR:
JMP END_LOOP
;--------------------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] LPARAM
; [EBP+10H] WAPARAM
; [EBP+0СН] MES
; [EBP+8] HWND
WNDPROC PROC
PUSH EBP
MOV EBP, ESP
PUSH EBX
PUSH ESI
PUSH EDI
CMP DWORD PTR [EBP+0CH], WM_DESTROY
JE WMDESTROY
CMP DWORD PTR [EBP+0CH], WM_CREATE
JE WMCREATE
CMP DWORD PTR [EBP+0CH], WM_LBUTTONDOWN ; левая кнопка
JE LBUTTON
CMP DWORD PTR [EBP+0CH], WM_RBUTTONDOWN ; правая кнопка
JE RBUTTON
JMP DEFWNDPROC
; нажатие правой кнопки приводит к закрытию окна
RBUTTON:
JMP WMDESTROY
; нажатие левой кнопки мыши
LBUTTON:
; выводим сообщение
PUSH 0 ; МВ_ОК
PUSH OFFSET CAP
PUSH OFFSET MES1
PUSH DWORD PTR [EBP+08H]
CALL MessageBoxA
MOV EAX, 0
JMP FINISH
WMCREATE:
MOV EAX, 0
JMP FINISH
DEFWNDPROC:
PUSH DWORD PTR [EBP+14H]
PUSH DWORD PTR [EBP+10H]
PUSH DWORD PTR [EBP+0CH]
PUSH DWORD PTR [EBP+08H]
CALL DefWindowProcA
JMP FINISH
WMDESTROY:
PUSH 0 ; MB_OK
PUSH OFFSET CAP
PUSH OFFSET MES2
PUSH DWORD PTR [EBP+08H] ; дескриптор окна
CALL MessageBoxA
PUSH 0
CALL PostQuitMessage ; сообщение WM_QUIT
MOV EAX, 0
FINISH:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Рис. 1.2.3. Простой пример программы для Windows (TASM32).

Кстати заметьте, что исполняемый модуль, транслируемый в TASM32, всегда несколько длиннее аналогичного модуля, транслируемого с помощью пакета MASM32.

Обратите внимание на то, как задаются свойства окон. Описание этих свойств можно найти в программе помощи для функции CreateWindow.

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

Приведенные примеры показывают, что процедура окна должна правильно реагировать на пришедшие сообщения. В дальнейшем Вы узнаете, что и сама процедура может посылать сообщения приложению, другим окнам, а также своему окну21.

Часть API-функций получила суффикс "А". Дело в том, что функции имеют два прототипа: с суффиксом "А" - поддерживают ANSI, а с суффиксом "W" - Unicode.


21 Собственно PostQuitMessage есть пример посылки сообщения своему приложению.


IV

Здесь мне хотелось бы рассмотреть подробнее вопрос о передачи параметров через стек.

Это не единственный способ передачи параметров, но именно через стек передаются параметры API-функциям, поэтому на это необходимо обратить внимание.

Состояния стека до и после вызова процедуры приводится на Рис. 1.2.4.

Рис. 1.2.4 демонстрирует стандартный вход в процедуру, практикующийся в таких языках высокого уровня, как Паскаль и Си.

При входе в процедуру выполняется стандартная последовательность команд:

PUSH EBP
MOV EBP, ESP
SUB ESP, N ; N - количество байт для локальных переменных.

Адрес первого параметра определяется как [EBP+8Н], что мы уже неоднократно использовали. Адрес первой локальной переменной, если она зарезервирована, определяется как [EBP-4] (имеется в виду переменная типа DWORD). На ассемблере не очень удобно использовать локальные переменные, и мы не будем резервировать для них место (смотрите, однако, Главу 2.5).

В конце процедуры идут команды:

MOV ESP, EBP
POP EBP
RET М

Здесь M - объем, взятый у стека для передачи параметров.

Такого же результата можно добиться, используя команду ENTER N,0 (PUSH EBP\MOV EBP,ESP\SUB ESP) в начале процедуры и LEAVE (MOV ESP,EBP\POP EBP) в конце процедуры.

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

Хотелось бы остановиться еще на одном вопросе, связанным со структурой процедуры и ее вызова.

Существуют два основных подхода (см. [1]). Условно первый подход можно назвать Си-подходом, а второй — Паскаль-подходом.

Первый подход предполагает, что процедура "не знает", сколько параметров находится в стеке. Естественно, в этом случае освобождение стека от параметров должно происходить после команды вызова процедуры, например, с помощью команды POP или команды ADD ESP,N (N - количество байт в параметрах).

Второй подход основан на том, что количество параметров фиксировано, а стек можно освободить в самой процедуре. Это достигается выполнением команды RET N (N - количество байт в параметрах). Как Вы уже, наверное, догадались, вызов функций API осуществяется по второй схеме. Впрочем, есть и исключения, о которых вы узнаете несколько позже (см. Гл. 2.1).

Рис. 1.2.4. Схема передачи параметров в процедуру