Assembler для Windows


Глава 4. Экскурс в 16-битное программирование

Данная глава носит уже исторический характер. Обычно при изложении исходных данных по программированию на ассемблере под Windows начинают именно с 16-битного программирования. На мой взгляд, в данный момент оно уже настолько отошло в тень, что не может служить введением в программирование под Windows25. Поскольку у Вас вряд ли возникнет потребность написания 16-битного приложения, я ограничусь одним несложным примером. Всех интересующихся отсылаю к книгам [1,7].

Начнем с того, что рассмотрим, чем отличается 16-битное программирование на ассемблере от 32-битного программирования. Вы убедитесь, что программировать стало гораздо проще.

1. В отличие от памяти в Windows 9x, модель памяти в старой системе Windows была сегментной. Соответственно, и в программе данные, стек и код относятся к разным сегментам26. Как и в операционной системе MS DOS, регистр DS будет указывать на сегмент данных, a CS на сегмент кода.

2. В 16-битной модели адресация осуществляется по схеме сегмент/смещение. Соответственно, адрес определяется двумя 16-битными компонентами. Например, чтобы поместить в стек адрес переменной М, потребуется по крайней мере две команды PUSH DS/PUSH OFFSET M. При этом сегментный адрес должен быть помещен в ячейку с большим адресом, чем смещение (стек растет в сторону меньших адресов).

3. Для создания 16-битных приложений удобнее всего пользоваться пакетом MASM версии 6.1. Поскольку данная глава — всего лишь исторический экскурс, мы не будем останавливаться на том, как делать 16-битные приложения с помощью Турбо Ассемблера. Для компоновки приложений мы будем использовать библиотеку LIBW.LIB, которая поставлялась в пакете MASM 6.1. Особенностью вызова API-функций в данном случае будет обратный порядок помещения параметров в стек, по сравнению с тем, что было до сих пор. В данной главе, и только в ней, будет действовать принцип: СЛЕВА НАПРАВО - СВЕРХУ ВНИЗ.

4. После запуска программы, должна быть выполнена начальная инициализация. Для этого требуется выполнить три функции API: INITTASK, WAITEVENT, INITAPP. Ниже дано описание этих функций.
INITTASK — инициализирует регистры, командную строку и память. Входных параметров не требует. Вызывается первой. Возвращаемые значения регистров: AX = 1 (0 — ошибка), CX — размер стека, DI — уникальный номер для данной задачи, DX — параметр NCMDSHOW (см. ниже), ES — сегментный адрес (селектор) PSP, ES:BX — адрес командной строки, SI — уникальный номер для ранее запущенного того же приложения. В Windows 3.1 при запуске приложения несколько раз каждый раз в память загружается только часть сегментов, часть сегментов является общим ресурсом. Таким методом достигалась экономия памяти. В Windows 95 от этого отказались. Каждая запущенная задача является изолированной и независимой. В Windows 95 SI всегда будет содержать 0. Кроме того, данная процедура заполняет резервный заголовок сегмента данных. WAITEVENT — проверяет наличие событий для указанного приложения. Если событие есть, то оно удаляется из очереди. Вызов:

PUSH AX ; AX — номер приложения, если 0, то текущее.
CALL WAITEVENT

INITAPP — инициализирует очередь событий для данного приложения. Вызов:

PUSH DI ; уникальный номер задачи
CALL INITAPP

В случае ошибки данная функция возвращает 0, иначе ненулевое значение.

5. Некоторые параметры API-функций 16-битного приложения имеют размер 2 байта. В частности параметры WPARAM и HWND процедуры окна также имеют размер 2 байта. С четырехбайтными же параметрами приходится работать в два приема.

6. Наконец последнее отличие: сегмент данных в начале должен содержать резервный блок размером в 16 байт.

7. Интересно, что в 16-битном приложении мы можем пользоваться обычными прерываниями MS DOS, используя INT 21H. Надо только иметь в виду, какие функции прерывания имеют смысл для Windows.


25 Как Вы убедитесь, 16-битное программирование даже несколько сложнее 32-битного.

26 Надеюсь, вы помните, что разделение данных и кода на сегменты в 32-битной модели — вещь условная.


Для иллюстрации сказанного я привожу программу из моей книги [1], которую я незначительно изменил. Трансляция программы производится средствами MASM 6.1:

ML /c prog.asm
LINK prog,prog,,libw (вопрос о файле .def можно проигнорировать)

