Assembler для Windows

              
Консольные программы - что это? О, это для тех, кто любит работать с командной строкой. Самая знаменитая консольная программа - это Far. А на первый взгляд кажется, что работаешь с DOS-программой, не правда ли? Но дело ведь не только в любви к текстовому режиму. Часто нет необходимости и времени для создания графического интерфейса, а программа должна что-то делать, например, обрабатывать большие объемы информации. И вот тут на помощь приходят консольные приложения. Ниже Вы увидите, что консольные приложения очень компактны не только в откомпилированном виде, но и в текстовом варианте. Но главное, консольное приложение имеет такие же возможности обращаться к ресурсам Windows посредством API-функций, как и обычное графическое приложение.

Надо сказать, что в книге автора "Assembler. Учебный курс" [1] излагается несколько экзотический способ трансляции консольных приложений, но связано это было с отсутствием у меня в то время нового инструментария. В данной книге мы используем MASM32 6.14 и TASM32 5.0. Здесь все достаточно просто.

Для MASM:

ml /с /coff cons1.asm
link /subsystem:console cons1.obj

Для TASM32:

TASM32 /ml cons1.asm
tlink32 /ap cons1.obj

Как и раньше, мы предполагаем, что библиотеки будут указываться при помощи директивы includelib. Ниже на Рис. 2.2.1 и Рис. 2.2.2 представлено простое консольное приложение для MASM и TASM соответственно. Для вывода текстовой информации используется функция API WriteConsoleA, параметры которой (слева направо) имеют следующий смысл.

  • 1-й параметр - дескриптор буфера вывода консоли, который может быть получен при помощи функции GetStdHandle.
  • 2-й параметр - указатель на буфер, где находится выводимый текст.
  • 3-й параметр - количество выводимых символов.
  • 4-й параметр - указывает на переменную DWORD, куда будет помещено количество действительно выведенных символов.
  • 5-й параметр - резервный параметр, должен быть равен нулю.

Заметим, что буфер, где находится выводимый текст, не обязательно должен заканчиваться нулем, поскольку для данной функции указывается количество выводимых символов. Договоримся только не путать входные-выходные буфера консоли и буфера, которые мы создаем в программе, в том числе и для обмена с буферами консоли.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
; строка в DOS-овской кодировке
STR1 DB "Консольное приложение",0
LENS DD ? ; количество выведенных символов
RES DD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
; длина строки
PUSH OFFSET STR1
CALL LENSTR
; вывести строку
PUSH OFFSET RES ; резерв
PUSH OFFSET LENS ; выведено символов
PUSH EBX ; длина строки
PUSH OFFSET STR1 ; адрес строки
PUSH EAX ; HANDLE вывода
CALL WriteConsoleA@20
PUSH 0
CALL ExitProcess@4
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
PUSH EBP
MOV EBP,ESP
PUSH EAX
;------------------------------
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX,EDI
MOV ECX,100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI,EBX ; длина строки, включая 0
MOV EBX,EDI
DEC EBX
;------------------------------
POP EAX
POP EBP
RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.1. Простое консольное приложение для MASM32.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
; прототипы внешних процедур
EXTERN GetStdHandle:NEAR
EXTERN WriteConsoleA:NEAR
EXTERN ExitProcess:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\tasm32\lib\import32.lib
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
; строка в DOS-овской кодировке
STR1 DB "Консольное приложение",0
LENS DD ?
; количество выведенных символов
RES DD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle
; длина строки
PUSH OFFSET STR1
CALL LENSTR
; вывести строку
PUSH OFFSET RES ; резерв
PUSH OFFSET LENS ; выведено символов
PUSH EBX ; длина строки
PUSH OFFSET STR1 ; адрес строки
PUSH EAX ; HANDLE вывода
CALL WriteConsoleA
PUSH 0
CALL ExitProcess
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
PUSH EBP
MOV EBP,ESP
PUSH EAX
; ----------
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX,EDI
MOV ECX,100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI, EBX ; длина строки, включая 0
MOV EBX,EDI
DEC EBX
; ----------
POP EAX
POP EBP
RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.2. Простое консольное приложение для TASM32.

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

