Если ничто другое не помогает,
прочтите, наконец, инструкцию.
Прикладная Мерфология

В этом разделе будут рассмотрены средства для работы с консолью в среде операционной системы 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 ление будет передано этой функции, и вы сможете произвести ее отладку.