Экскурс в битное программирование
Глава 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