Прокомментируем теперь приведенные выше программы. При запуске их из командной строки, например из Far'a, в строку выводится сообщение "Консольное приложение". При запуске программы как Windows-приложения консольное окно появляется лишь на секунду. В чем тут дело? Дело в том, что консольные приложения могут создать свою консоль. В этом случае весь ввод-вывод будет производиться в эту консоль. Если же приложение консоль не создает, то здесь может возникнуть двоякая ситуация: либо наследуется консоль, в которой программа была запущена, либо Windows создает для приложения свою консоль.

II

Рассмотрим несколько простых консольных функций и их применение. Во-первых, работать с чужой консолью не всегда удобно. А для того чтобы создать свою консоль, используется функция AllocConsole. По завершении программы все выделенные консоли автоматически освобождаются. Однако это можно сделать и принудительно, используя функцию FreeConsole. Для того чтобы получить дескриптор консоли, используется уже знакомая Вам функция GetStdHandle, аргументом которой может являться следующая из трех констант:

STD_INPUT_HANDLE equ -10 ; для ввода
STD_OUTPUT_HANDLE equ -11 ; для вывода
STD_ERROR_HANDLE equ -12 ; для сообщения об ошибке

Следует отметить, что один процесс может иметь только одну консоль, поэтому выполнение в начале программы FreeConsole обязательно. При запуске программы в "чужой" консоли она наследует эту консоль, поэтому, пока мы не выполним функцию FreeConsole, новой консоли не создать - чужой консоли эта функция закрыть не может.

Для чтения из буфера консоли используется функция ReadConsole. Значения параметров этой функции (слева-направо)28 следующие:

  • 1-й, дескриптор входного буфера.
  • 2-й, адрес буфера, куда будет помещена вводимая информация.
  • 3-й, длина этого буфера.
  • 4-й, количество фактически прочитанных символов.
  • 5-й, зарезервировано.

Установить позицию курсора в консоли можно при помощи функции SetConsoleCursorPosition со следующими параметрами:

  • 1-й, дескриптор входного буфера консоли.
  • 2-й, структура COORD:

COORD STRUC
Х WORD ?
Y WORD ?
COORD ENDS

Хочу лишний раз подчеркнуть, что вторым параметром является не указатель на структуру (что обычно бывает), а именно структура. На самом деле для ассемблера это просто двойное слово (DWORD), у которого младшее слово - координата X, а старшее слово — координата Y.

Установить цвет выводимых букв можно с помощью функции SetConsoleTextAttribute. Первым параметром этой функции является дескриптор выходного буфера консоли, а вторым - цвет букв и фона. Цвет получается путем комбинации (сумма или операция "ИЛИ") двух или более из представленных ниже констант. Причем возможна "смесь" не только цвета и интенсивности, но и цветов (см. программа ниже).

FOREGROUND_BLUE equ 1h ; синий цвет букв
FOREGROUND_GREEN equ 2h ; зеленый цвет букв
FOREGROUND_RED equ 4h ; красный цвет букв
FOREGROUND_INTENSITY equ 8h ; повышенная интенсивность
BACKGROUND_BLUE equ 10h ; синий свет фона
BACKGROUND_GREEN equ 20h ; зеленый цвет фона
BACKGROUND_RED equ 40h ; красный цвет фона
BACKGROUND_INTENSITY equ 80h ; повышенная интенсивность

Для определения заголовка окна консоли используется функция SetConsoleTitle, единственным параметром которой является адрес строки с нулем на конце. Здесь следует оговорить следующее: если для вывода в само окно консоли требовалась DOS-кодировка, то для установки заголовка требуется Windows-кодировка. Чтобы покончить с этой проблемой раз и навсегда, посмотрим, как это можно решить средствами Windows.

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

Мы рассмотрели несколько консольных функций, всего их около пятидесяти. Нет нужды говорить обо всех этих функциях. О некоторых из них я еще скажу, но читатель, я думаю, по приведенным в книге примерам и обсуждениям сможет сам использовать в своих программах другие консольные функции. Замечу только, что для большинства консольных функций характерно то, что при правильном их завершении возвращается ненулевое значение. В случае ошибки в EAX помещается ноль.

