Если ничто другое не помогает,
прочтите, наконец, инструкцию.
Прикладная Мерфология
В этом разделе будут рассмотрены средства для работы с
консолью в среде операционной системы Windows. Как известно, Windows
поддерживает работу двух типов приложений — оконных, в полной мере
использующих все достоинства графического интерфейса, и консольных,
работающих исключительно в текстовом режиме. Поэтому не следует путать
понятие «консоли», используемое выше, с понятием «консольного
приложения» Windows. В предшествующем материале под «консолью»
подразумевались средства для ввода информации с клавиатуры и вывода ее
на экран. Для однозначности изложения далее под термином «консоль» мы
будем иметь в виду либо само консольное приложение, либо его видимую
часть — окно консольного приложения.
Далее мы рассмотрим порядок ввода-вывода данных в консольное
приложение для Windows, написанное на ассемблере. Организация
ввода-вывода в оконном приложении Windows здесь рассматриваться не
будет, так как в уроках 18 «Создание Windows-приложений на ассемблере» и
19 «Архитектура и программирование сопроцессора» учебника этот вопрос
был рассмотрен очень подробно и полно. Что-либо существенное добавить к
уже сказанному трудно. Данная книга рассматривается как практическое
продолжение учебника, поэтому повторяться просто не имеет смысла. Что же
касается организации работы с консольным приложением, то этот вопрос в
учебнике был рассмотрен слабо — в контексте одной из задач урока 20
«ММХ-технология микропроцессоров Intel». Поэтому есть смысл рассмотреть
его более систематично, попутно осветив проблемы ввода-вывода в
консольном приложении Windows. Это тем более актуально, что при
программировании на ассемблере необходимость написания консольного
приложения возникает более часто, чем оконного. Причина проста — малыми
затратами нам становятся доступны многие возможности API Win32.
Организация ввода-вывода в консольном приложении Windows
Язык ассемблера — язык системных программистов,
исследователей принципов работы операционных систем, программ и
аппаратных средств. Здесь не нужны красивые графические оболочки, а,
наоборот, велика потребность в удобных средствах для работы с текстовой
информацией. Операционная система Windows обеспечивает встроенную
поддержку консолей, которые, по определению, являются интерфейсами
ввода-вывода для приложений, работающих в текстовом режиме.
Консоль состоит из одного входного и нескольких экранных
буферов. Входной буфер представляет собой очередь, каждая запись которой
содержит информацию относительно отдельного входного события консоли.
Экранный буфер — h двумерный массив, содержащий символы, выводимые в
окно консоли, и данные [ об их цвете.
Очередь входного буфера содержит информацию о следующих событиях:
- нажатии и отпускании клавиш;
- манипуляциях мышью — движение, нажатие-отпускание кнопок;
- изменение размера активного экранного буфера, состояние прокрутки.
Для поддержки работы консольных приложений API Win32
содержит более сорока функций, предназначенных для интеграции в среду
Windows программ, работающих в текстовом режиме. Данные функции
предоставляют два уровня доступа к консоли — высокий и низкий.
Консольные функции ввода высокого уровня позволяют приложению извлечь
данные, полученные при вводе с клавиатуры и сохраненные во входном
буфере консоли. Консольные функции вывода высокого уровня позволяют
приложению записать данные в устройства стандартного вывода или ошибки с
тем, чтобы отобразить этот текст в экранном буфере консоли. Функции
высокого уровня также поддерживают переназначение стандартных
дескрипторов ввода-вывода и управление режимами работы консоли.
Консольные функции низкого уровня позволяют приложениям получить
детальную информацию о вводе с клавиатуры, событиях нажатия-отпускания
кнопок мыши и о манипуляциях пользователя с окном консоли, а также
осуществить больший контроль над выводом данных на экран.
Таким образом, API Win32 предоставляет два разных подхода для
обеспечения ввода-вывода с консолью, выбор нужного зависит от гибкости и
полноты контроля, которыми хочет обладать приложение для обеспечения
своей работы с консолью. Функции высокого уровня обеспечивают простоту
процесса ввода-вывода путем использования стандартных дескрипторов
ввода-вывода, но при этом невозможен доступ к входному и экранным
буферам консоли. Функции низкого уровня требуют учета большего
количества деталей и объема кода, но это компенсируется большей
гибкостью.
Высокоуровневый и низкоуровневый консольный ввод-вывод не
являются взаимоисключающими, и приложение может использовать любую
комбинацию этих функций.
С каждой консолью связаны две кодовые таблицы — по одной для
ввода и вывода. Консоль использует входную кодовую таблицу для
трансляции ввода с клавиатуры в соответствующее символьное значение.
Аналогичным образом используется кодовая таблица вывода — для трансляции
символьных значений, формируемых различными функциями вывода, в символ,
отображаемый в окне консоли. Для работы с кодовыми таблицами приложение
может использовать пары — функции SetConsoleCP и GetConsoleCP для
входных кодовых таблиц и функции SetConsoleOutputCP и GetConsoieOutputCP
для выходных кодовых таблиц. Идентификаторы кодовых таблиц, доступные
на данном компьютере, сохраняются в системном реестре следующим ключом:
<
HKEY_LOCAL_MACHINE\\SYSTEM\\
CurrentControlSet\\Control\\Nls\\CodePage
Минимальная программа консольного приложения
Минимальная программа консольного приложения на ассемблере выглядит так:
.model flat.STDCALL ;модель памяти flat.
i ncludeWindowConA.i nc
:Обьявление внешними используемых в данной программе функций Win32 (ASCII):
extrn AllocConsole:PROC
extrn SetConsoleTitleA:PROC
extrn ExitProcess:PROC
.data
TitleText db 'Минимальное консольное приложение Win32'.0
.code
start proc near :точка входа в программу:
:запрос консоли
call AllocConsole проверить успех запроса консоли
test eax.eax
jz exit :неудача :выведем заголовок окна консоли SetConsoleTitle:
push offset TitleText
call SetConsoleTitleA проверить успех вывода заголовка
test eax.eax
jz exit ;неудача
exit: :выход из приложения
:готовим вызов VOID ExitProcess(UINT uExitCode)
push 0
call ExitProcess start endp end start
Если убрать комментарии, то кода будет совсем немного. В
нем представлены вызовы трех функций: AllocConsole, SetConsoleTitle,
ExitProcess.
Первой функцией консольного приложения должна быть функция запроса консоли AllocConsole.
B00L AllocConsole(VOID);
Для вызова функции Al I ocConsol e не требуется никаких параметров. В случае успеха функция Al I ocConsol e возвращает ненулевое значение, при неудаче — нуль. Выделенная консоль представляет собой типичное для Windows окно. Процесс в конкретный момент времени может использовать одну консоль. Если ему нужно запустить еще одну консоль, то прежняя должна быть закрыта или освобождена с помощью функции FreeConsole.
B00L FreeConsole(VOID);
В случае успеха функция FreeConsol e возвращает ненулевое значение, при неудаче — нуль.
При завершении процесса выделенная процессу консоль освобождается
автоматически. В нашем случае использован именно этот вариант закрытия
консоли — функцией ExitProcess.
VOID ExitProcesstUINT uExitCode):
Функции ExitProcess передается код завершения процесса и
всех завершаемых цепочек в этом процессе. Проанализировать этот код
можно с помощью функ-
ций GetExitCodeProcess и GetExitCodeThread. В общем случае в
различных ветвях кода может быть несколько точек выхода с вызовом
ExitProcess. Задавая различные значения кода завершения, можно таким
образом идентифицировать причину завершения процесса.
Окно консоли может иметь заголовок, для отображения которого предназначена функция SetConsoleTitle.
B00L SetConsolеTitle(LPCTSTR lpConsoleTitle) ;
Функция SetConsoleTitle имеет один параметр — указатель на строку с заголовком консоли, заканчивающуюся нулем.
Организация высокоуровневого консольного ввода-вывода
Для высокоуровневого ввода-вывода приложение может использовать
файловые функции ReadFile и WriteFile, а также функции консольного
ввода-вывода Read-Console и WriteConsole. Эти функции обеспечивают
косвенный доступ к входному и экранным буферам пульта. Физически эти
функции фильтруют записи входного буфера консоли так, чтобы возвратить
ввод как поток символов, игнорируя все другие записи с расширенной
информацией о мыши, клавиатуре и изменении размеров окна консоли.
Отфильтрованный поток символов отображается в окне консоли начиная с
текущей позиции курсора. Существуют два важных отличия в использовании
пар функций ReadFile\WriteFile и ReadConsoleNWrite-Console.
- Поддержка символов Unicode и ANSI. Консольные функции (ReadConso-le\WriteConsole) поддерживают эти наборы, а файловые (ReadFile\Write-File) — нет.
- Функции файлового ввода-вывода могут использоваться как для обращения к файлам, так и к именованным каналам, устройствам, присоединенным к последовательному интерфейсу. Консольные функции ввода-вывода можно использовать только с тремя дескрипторами стандартного ввода-вывода (см. ниже описание функций ReadConsole\WriteConsole).
Из вышесказанного следует, что функции высокоуровневого ввода-вывода обеспечивают простой способ обмена (чтения-записи) потоков символов с консолью.
Операция чтения высокого уровня реализуется функцией ReadConsole, которая получает входные символы из буфера ввода консоли и сохраняет их в указанном буфере.
B00L ReadConsoleCHANDLE hConsolelnput. LPVOID ipBuffer. DWORD nNumberOfCharsToRead. LPDWORD lpNumberOfCharsRead. LPVOID lpReserved);
Параметры этой функции означают следующее:
- hConsolelnput — дескриптор входного потока консоли;
- IpBuffer — указатель на строку, в которую будет записана вводимая строка символов;
- nNumberOfCharsToRead — размер буфера, указанного lpBuffer;
- ipNumberOfCharsRead — количество действительно введенных символов;
- lpReserved — этот параметр не используется, поэтому должен задаваться
как NULL.
Операция записи высокого уровня реализуется
функцией WriteConsole, которая извлекает символы из указанного буфера и
записывает их в экранный буфер, начиная с текущей позиции курсора и
продвигая ее по мере записи символов. B00L WriteConsoleCHANDLE
hConsoleOutput. CONST VOID *lpBuffer.
DWORD nNumberOfCharsToWrite. LPDWORD
ipNumberOfCharsWritten. LPVOID lpReserved);
Параметры этой функции означают следующее:
- hConsoleOutput — дескриптор выходного потока консоли;
- lpBuffer — указатель на выводимую строку;
- nNumberOfCharsToWrite — размер буфера, указанного IpBuffer;
- IpNumberOfCharsWritten — количество действительно выведенных символов;
- lpReserved — этот параметр не используется, поэтому должен задаваться
как NULL.
Для своей работы эти и некоторые другие консольные функции требуют получения стандартных дескрипторов ввода-вывода. Значения этих дескрипторов присваиваются параметрам hConsolelnput и hConsoleOutput. По умолчанию стандартный дескриптор ввода связан с клавиатурой, стандартный дескриптор вывода—с экраном. Получить стандартный дескриптор ввода-вывода можно с помощью функции GetStdHandle.
HANDLE GetStdHand 1 e(DWORD nStdHandle):
На вход функции GetStdHandle должно быть подано одно из следующих значений:
- STD_INPUT_HANDLE = -10 — дескриптор стандартного входного потока;
- STD_OUTPUT_HANDLE = -11 — дескриптор стандартного выходного потока;
- STD_ERROR_HANDLE - -12 — дескриптор стандартного потока ошибок.
Используя функции высокоуровневого ввода-вывода, приложение может управлять цветом текста и фона, с которыми должны отображаться символы, записываемые в экранный буфер. Приложение может изменять следующие свойства высокоуровневого консольного ввода-вывода:
- эхо-контроль вводимых символов на экране из активного экранного буфера;
- ввод строки, окончание операции чтения которой происходит при нажатии клавиши Enter;
- автоматическая обработка некоторых символов, вводимых с клавиатуры:
перевода каретки, нажатия клавиш Ctrl+C и т. д.; - автоматическая обработка некоторых символов, выводимых на экран: перевода строки и каретки, возврата на один символ и т. д.
Функция SetConsol eCursorPosition предназначена для указания позиции, с которой начинается выполнение операций чтения-записи в окно консоли. B00L SetConsoleCursorPosition(HANDLE hConsoleOutput. COORD dwCursorPosition); Параметрами этой функции являются стандартный дескриптор вывода hCon-[' soleOutput, полученный функцией GetStdHandle, и указатель на структуру COORD с координатами новой позиции курсора:
COORD struc x dw 0 у dw 0 ends
По умолчанию цветовое оформление окна консоли
достаточно унылое — черный фон, белый текст. Внести разнообразие во
внешний вид окна консоли поможет функция SetConsoleTextAttribute, с
помощью которой можно изменить установки цвета по умолчанию для текста и
фона.
B00L SetConsoleTextAttributetHANDLE hConsoleOutput. WORD wAttributes):
Первый параметр — без комментариев, второй определяет цвет
текста и фона. Второй параметр формируется как логическое ИЛИ следующих
значений:
- FOREGROUND_BLUE=0001h - синий текст;
- FOREGROUND_GREEN=0002h - зеленый текст;
- FOREGROUND_RED=0004h — красный текст;
- FOREGROUND_INTENSITY=0008h — текст повышенной яркости;
- BACKGROUND_BLUE=0010h - голубой фон;
- BACKGROUND_GREEN=0020h - зеленый фон;
- BACKGROUND_RED=0040h - красный фон;
- BACKGROUND_INTENSITY=0080h — фон повышенной яркости.
Для задания белого цвета складываются три компоненты, для задания черного — компоненты не задаются вовсе.
Пример программы ввода-вывода в консоль
Для демонстрации использования функций высокоуровневого ввода-вывода в окно консоли разработаем программу, которая вводит с клавиатуры строку и отображает ее в заголовке окна консоли, а затем выводит эту строку в окне консоли с изменением текущей позиции курсора и цвета текста.
:prg05_11.asm - программа ввода-вывода в консоль с изменением атрибутов выводимого текста
!
.data
.code
start proc near -.точка входа в программу:
..........
:получим стандартные дескрипторы ввода-вывода
push STD_OUTPUT_HANDLE
call GetStdHandle
movdOut.eax :dOut-fleCKpnnTop вывода консоли
push STD_INPUT_HANDLE
call GetStdHandle
mov din.eax idln-дескриптор ввода консоли :введем строку
.¦установим курсор в позицию (2,6)
mov con.хх.2
mov con.yy,6
push con
push dOut
call SetConsoleCursorPosition cmp eax. 0
jz exit ;если неуспех
push 0
push offset NumWri количество действительно введенных символов
push 80 :размер буфера TitleText для ввода
push offset TitleText
push din
call ReadConsoleA
cmp eax, 0
jz exit :если неуспех
:выведем введенную строку в заголовок окна консоли:
push offset TitleText
call SetConsoleTitleA проверить успех вывода заголовка
test eax.eax
jz exit ;неудача
:выведем строку в окно консоли с различных позиций и
с разными цветами установим курсор в позицию (2.5)
mov ecx.10 ;строку выведем 10 раз
mov bl.10000001b начальные атрибуты
ml: push ecx
inc con.xx
inc con.yy
push con
push dOut
call SetConsoleCursorPosition
cmp eax.O
jz exit :если неуспех ;определим атрибуты выводимых символов -
будем получать их циклически сдвигом - регистр
BL
хог еах.еах
rol Ы.1
mov al.bl
push eax
push dOut
call SetConsoleTextAttribute
cmp eax.O
jz exit ;если неуспех :вывести строку
push 0
push offset NumWri действительное количество выведенных на экран
push NumWri ;длина строки для вывода на экран
push offset TitleText :адрес строки для вывода на экран
push dOut
call WriteConsoleA
cmp eax.O
jz exit :если неуспех pop ecx
loop ml
exit: :выход из приложения
Каждый консольный процесс имеет свой собственный список
функций-обработчиков, которые вызываются системой, когда происходят
определенные собы
тия, например при активном окне консоли пользователь нажимает
комбинации клавиш Ctrl+C, Ctrl+Break или Ctrl+Close. При запуске
консольного приложения список функций-обработчиков содержит только
заданную по умолчанию функцию-обработчик, которая вызывает функцию
ExitProcess. Консольный процесс может добавлять или удалять
дополнительные функции-обработчики, вызывая функцию
SetConsoleCtrlHandler.
B00L SetConsoleCtrlHandler(PHANDLER_ROUTINE HandlerRoutine. B00L Add): Данная функция имеет два параметра:
- HandlerRoutine — указатель на определенную приложением функцию HandlerRoutine, которая должна быть добавлена или удалена;
- Add — логическое значение, которое означает: 1 — функция должна быть
добавлена, 0 — функцию необходимо удалить.
Функция HandlerRoutine — это определенная
приложением функция обратного вызова. Консольный процесс использует эту
функцию, чтобы обработать нажатия клавиш управления. На самом деле
HandlerRoutine — идентификатор-заполнитель для определенного приложением
имени функции. B00L WINAPI HandIerRoutine(DWORD dwCtrlType):
Параметр DwCtrlType определяет тип сигнала управления,
получаемого обработчиком. Этот параметр может принимать одно из
следующих значений:
- CTRL_C_EVENT=O — сигнал, имитирующий нажатие клавиш Ctrl+C, может быть получен из двух источников: с клавиатуры или как сигнал, сгенерированный функцией GenerateConsoleCtrl Event;
- CTRL_BREAK_EVENT=1 — сигнал имитирующий нажатие клавиш Ctrl+Break, может быть получен из двух источников: с клавиатуры или как сигнал, сгенерированный функцией GenerateConsoleCtrl Event;
- CTRL_CL0SE_EVENT=2 — сигнал, который система посылает всем процессам, подключенным к данному консольному приложению, когда пользователь его закрывает (либо выбирая пункт Close в системном меню окна консоли, либо щелкая на кнопке завершения задачи в диалоговом окне Менеджера задач);
- CTRL_LOGOFF_EVENT=5 — сигнал, который посылается всем консольным процессам, когда пользователь завершает работу в системе (этот сигнал не указывает, какой именно пользователь завершает работу);
- CTRL_SHUTD0WN_EVENT=6 — сигнал, который система посылает всем консольным процессам при подготовке к выключению машины. Функция HandlerRoutine должна возвратить логическое значение: 1 — если она обрабатывает конкретный сигнал управления; 0 — для обработки полученного события будет использоваться другая функция-обработчик HandlerRoutine из списка функций-обработчиков для этого процесса (то есть включенная в этот список раньше данной функции).
Как уже было упомянуто, каждый консольный
процесс может определить несколько функций HandlerRoutine, которые
связываются в цепочку. Первоначально этот список содержит только
заданную по умолчанию функцию обработчика, которая вызывает функцию
ExitProcess и, как результат, приводит к завершению текущего консольного
приложения. Консольный процесс добавляет или удаляет
Работа с консолью в среде Windows 233
дополнительные функции обработчика, вызывая функцию
SetConsoleCtrl Handler, которая не затрагивает список
функций-обработчиков для других процессов. Когда консольный процесс
принимает любой из сигналов управления (см. выше), то вызывается
последняя зарегистрированная функция-обработчик, если она не возвращает
1, то управление передается следующему (предыдущему) зарегистрированному
обработчику и т. д., до тех пор пока один из обработчиков не возвратит
1. Если ни один из обработчиков этого не сделал, то вызывается
обработчик, заданный по умолчанию.
Установка обработчиков для сигналов CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT и
CTRL_SHUTDOWN_EVENT дает процессу возможность выполнить специфичные для него
действия по корректному завершению приложения. Пользовательская функция
HandlerRoutine может быть вызвана для того, чтобы выполнить следующие действия:
- вызвать функцию ExitProcess для завершения процесса;
- возвратить 0 (ложь) — это означает, что завершение приложения должен
выполнить обработчик, заданный по умолчанию;
В возвратить 1 — в этом случае никакие другие функции-обработчики не вызываются, а система отображает всплывающее диалоговое окно с запросом о необходимости завершения процесса; система также отображает диалоговое окно, если процесс не отвечает определенное время (5 секунд для CTRLCLOSEEVENT и 20 секунд для CTRLLOGOFFEVENT и CTRLSHUTDOWNEVENT); процесс может использовать функцию SetProcessShutdownParameters, чтобы запретить системе отображать последнее диалоговое окно, в этом случае система просто заканчивает процесс, когда HandlerRoutine возвращает истину или когда истекает определенный период времени. Ниже приведен пример пользовательского обработчика события — ввода комбинации Ctrl+C или Ctrl+Break. За основу взята предыдущая программа.
:prg05_12.asm - программа, демонстрирующая использование пользовательского обработчика события.
.data
Text_CTRL_C db "Нажаты CTRL+C"
Len_Text_CTRL=$-Text_CTRL_C
TextJREAK db "Нажаты CTRL+BREAK"
Len_BREAK=$-Text_BREAK
.code
CtrlHandler proc
arg @@dwCtrlType:DWORD
uses ebx.edi. esi ;эти регистры обязательно должны сохраняться
:анализируем тип сигнала управления
cmp @@dwCtrlType.CTRL_C_EVENT
je h_CTRL_C_EVENT
cmp (a@dwCtrlType.CTRL_BREAK_EVENT
je h_CTRL_BREAK_EVENT
jmp h_default
h_CTRL_C_EVENT: :при нажатии CTRL+C выводим сообщение: установим курсор
call SetConsoleCursorPosition :вывести строку Text_CTRL_C call WriteConsoleA
; возвращаем признак обработки
mov eax.l
jmp exit_CtrlHandler h_CTRL_BREAK_EVENT:
;при нажатии CTRL+BREAK выводим сообщение:
установим курсор
call SetConsoleCursorPosition : вывести строку
call WriteConsoleA
;возвращаем признак обработки
mov eax.l
jmp exit_CtrlHandler
h_default: mov eax.Offffffffh;возвращаем остальное не обрабатываем
exit_CtrlHandler: ret CtrlHandler endp start proc near ;точка входа в программу:
:работаем .........
:получим стандартные дескрипторы ввода-вывода
установим функцию-обработчик сигналов управления
push TRUE
push offset cs: CtrlHandler
call SetConsoleCtrlHandler
onp eax. 0
jz exit :если неуспех ;введем строку в буфер TitleText установим курсор в позицию (2.6)
call SetConsoleCursorPosition call ReadConsoleA
:выведем введенную строку в заголовок окна консоли: push offset TitleText call SetConsoleTitleA
:выведем строку в окно консоли с различных позиций и с разными цветами
mov ecx.10 :строку выведем 10 раз
mov bl.10000001b начальные атрибуты ml: push ecx установим курсор в позицию
call SetConsoleCursorPosition
определим атрибуты выводимых символов - будем получать их циклически сдвигом регистра BL хог еах.еах
rol Ы .1
mov al ,Ы
push eax
push d0ut
call SetConsoleTextAttribute . :вывести строку TitleText
call WriteConsoleA cmp eax.0
jz exit ;если неуспех pop ecx
loop ml
Относительно этой программы можно сделать два замечания. Первое касается функции Handl erRoutine, которая в нашей программе называется Ctrl Handler. Как упоминалось, эта функция является функцией обратного вызова. Ее вызов производится при возникновении определенных событий неявно — из системы Windows. По структуре и алгоритму работы она аналогична оконной функции, которую мы рассматривали в уроке 18 «Создание Windows-приложений на ассемблере» учебника. Поэтому за всеми подробностями отсылаем читателя к этому материалу. Второе замечание касается порядка отладки приложений, содержащих определяемые пользователем функции (процедуры) обратного вызова. Первое, что нужно сделать в процессе пошагового выполнения программы в отладчике, — выяснить адрес процедуры обратного вызова. В программе выше это можно сделать, выяснив, какое значение будет помещено в стек при выполнении команд:
..........
[установим функцию-обработчик сигналов управления
push TRUE
push offset cs: Ctrl Handler
call SetConsoleCtrlHandler
cmp eax. 0
jz exit [если неуспех
.........
После этого, сделав активным окно отладчика CPU (выбрав в меню команду
View CPU), необходимо установить указатель мыши в окно с
командами процес-; сора и щелкнуть правой кнопкой мыши. В появившемся
контекстном меню вы-
бер*етс пункт Goto... В результате этих действий отладчик
отобразит диалоговое ¦ окно, в которое необходимо внести адрес
программы-обработчика Ctrl Handler. ; В результате этого в верхней части
окна команд отобразится первая команда [' процедуры Ctrl Handler.
Установите на нее курсор и нажмите клавишу F4. Все, S программа начнет
выполняться по своему алгоритму. При нажатии пользователем
управляющих комбинаций клавиш, допустимых функцией Handl
erRoutine, управ-I ление будет передано этой функции, и вы сможете
произвести ее отладку.