После того как вы познакомились с программой на Рис. 2.2.4, давайте ее подробнее обсудим.
Начнем с функции wsprintfA
. Как я уже заметил, функция
необычная.
- Она имеет переменное число параметров. Первые два параметра обязательны.
Вначале идет указатель на буфер, куда будет скопирована результирующая строка.
Вторым идет указатель на форматную строку. Форматная строка может содержать
текст, а также формат выводимых параметров. Поля, содержащие информацию о
параметре, начинаются с символа "%". Формат этих полей в точности соответствует
формату полей, используемых в стандартных Си-функциях
printf
,sprintf
и др. Исключением является отсутствие в формате для функцииwsprintf
вещественных чисел. Нет нужды излагать этот формат, заметим только, что каждое поле в форматной строке соответствует параметру (начиная с третьего). В нашем случае форматная строка была равна:"Координаты: %u %u"
. Это означало, что далее в стек будет отправлено два числовых параметра типаWORD
. Конечно, в стек мы отправили два двойных слова, позаботившись лишь о том, чтобы старшие слова были обнулены. Для такой операции очень удобна команда микропроцессораMOVZX
, которая копирует второй операнд в первый так, чтобы биты старшего слова были заполнены нулями. Если бы параметры были двойными словами, то вместо поля%u
мы бы поставили%lu
. В случае, если поле форматной строки определяет строку-параметр, например"%S"
, в стек следует отправлять указатель на строку (что естественно). - Поскольку функция "не знает", сколько параметров может быть в нее
отправлено, разработчики не стали усложнять текст этой функции, и оставили нам
проблему освобождения стека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.