.286
.DOSSEG ; порядок сегментов согласно соглашению Microsoft
DGROUP GROUP DATA, STA
ASSUME CS: CODE, DS: DGROUP
;прототипы внешних процедур
EXTRN INITTASK:FAR
EXTRN INITAPP:FAR
EXTRN WAITEVENT:FAR
EXTRN DOS3CALL:FAR
EXTRN REGISTERCLASS:FAR
EXTRN LOADCURSOR:FAR
EXTRN GETSTOCKOBJECT:FAR
EXTRN GETMESSAGE:FAR
EXTRN TRANSLATEMESSAGE:FAR
EXTRN DISPATCHMESSAGE:FAR
EXTRN CREATEWINDOW:FAR
EXTRN CREATEWINDOWEX:FAR
EXTRN UPDATEWINDOW:FAR
EXTRN SHOWWINDOW:FAR
EXTRN POSTQUITMESSAGE:FAR
EXTRN DEFWINDOWPROC:FAR
; шаблоны
WNDCL STRUCT
STYLE DW 0 ; стиль класса окна
LPFNWNDPROC DD 0 ; указатель на процедуру обработки
CBCLSEXTRA DW 0
CBWNDEXTRA DW 0
HINSTANCE DW 0
HICON DW 0
HCURSOR DW 0
HBRBACKGROUND DW 0
LPSZMENUNAME DD 0 ; указатель на строку
LPSZCLASSNAME DD 0 ; указатель на строку
WNDCL ENDS
;
MESSA STRUCT
HWND DW ?
MESSAGE DW ?
WPARAM DW ?
LPARAM DD ?
TIME DW ?
X DW ?
Y DW ?
MESSA ENDS
; сегмент стека
STA SEGMENT STACK 'STACK'
DW 2000 DUP(?)
STA ENDS
; сегмент данных
DATA SEGMENT WORD 'DATA'
; в начале 16 байт — резерв, необходимый 16-ти битному
; приложению для правильной работы в среде Windows
DWORD 0
WORD 5
WORD 5 DUP (0)
HPREV DW ?
HINST DW ?
LPSZCMD DD ?
CMDSHOW DW ?
; структура для создания класса
WNDCLASS WNDCL <>
; структура сообщения
MSG MESSA <>
;имя класса окна
CLAS_NAME DB 'PRIVET',0
; заголовок окна
APP_NAME DB '16-битная программа',0
; тип курсора
CURSOR EQU 00007F00H
; стиль окна
STYLE EQU 000CF0000H
; параметры окна
XSTART DW 100
YSTART DW 100
DXCLIENT DW 300
DYCLIENT DW 200
DATA ENDS
; сегмент кода
CODE SEGMENT WORD 'CODE'
_BEGIN: ; I. Начальный код
CALL INITTASK ; инициализировать задачу
OR AX, AX ; CX - границы стека (!!! CX или AX ????)
JZ _ERR
MOV HPREV, SI ; номер предыдущего прил.
MOV HINST, DI ; номер для новой задачи
MOV WORD PTR LPSZCMD, BX ; ES:BX - адрес
MOV WORD PTR LPSZCMD+2,ES ; командной строки
MOV CMDSHOW, DX ; экранный параметр
PUSH 0 ; текущая задача
CALL WAITEVENT ; очистить очередь событий
PUSH HINST
CALL INITAPP ; инициализировать приложения
OR AX, AX
JZ _ERR
CALL MAIN ; запуск основной части
_TO_OS:
MOV AH,4CH
INT 21H ; выйти из программы
_ERR:
; здесь можно поставить сообщение об ошибке
JMP SHORT _TO_OS
; основная процедура
;************************************************************
MAIN PROC
; II. Регистрация класса окна
; стиль окна NULL — стандартное окно
MOV WNDCLASS.STYLE, 0
; процедура обработки
LEA BX,WNDPROC
MOV WORD PTR WNDCLASS.LPFNWNDPROC, BX
MOV BX,CS
MOV WORD PTR WNDCLASS.LPFNWNDPROC+2, BX
;------------------------------------------------------------
; резервные байты в конце резервируемой структуры
MOV WNDCLASS.CBCLSEXTRA, 0
; резервные байты в конце структуры для каждого окна
MOV WNDCLASS.CBWNDEXTRA, 0
; иконка окна отсутствует
MOV WNDCLASS.HICON, 0
; номер запускаемой задачи
MOV AX, HINST
MOV WNDCLASS.HINSTANCE,AX
; определить номер стандартного курсора
PUSH 0
PUSH DS
PUSH CURSOR
CALL LOADCURSOR
MOV WNDCLASS.HCURSOR, AX
; определить номер стандартного объекта
PUSH 0 ; WHITE_BRUSH
CALL GETSTOCKOBJECT
; цвет фона
MOV WNDCLASS.HBRBACKGROUND, AX
; имя меню из файла ресурсов (отсутствует = NULL)
MOV WORD PTR WNDCLASS.LPSZMENUNAME, 0
MOV WORD PTR WNDCLASS.LPSZMENUNAME+2,0
; указатель на строку, содержащую имя класса
LEA BX,CLAS_NAME
MOV WORD PTR WNDCLASS.LPSZCLASSNAME,BX
MOV WORD PTR WNDCLASS.LPSZCLASSNAME+2,DS
; вызов процедуры регистрации
PUSH DS ; указатель на
LEA DI,WNDCLASS
PUSH DI ; структуры WNDCLASS
CALL REGISTERCLASS
CMP AX,0
JNZ _OK1
; ошибка регистрации
RET ; ошибка при регистрации
_OK1:
; III. Создание окна
; адрес строки-имени класса окна
PUSH DS
LEA BX,CLAS_NAME
PUSH BX
; адрес строки-заголовка окна
PUSH DS
LEA BX,APP_NAME
PUSH BX
; стиль окна
MOV BX,HIGHWORD STYLE
PUSH BX
MOV BX,LOWWORD STYLE
PUSH BX
; координата X левого верхнего угла
PUSH XSTART
; координата Y левого верхнего угла
PUSH YSTART
; ширина окна
PUSH DXCLIENT
; высота окна
PUSH DYCLIENT
; номер окна-родителя
PUSH 0
; номер (идентификатор) меню окна
PUSH 0 ; NULL
; номер задачи
PUSH HINST
; адрес блока параметров окна (нет)
PUSH 0
PUSH 0
CALL CREATEWINDOW
CMP AX,0
JNZ NO_NULL
; ошибка создания окна
RET ; ошибка при создании окна
; установка для окна состояния видимости
; (окно или пиктограмма)
; согласно параметру CMDSHOW
; и его отображение
NO_NULL:
MOV SI,AX
PUSH SI
PUSH CMDSHOW
CALL SHOWWINDOW
; посылка команды обновления области окна (команда WM_PAINT)
; сообщение посылается непосредственно окну
PUSH SI
CALL UPDATEWINDOW
; IV. Цикл ожидания
LOOP1:
; извлечение сообщения из очереди
PUSH DS
LEA BX,MSG ; указатель на структуру
PUSH BX ; сообщения
PUSH 0
PUSH 0
PUSH 0
CALL GETMESSAGE
; проверка — не получено сообщение "выход"
CMP AX,0
JZ NO_LOOP1
; перевод всех пришедших сообщений к стандарту ANSI
PUSH DS
LEA BX,MSG
PUSH BX
CALL TRANSLATEMESSAGE
; указать WINDOWS
; передать данное сообщение соответствующему окну
PUSH DS
LEA BX,MSG
PUSH BX
CALL DISPATCHMESSAGE
; замкнуть цикл (петлю)
JMP SHORT LOOP1
NO_LOOP1:
RET
MAIN ENDP
; процедура для заданного класса окон
; WINDOWS передает в эту процедуру параметры:
; HWND - дескриптор (номер) окна, тип WORD
; MES - номер сообщения, тип WORD
; WPARAM - дополнительная информация, тип WORD
; LPARAM - дополнительная информация, тип DWORD
WNDPROC PROC
PUSH BP
MOV BP,SP
MOV AX, [BP+0CH] ; MES - номер сообщения
CMP AX, 2 ; не сообщение ли о закрытии WM_DESTROY
JNZ NEXT
; передать сообщение о закрытии приложения,
; это сообщение будет принято в цикле ожидания,
; и т.о. приложение завершит свой путь
PUSH 0
CALL POSTQUITMESSAGE
JMP _QUIT
NEXT:
; передать сообщение дальше WINDOWS
; своего рода правило вежливости — то,
; что не обработано процедурой обработки,
; предоставляется для обработки WINDOWS
PUSH [BP+0EH] ; HWND
PUSH [BP+0CH] ; MES - номер сообщения
PUSH [BP+0AH] ; WPARAM
PUSH [BP+8] ; HIGHWORD LPARAM
PUSH [BP+6] ; LOWWORD LPARAM
CALL DEFWINDOWPROC
;************************************************************
_QUIT:
POP BP
; вызов процедуры окна всегда дальний, поэтому RETF
RETF 10 ; освобождаем стек от параметров
WNDPROC ENDP
CODE ENDS
END _BEGIN

Рис. 1.4.1. Пример 16-битного приложения.

Вот и все. Сделаю еще замечание по поводу директивы .DOSSEG. Эта директива задает определенный порядок сегментов в ЕХЕ-файле. Этот порядок таков: в начале идет сегмент класса CODE, далее идут сегменты, отличные от класса CODE и не входящие в группу сегментов (GROUP), и наконец — сегменты, сгруппированные при помощи директивы GROUP. Причем в начале идут сегменты, отличные от класса BSS и класса STACK, далее — сегменты класса BSS (если есть) и последний сегмент класса STACK.

Попрощаемся с 16-битным программированием. Более в книге мы с ним не встретимся. Согласитесь теперь, что с точки зрения программирования, Windows 95 была несомненным шагом вперед. Программировать на ассемблере стало еще проще.