Ну что же, пора приступать к разбору следующих примеров.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE equ -10
; атрибуты цветов
FOREGROUND_BLUE equ 1h ; синий цвет букв
FOREGROUND_GREEN equ 2h ; зеленый цвет букв
FOREGROUND_RED equ 4h ; красный цвет букв
FOREGROUND_INTENSITY equ 8h ; повышенная интенсивность
BACKGROUND_BLUE equ 10h ; синий свет фона
BACKGROUND_GREEN equ 20h ; зеленый цвет фона
BACKGROUND_RED equ 40h ; красный цвет фона
BACKGROUND_INTENSITY equ 80h ; повышенная интенсивность
COL1 = 2h+8h ; цвет выводимого текста
COL2 = 1h+2h+8h ; цвет выводимого текста 2
; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTitleA@4:NEAR
EXTERN FreeConsole@0:NEAR
EXTERN AllocConsole@0:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTextAttribute@8:NEAR
EXTERN ReadConsoleA@20:NEAR
EXTERN SetConsoleScreenBufferSize@8:NEAR
EXTERN ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------------------
COOR STRUC
X WORD ?
Y WORD ?
COOR ENDS
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
HANDL DWORD ?
HANDL1 DWORD ?
STR1 DB "Введите строку: ",13,10,0
STR2 DB "Простой пример работы консоли",0
BUF DB 200 dup (?)
LENS DWORD ? ; количество выведенных символов
CRD COOR <?>
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; перекодируем строку
PUSH OFFSET STR1
PUSH OFFSET STR1
CALL CharToOemA@8
; образовать консоль
; вначале освободить уже существующую
CALL FreeConsole@0
CALL AllocConsole@0
; получить HANDL1 ввода
PUSH STD_INPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL1, EAX
; получить HANDL вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL, EAX
; установить новый размер окна консоли
MOV CRD.X, 100
MOV CRD.Y, 25
PUSH CRD
PUSH EAX
CALL SetConsoleScreenBufferSize@8
; задать заголовок окна консоли
PUSH OFFSET STR2
CALL SetConsoleTitleA@4
; установить позицию курсора
MOV CRD.X,0
MOV CRD.Y,10
PUSH CRD
PUSH HANDL
CALL SetConsoleCursorPosition@8
; задать цветовые атрибуты выводимого текста
PUSH COL1
PUSH HANDL
CALL SetConsoleTextAttribute@8
; вывести строку
PUSH OFFSET STR1
CALL LENSTR ; в EBX длина строки
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH OFFSET STR1
PUSH HANDL
CALL WriteConsoleA@20
; ждать ввод строки
PUSH 0
PUSH OFFSET LENS
PUSH 200
PUSH OFFSET BUF
PUSH HANDL1
CALL ReadConsoleA@20
; вывести полученную строку
; вначале задать цветовые атрибуты выводимого текста
PUSH COL2
PUSH HANDL
CALL SetConsoleTextAttribute@8
;------------------------------------------------------------
PUSH 0
PUSH OFFSET LENS
PUSH [LENS] ; длина вводимой строки
PUSH OFFSET BUF
PUSH HANDL
CALL WriteConsoleA@20
; небольшая задержка
MOV ECX,01FFFFFFFH
L1:
LOOP L1
; закрыть консоль
CALL FreeConsole@0
CALL ExitProcess@4
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
ENTER 0,0
PUSH EAX
;--------------
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX, EDI
MOV ECX, 100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI, EBX ; длина строки, включая 0
MOV EBX, EDI
DEC EBX
;--------------
POP EAX
LEAVE
RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.3. Пример создания собственной консоли.

В программе на Рис. 2.2.3, кроме уже описанных функций, появились еще две SetConsoleCursorPosition - установить позицию курсора, и здесь все довольно ясно. Функция SetConsoleScreenBufferSize менее понятна. Она устанавливает размер буфера окна консоли. Этот размер не может уменьшить уже существующий буфер (существующее окно), а может только его увеличить.

Заметим, кстати, что в функции LENSTR мы теперь используем пару команд ENTER-LEAVE (см. Гл. 1.2) вместо обычных сочетаний. Честно говоря, никаких особых преимуществ такое использование не дает. Просто пора расширять свой командный запас.


28 Вообще, как Вы понимаете, для ассемблера практически все параметры имеют тип DWORD. По смыслу же они - или адреса, или значения. Поэтому проще перечислять их, с указанием смысла, чем записывать функцию в Си-нотации.


III

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

Прежде, однако, мы рассмотрим одну весьма необычную, но чрезвычайно полезную API-функцию. Эта функция wsprintfA. Я подчеркиваю, что это именно API-функция, которая предоставляется системой приложению. Эта функция является неким аналогом библиотечной Си-функции - sprintf. Первым параметром функции является указатель на буфер, куда помещается результат форматирования. Второй - указатель на форматную строку, например: "Числа: %lu, %lu". Далее идут указатели на параметры (либо сами параметры, если это числа, см. ниже), число которых определено только содержимым форматной строки. А теперь - самое главное. Поскольку количество параметров не определено, то стек придется освобождать нам. Пример использования этой функции будет дан ниже. Заметим также, что прототипом этой функции для библиотеки import32.lib (TASM32) будет не wsprintfA, a _wsprintfA (!). Наконец отметим, что если функция выполнена успешно, то в EAX будет возвращена длина скопированной строки.

В основе получения информации о клавиатуре и мыши в консольном режиме является функция ReadConsoleInput. Параметры этой функции:

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

А теперь подробно разберемся со структурой, в которой содержится информация о консольном событии. Прежде всего замечу, что в Си эта структура записывается с помощью типа данных union (о типах данных см. Гл. 6 данной части). На мой взгляд, частое использование этого слова притупляет понимание того, что же за этим стоит. И при описании этой структуры мы обойдемся без STRUCT и UNION. Замечу также, что в начале этого блока данных идет двойное слово, младшее слово которого определяет тип события. В зависимости от значения этого слова последующие байты (максимум 18) будут трактоваться так или иначе. Те, кто уже знаком с различными структурами, используемыми в Си и Макроассемблере, теперь должны понять, почему UNION здесь весьма подходит.

Но вернемся к типу события. Всего системой зарезервировано пять типов событий:

KEY_EVENT equ 1h ; клавиатурное событие
MOUSE_EVENT equ 2h ; событие с мышью
WINDOW_BUFFER_SIZE_EVENT equ 4h ; изменился размер окна
MENU_EVENT equ 8h ; зарезервировано
FOCUS_EVENT equ 10h; зарезервировано

А теперь разберем значение других байт структуры в зависимости от происшедшего события.

Событие KEY_EVENT

СмещениеДлинаЗначение
+44При нажатии клавиши значение поля больше нуля.
+82Количество повторов при удержании клавиши.
+102Виртуальный код клавиши.
+122Скан-код клавиши.
+142Для функции ReadConsoleInputA-младший байт равен ASCII-коду клавиши. Для функции ReadConsoleInputW слово содержит код клавиши в двухбайтной кодировке (Unicode).
+164Содержится состояния управляющих клавиш. Может являться суммой следующих констант:
RIGHT_ALT_PRESSED equ 1h
LEFT_ALT_PRESSED equ 2h
RIGHT_CTRL_PRESSED equ 4h
LEFT_CTRL_PRESSED equ 8h
SHIFT_PRESSED equ 10h
NUMLOCK_ON equ 20h
SCROLLLOCK_ON equ 40h
CAPSLOCK_ON equ 80h
ENHANCED_KEY equ 100h
Смысл констант очевиден.

Событие MOUSE_EVENT

СмещениеДлинаЗначение
+44Младшее слово - Х-координата курсора мыши,
старшее слово - Y-координата мыши.
+8 4Описывает состояние кнопок мыши. Первый бит - левая кнопка, второй бит - правая кнопка, третий бит - средняя кнопка. Бит установлен - кнопка нажата.
+12 4Состояние управляющих клавиш. Аналогично предыдущей таблице.
+164Может содержать следующие значения:
MOUSE_MOV equ 1h; было движение мыши
DOUBLE_CL equ 2h; был двойной щелчок

Событие WINDOW_BUFFER_SIZE_EVENT

По смещению +4 находится двойное слово, содержащее новый размер консольного окна. Младшее слово - это размер по X, старшее слово - размер по Y. Да, когда речь идет о консольном окне, все размеры и координаты даются в "символьных" единицах.

Что касается последних двух событий, то там также значимым является двойное слово по смещению +4, Ниже на Рис. 2.2.4 дана простая программа обработки консольных событий.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE equ -10
; тип события
KEY_EV equ 1h
MOUSE_EV equ 2h
; константы - состояния клавиатуры
RIGHT_ALT_PRESSED equ 1h
LEFT_ALT_PRESSED equ 2h
RIGHT_CTRL_PRESSED equ 4h
LEFT_CTRL_PRESSED equ 8h
SHIFT_PRESSED equ 10h
NUMLOCK_ON equ 20h
SCROLLLOCK_ON equ 40h
CAPSLOCK_ON equ 80h
ENHANCED_KEY equ 100h
; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTitleA@4:NEAR
EXTERN FreeConsole@0:NEAR
EXTERN AllocConsole@0:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN SetConsoleTextAttribute@8:NEAR
EXTERN ReadConsoleInputA@16:NEAR
EXTERN ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; структура для определения событий
COOR STRUC
Х WORD ?
Y WORD ?
COOR ENDS
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
HANDL DWORD ?
HANDL1 DWORD ?
TITL DB "Обработка событий мыши",0
BUF DB 200 dup (?)
LENS DWORD ? ; количество выведенных символов
C0 DWORD ?
FORM DB "Координаты: %u %u "
CRD COOR <?>
STR1 DB "Для выхода нажмите ESC",0
MOUS_KEY WORD 9 dup (?)
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; образовать консоль
; вначале освободить уже существующую
CALL FreeConsole@0
CALL AllocConsole@0
; получить HANDL1 ввода
PUSH STD_INPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL1,EAX
; получить HANDL вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
; задать заголовок окна консоли
PUSH OFFSET TITL
CALL SetConsoleTitleA@4
;**********************************
; перекодировка строки
PUSH OFFSET STR1
PUSH OFFSET STR1
CALL CharToOemA@8
; длина строки
PUSH OFFSET STR1
CALL LENSTR
; вывести строку
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH OFFSET STR1
PUSH HANDL
CALL WriteConsoleA@20
; цикл ожиданий: движение мыши или двойной щелчок
L00:
; координаты курсора
MOV CRD.X,0
MOV CRD.Y,10
PUSH CRD
PUSH HANDL
CALL SetConsoleCursorPosition@8
; прочитать одну запись о событии
PUSH OFFSET C0
PUSH 1
PUSH OFFSET MOUS_KEY
PUSH HANDL1
CALL ReadConsoleInputA@16
; проверим, не с мышью ли что?
CMP WORD PTR MOUS_KEY, MOUSE_EV
JNE L001
; здесь преобразуем координаты мыши в строку
MOV AX, WORD PTR MOUS_KEY+6 ; Y-мышь
; копирование с обнулением старших битов
MOVZX EAX,AX
PUSH EAX
MOV AX, WORD PTR MOUS_KEY+4 ; Х-мышь
; копирование с обнулением старших битов
MOVZX EAX,AX
PUSH EAX
PUSH OFFSET FORM
PUSH OFFSET BUF
CALL wsprintfA
; восстановить стек
ADD ESP,16
; перекодировать строку для вывода
PUSH OFFSET BUF
PUSH OFFSET BUF
CALL CharToOemA@8
; длина строки
PUSH OFFSET BUF
CALL LENSTR
; вывести на экран координаты курсора
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH OFFSET BUF
PUSH HANDL
CALL WriteConsoleA@20
JMP L00 ; к началу цикла
L001:
; нет ли события от клавиатуры?
CMP WORD PTR MOUS_KEY,KEY_EV
JNE L00
; есть, какое?
CMP BYTE PTR MOUS_KEY+14,27
JNE L00
;********************************
; закрыть консоль
CALL FreeConsole@0
PUSH 0
CALL ExitProcess@4
RET
; процедура определения длины строки
; строка - [EBP+08Н]
; длина в EBX
LENSTR PROC
ENTER 0,0
PUSH EAX
CLD
MOV EDI, DWORD PTR [EBP+08Н]
MOV EBX, EDI
MOV ECX, 100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI, EBX ; длина строки, включая 0
MOV EBX, EDI
DEC EBX
POP EAX
LEAVE
RET 4
LENSTR ENDP
_TEXT ENDS
END START

Рис. 2.2.4. Пример обработки событий от мыши и клавиатуры для консольного приложения.

После того как вы познакомились с программой на Рис. 2.2.4, давайте ее подробнее обсудим.

Начнем с функции wsprintfA. Как я уже заметил, функция необычная.

  1. Она имеет переменное число параметров. Первые два параметра обязательны. Вначале идет указатель на буфер, куда будет скопирована результирующая строка. Вторым идет указатель на форматную строку. Форматная строка может содержать текст, а также формат выводимых параметров. Поля, содержащие информацию о параметре, начинаются с символа "%". Формат этих полей в точности соответствует формату полей, используемых в стандартных Си-функциях printf, sprintf и др. Исключением является отсутствие в формате для функции wsprintf вещественных чисел. Нет нужды излагать этот формат, заметим только, что каждое поле в форматной строке соответствует параметру (начиная с третьего). В нашем случае форматная строка была равна: "Координаты: %u %u". Это означало, что далее в стек будет отправлено два числовых параметра типа WORD. Конечно, в стек мы отправили два двойных слова, позаботившись лишь о том, чтобы старшие слова были обнулены. Для такой операции очень удобна команда микропроцессора MOVZX, которая копирует второй операнд в первый так, чтобы биты старшего слова были заполнены нулями. Если бы параметры были двойными словами, то вместо поля %u мы бы поставили %lu. В случае, если поле форматной строки определяет строку-параметр, например "%S", в стек следует отправлять указатель на строку (что естественно).29
  2. Поскольку функция "не знает", сколько параметров может быть в нее отправлено, разработчики не стали усложнять текст этой функции, и оставили нам проблему освобождения стека30. Это производится командой ADD ESP,N. Здесь N - это количество освобождаемых байтов.

Обратимся теперь к функции ReadConsoleInputA. К уже сказанному о ней добавлю только, что если буфер событий пуст, то функция будет ждать, пока "что-то" не случится с консольным окном, и только тогда возвратит управление. Кроме того, мы можем указать, чтобы функция возвращала не одну, а несколько записей о происшедших с консолью событиях. В этом случае в буфер будет помещена не одна, а несколько информационных записей. Но мы на этом останавливаться не будем.

По обыкновению отмечу, как откомпилировать данную программу в TASM32. Как обычно, удаляем все значки @N, указываем библиотеку import32.lib и наконец wsprintfA меняем на _wsprintfA.


29 В этой связи не могу не отметить, что встречал в литературе по ассемблеру (!) утверждение, что все помещаемые в стек для этой функции параметры являются указателями. Как видим, вообще говоря, это не верно.

30 Компилятор Си, естественно, делает это за нас.


IV

В последнем разделе главы мы рассмотрим довольно редко освещаемый в литературе вопрос - таймеры в консольном приложении. Надо сказать, что мы несколько опережаем события и рассматриваем таймер в консольном приложении раньше, чем в приложении GUI (Graphic Universal Interface - так называются обычные оконные приложения).

Основным способом создания таймера является использование функции SetTimer. Позднее мы будем подробно о ней говорить. Таймер может быть установлен в двух режимах. Первый режим - это когда последний параметр равен нулю. В этом случае на текущее окно (его функцию) через равные промежутки времени, определяемые третьим параметром, будет приходить сообщение WM_TIMER. Во втором режиме последний параметр указывает на функцию, которая будет вызываться опять через равные промежутки времени. Однако для консольного приложения эта функция не подходит, так как сообщение WM_TIMER пересылается окну функцией DispatchMessage, которая используется в петле обработки сообщений. Но использование этой функции для консольных приложений проблематично.

Для консольных приложений следует использовать функцию timeSetEvent. Вот параметры этой функции:

  • 1-й параметр - время задержки таймера, для нас это время совпадает со временем между двумя вызовами таймера.
  • 2-й параметр - точность работы таймера (приоритет посылки сообщения).
  • 3-й параметр - адрес вызываемой процедуры.
  • 4-й параметр - параметр, посылаемый в процедуру.
  • 5-й параметр - тип вызова - одиночный или периодический.

Если функция завершилась удачно, то в EAX возвращается идентификатор таймера.

Сама вызываемая процедура получает также 5 параметров:

  • 1-й параметр - идентификатор таймера.
  • 2-й параметр - не используется.
  • 3-й параметр - параметр Dat (см. timeSetEvent).
  • 4 и 5-й параметры - не используются.

Для удаления таймера используется функция timeKillEvent, параметром которой является идентификатор таймера.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
STD_INPUT_HANDLE equ -10
TIME_PERIODIC equ 1 ; тип вызова таймера
; атрибуты цветов
FOREGROUND_BLUE equ 1h ; синий цвет букв
FOREGROUND_GREEN equ 2h ; зеленый цвет букв
FOREGROUND_RED equ 4h ; красный цвет букв
FOREGROUND_INTENSITY equ 8h ; повышенная интенсивность
BACKGROUND_BLUE equ 10h ; синий свет фона
BACKGROUND_GREEN equ 20h ; зеленый цвет фона
BACKGROUND_RED equ 40h ; красный цвет фона
BACKGROUND_INTENSITY equ 80h ; повышенная интенсивность
COL1 = 2h+8h ; цвет выводимого текста
; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTitleA@4:NEAR
EXTERN FreeConsole@0:NEAR
EXTERN AllocConsole@0:NEAR
EXTERN CharToOemA@8:NEAR
EXTERN SetConsoleCursorPosition@8:NEAR
EXTERN SetConsoleTextAttribute@8:NEAR
EXTERN ReadConsoleA@20:NEAR
EXTERN timeSetEvent@20:NEAR
EXTERN timeKillEvent@4:NEAR
EXTERN ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
includelib c:\masm32\lib\winmm.lib
;------------------------------------------------------------
COOR STRUC
X WORD ?
Y WORD ?
COOR ENDS
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
HANDL DWORD ?
HANDL1 DWORD ?
STR2 DB "Пример таймера в консольном приложении",0
STR3 DB 100 dup (0)
FORM DB "Число вызовов таймера: %lu",0
BUF DB 200 dup (?)
NUM DWORD 0
LENS DWORD ? ; количество выведенных символов
CRD COOR <?>
ID DWORD ? ; идентификатор таймера
HWND DWORD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; образовать консоль
; вначале освободить уже существующую
CALL FreeConsole@0
CALL AllocConsole@0
; получить HANDL1 ввода
PUSH STD_INPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL1,EAX
; получить HANDL вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
; задать заголовок окна консоли
PUSH OFFSET STR2
CALL SetConsoleTitleA@4
; задать цветовые атрибуты выводимого текста
PUSH COL1
PUSH HANDL
CALL SetConsoleTextAttribute@8
; установить таймер
PUSH TIME_PERIODIC ; периодический вызов
PUSH 0
PUSH OFFSET TIME ; вызываемая таймером процедура
PUSH 0 ; точность вызова таймера
PUSH 1000 ; вызов через одну секунду
CALL timeSetEvent@20
MOV ID, EAX
; ждать ввод строки
PUSH 0
PUSH OFFSET LENS
PUSH 200
PUSH OFFSET BUF
PUSH HANDL1
CALL ReadConsoleA@20
; закрыть таймер
PUSH ID
CALL timeKillEvent@4
; закрыть консоль
CALL FreeConsole@0
PUSH 0
CALL ExitProcess@4
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
ENTER 0,0
PUSH EAX
;--------------------
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX,EDI
MOV ECX,100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI,EBX ; длина строки, включая 0
MOV EBX,EDI
DEC EBX
;--------------------
POP EAX
LEAVE
RET 4
LENSTR ENDP
; процедура вызывается таймером
TIME PROC
PUSHA ; сохранить все регистры
; установить позицию курсора
MOV CRD.X,0
MOV CRD.Y,10
PUSH CRD
PUSH HANDL
CALL SetConsoleCursorPosition@8
; заполнить строку STR3
PUSH NUM
PUSH OFFSET FORM
PUSH OFFSET STR3
CALL wsprintfA
ADD ESP,12 ; восстановить стек
; перекодировать строку STR3
PUSH OFFSET STR3
PUSH OFFSET STR3
CALL CharToOemA@8
; вывести строку с номером вызова таймера
PUSH OFFSET STR3
CALL LENSTR
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH OFFSET STR3
PUSH HANDL
CALL WriteConsoleA@20
INC NUM
POPA
RET 20 ; выход с освобождением стека
TIME ENDP
_TEXT ENDS
END START

Рис. 2.2.5. Таймер в консольном режиме.

Программа на Рис. 2.2.5 будет выводить в окно значение счетчика, которое будет каждую секунду увеличиваться на единицу.

Я начал данную главу с рассуждения о командной строке, но до сих пор не объявил, как работать с командной строкой. О, здесь все очень просто. Есть API-функция GetCommandLine, которая возвращает указатель на командную строку. Эта функция одинаково работает как для консольных приложений, так и для приложений GUI. Ниже представлена программа, печатающая параметры командной строки. Надеюсь, вы понимаете, что первым параметром является полное имя программы.

; программа вывода параметров командной строки
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
; прототипы внешних процедур
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
;------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
BUF DB 100 dup (0)
LENS DWORD ? ; количество выведенных символов
NUM DWORD ?
CNT DWORD ?
HANDL DWORD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
; получить количество параметров
CALL NUMPAR
MOV NUM,EAX
MOV CNT,0
;-------------------------------------
; вывести параметры командной строки
LL1:
MOV EDI,CNT
CMP NUM,EDI
JE LL2
; номер параметра
INC EDI
MOV CNT, EDI
; получить параметр номером EDI
LEA EBX,BUF
CALL GETPAR
; получить длину параметра
PUSH OFFSET BUF
CALL LENSTR
; в конце - перевод строки
MOV BYTE PTR [BUF+EBX],13
MOV BYTE PTR [BUF+EBX+1],10
MOV BYTE PTR [BUF+EBX+2],0
ADD EBX,2
; вывод строки
PUSH 0
PUSH OFFSET LENS
PUSH EBX
PUSH OFFSET BUF
PUSH HANDL
CALL WriteConsoleA@20
JMP LL1
LL2:
PUSH 0
CALL ExitProcess@4
; строка - [EBP+08H]
; длина в EBX
LENSTR PROC
PUSH EBP
MOV EBP,ESP
PUSH EAX
;--------------------
CLD
MOV EDI, DWORD PTR [EBP+08H]
MOV EBX,EDI
MOV ECX,100 ; ограничить длину строки
XOR AL,AL
REPNE SCASB ; найти символ 0
SUB EDI,EBX ; длина строки, включая 0
MOV EBX,EDI
DEC EBX
;--------------------
POP EAX
POP EBP
RET 4
LENSTR ENDP
; определить количество параметров (->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

Рис. 2.2.6. Пример работы с параметрами командной строки.

Рекомендую читателю разобраться в алгоритме работы процедур NUMPAR и GETPAR.

Следует отметить, что для трансляции программы на Рис. 2.2.6 в TASM, кроме обычных, уже известных Вам изменений, для совпадающих меток следует в начале имени поставить "@@" - признак локальности, а в начале программы поставить директиву LOCALS. Транслятор MASM метки, стоящие в процедуре, считает локальными автоматически. Подробнее о локальных метках будет сказано в Главах 2.5 и 2.6.