Консольное приложение на ассемблере

Метки:  , , , , , , , , , , , , ,

В процессе изучения основных правил работы оконных приложений, мы познакомились с некоторыми принципами программирования графических примитивов. Теперь самое время открыть для себя еще одну разновидность приложений под Windows - консольные приложения.
Во времена разработки первых операционных систем, в них не было такого понятия как графический интерфейс пользователя, был доступен только текстовый режим видеоадаптера с интерфейсом командной строки. Однако, в ходе развития ОС был создан графический интерфейс пользователя (GUI), роль которого со временем существенно возросла. Как раз во время создания GUI стало очевидно, что необходимость в консольных приложениях сохраняется, следствием чего было создание консоли Windows. Консоль, в зависимости от ОС, может работать как в привычных по старым операционным системам текстовых режимах (они еще поддерживаются на аппаратном уровне в современных видеоадаптерах), так и в режимах эмуляции текстового режима. Основная причина по которой консоль в Windows была выделена в отдельную разработку и продолжает своё существование, заключается в следующем:

Большое количество системных утилит, входящих в состав операционной системы, используют исключительно текстовый интерфейс и рассчитаны на использование аргументов командной строки.

Как вы знаете, большинство упомянутых системных утилит мигрировали к нам из ранних версий операционной системы Windows, куда они, в свою очередь, заимствовались как из той же MSDOS (фактически предка Windows), так и из сторонних операционных систем. Соответственно, в старых ОС, до определенного времени, интерфейс взаимодействия с пользователем (командная строка) ограничивался текстовым режимом, по этой причине и разрабатывались утилиты исключительно под него. Казалось бы, при переходе операционных систем к графическому режиму, стоило бы переписывать и системные утилиты под новые реалии? Тем не менее это не имело ни малейшего смысла, поскольку системные утилиты представляют собой обособленную группу программ, предназначенных для работы в командных сценариях, своеобразных программах, которые используют простые языки описания действий, в которых вывод одной программы может поступать на вход другой. То есть вся работа зачастую ведется с минимальным выводом статусных сообщений, либо и вовсе без какого-либо визуального оповещения пользователя. Очевидно, что использование графического интерфейса тут явно не к месту (избыточно). Описанная выше так называемая "преемственность" консольных утилит имеет под собой ряд весомых причин:

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

Помимо класса консольных утилит, в практике разработчика часто возникают задачи, в которых разворачивать оконный графический интерфейс соразмерно напрасной трате своего и процессорного времени. В подобного рода задачах вполне достаточным условием является использование текстового (для вывода диагностических сообщений) или вовсе неинтерактивного (вывод данных в файл) режимов. Действительно, зачем пытаться изображать графический интерфейс там, где он явно излишен или вовсе не обязателен, не проще ли в таких задачах от него отказаться вовсе? Поэтому, сегодня мы акцентируем внимание на особенностях языка Ассемблера (FASM) при написании консольного приложения на ассемблере под Windows, и темой данной статьи будет создание серии простейших консольных приложений, демонстрирующих основные алгоритмы взаимодействия с консолью. Разработанные шаблоны (в примерах) могут быть в дальнейшем использованы в качестве базовых в различного рода проектах.
Итак, в словосочетании консольное приложение на ассемблере присутствует ключевое слово "консоль", на которое стоит обратить особое внимание, поскольку именно оно даст нам понимание основных принципов работы. Скорее всего, люди уже знакомые с компьютером в общем и операционными системами в частности, знают что это такое, хотя могут понимать это в широком, так сказать, смысле этого слова: механизм для ввода информации с клавиатуры и вывода ее на экран. Поэтому, не лишним будет дать серию расширенных определений:

Текстовый пользовательский интерфейс (Text user interface, TUI / Character-Mode User Interface, CUI) - разновидность интерфейса, использующая для ввода-вывода/представления информации текстовый режим (или его эмуляцию) работы видеоадаптера и набор буквенно-цифровых символов и символов псевдографики.

что является базой для:

Консоль (интерфейс командной строки) - текстовый интерфейс управления консольным приложением, ввод-вывод в рамках которого может осуществляться через стандартные потоки: ввод (stdin), вывод (stdout), ошибка (stderr), а так же прямыми чтением/записью из буферов ввода-вывода. По умолчанию к потоку ввода "подключена" клавиатура (мышь), а к потоку вывода — экран (монитора). В частном случае может быть представлена в виде бесконечной бумажной ленты, прокручивающейся в обе стороны.

что, в свою очередь, формирует понятие:

Консольное приложение (character-mode applications) Windows - класс приложений, использующих для взаимодействия с системой/пользователем объекты консоли: текстовый интерфейс и стандартные потоки ввода-вывода. Считается что консольные приложения управляются консолями.

Консольное приложение операционной системы Windows обеспечивает взаимодействие с пользователем через так называемое окно консоли. Примером подобных консольных приложений могут являться: окно командной строки (cmd), файловые менеджеры (например, Far Commander), и ряд типовых системных консольных утилит:

консоль cmd

Различия оконного и консольного приложений

Консольные приложения являются одним из типов исполняемых образов (приложений) Windows, наряду с типовыми оконными (GUI), библиотеками (DLL), драйверами (native) и некоторыми другими. По сути это полноценные приложениями, имеющие ряд специфических отличий:

  • Значение поля Subsystem заголовка результирующего PE-файла равно 3 (IMAGE_SUBSYSTEM_WINDOWS_CUI). На основании значения данного поля, загрузчик образов при подготовке приложения к выполнению производит свойственную консольным приложениям последовательность загрузки.
  • При запуске консольного приложения, загрузчик образов пытается наследовать консоль от процесса-родителя (приложение, из-под которого был произведен запуск):
    • Создается новая консоль, если родительский процесс не имеет консоли.
    • Консоль наследуется, если процесс-родитель тоже является консольным приложением (окно командной строки и прочее).
  • Консоль создается/наследуется с тремя стандартными потоками (объектами) ввода/вывода, каждый из которых имеет свой описатель (для организации программного обмена).
  • [визуально] На основе параметров в ветви реестра HKEY_CURRENT_USER\Console, на рабочем столе создается/открывается отдельное окно с текстовым режимом отображения содержимого;
  • [визуально] При запуске из проводника (explorer.exe), операционная система открывает окно консольного приложения, производит выполнение кода консольного приложения и затем закрывает окно консоли. То есть, если не предпринято дополнительных мер, окно консольного приложения очень быстро "промелькивает" на экране.
  • Имеется возможность создания приложения, которое функционально ограничено единственной библиотекой kernel32.dll (использование функции работы с консолью и сохранение совместимости между версиями Windows). Это позволяет отказаться от подключения библиотеки user32.dll (функции работы с окнами), что, в свою очередь, предотвращает загрузку серии зависимых библиотек на этапе подготовки образа к выполнению, тем самым обеспечивается создание более оптимизированных по использованию памяти приложений.

Следует ли из всего вышеперечисленного, что все функциональные методы консольного приложения "заключены" внутри набора текстовых функций и им лишь и ограничиваются? Отнюдь, основная идея заключается в том, что отличие оконного приложения от консольного чисто условное, поскольку консольное приложение сохраняет возможность вызывать GUI-функции (то есть функции, работающие уже с графическими примитивами) программного интерфейса Win32, все зависит лишь от набора подключаемых библиотек. Да, в простейшем случае текстовый интерфейс использует интерфейс командной строки, тем не менее многие приложения могут создавать более дружественный пользователю интерфейс при помощи интерактивных элементов, тем самым приближаясь по удобству к полноценному оконному (графическому).

Ограничение консольных приложений "текстовым" режимом в операционной системы Windows чисто условное, поскольку им доступны все базовые элементы управления, используемые и в графическом интерфейсе: кнопки, списки, меню, переключатели, флажки, полосы прокрутки и тому подобное.

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

Начиная с Windows 7, на системном уровне функционал консоли был вынесен из диспетчера csrss.exe и оформлен в качестве самостоятельных исполняемых образов:

  • conhost.exe – обработчик консольных окон режима пользователя (обеспечивает весь функционал работы с консолью);
  • condrv.sys – драйвер режима ядра, обеспечивающий взаимодействие conhost и консольных приложений;

Объекты консольного приложения

Выводить текст на консоль, осуществлять ввод символов, а так же совершать любые иные действия с консолью можно лишь ассоциировав с ней некие системные сущности (объекты), посредством которых можно обеспечивать обмен данными. Каждая консоль состоит из следующих основных объектов:

  • [единственный] входной буфер - область данных (события/сигналы/данные) для ввода (передачи на консоль);
  • [несколько] экранный выходной буфер - область данных (символы/атрибуты) для вывода (отображения на экране);
  • Окно консоли - область экрана, отображающая часть выходного буфера;
  • Текущая позиция курсора - маркера вывода, обозначающий текущую позицию вывода;

Стандартные потоки (дескрипторы консоли)

На программном уровне для ввода/вывода информации консольные приложения используют три основных стандартных устройства ввода-вывода:

Наименование Назначение
Стандартный ввод (stdin) Поток данных, идущих в программу.
Стандартный вывод (stdout) Поток данных, идущих из программы.
Стандартная ошибка (stderr) Поток сообщений об ошибках, идущих из программы.

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

Стандартный поток — объект процесса консольного приложения, предназначающийся для организации обмена данными.

Традиционно, со стандартным вводом ассоциирована клавиатура, а со стандартным выводом/ошибкой ассоциирован монитор (экран), таким образом вывод печатных символов в STDOUT и STDERR приводит к появлению этих символов на устройстве вывода и к получению их пользователем. В дополнение, потоки могут быть переопределены (перенаправлены) и на другие логические устройства (файл, ввод/вывод другой программы и прочее). Поэтому определение консольного приложения может быть расширено:

Любая программа, получающая данные путём чтения STDIN и передающая данные путём записи в STDOUT, является консольной.

Тем не менее возникает резонный вопрос: обязательно ли наличие окна консоли у консольного приложения? Ведь, теоретически, консольные программы могут обходиться и без классического ввода (с клавиатуры) и вывода (в окно, на экран), поскольку объекты stdin и stdout могут быть связаны с файлами, потоками ввода/вывода других программ или иными объектами операционной системы? Тем не менее, стандартный сценарий использования консольного приложения в Windows подразумевает создание отдельного окна консоли.
В ходе запуска консольного приложения, система генерирует вышеперечисленные дескрипторы для вновь создаваемого процесса консоли. Процесс консольного приложения обычно использует функции GetStdHandle, CreateFile, CreateConsoleScreenBuffer для того, чтобы открыть один из вышеописанных дескрипторов. Функция GetStdHandle обеспечивает механизм получения кодом приложения дескрипторов стандартного ввода, стандартного вывода и стандартной ошибки, связываемых с процессом в момент создания. В случае необходимости имеется возможность переназначить стандартные дескрипторы через функцию SetStdHandle, изменяющую дескрипторы, связанные с STDIN, STDOUT или STDERR.
Стандартные дескрипторы родительского процесса всегда наследуются всеми создаваемыми дочерними процессами, поэтому вызовы функции GetStdHandle дочерними процессами возвращают переназначенный дескриптор. По этой причине, в зависимости от действий родительского процесса, дескриптор, возвращенный функцией GetStdHandle, может сослаться на что-либо, отличное от привычного нам консольного ввода-вывода. К примеру, родительский процесс может при помощи SetStdHandle изменить дескриптор какого-либо потока (например STDIN) перед созданием дочернего процесса. Затем, когда созданный дочерний процесс у себя в коде вызовет функцию GetStdHandle, он получает дескриптор переназначенного канала. Этим обеспечивается механизм управления родительским процессом стандартными дескрипторами дочернего процесса.

К дескрипторам применяются права доступа. По умолчанию дескрипторы, возвращенные функцией GetStdHandle, имеют доступ GENERIC_READ | GENERIC_WRITE.

Буфер ввода

Каждое консольное приложение имеет буфер вводимых данных:

Входной буфер консоли (Input buffer) -- очередь данных, каждая запись которой содержит информацию относительно входного события консоли (нажатие/отпускание клавиш, движение/нажатие/отпускание мыши и прочие).

В момент, когда окно консольного приложения имеет фокус клавиатуры (является активным), консоль оформляет каждое событие ввода (типа нажатия/отпускания клавиши, перемещение указателя мыши или щелчка кнопки мыши) в качестве данных, которые помещаются в буфер вводимых данных консоли. Что такое эти самые данные? Запись данных о вводе ― структура Windows, содержащая в себе информацию о деталях события: тип, источник (клавиатура, мышь, размеры окна, фокус, меню) и прочих. Структура имеет внутренний тип INPUT_RECORD и представляет собой следующее:

По замыслу разработчиков, структуры фокуса и события меню помещены для внутреннего использования [системой] и не должны использоваться прикладными программами. Давайте попробуем более подробно рассмотреть поля описанной выше структуры:

Событие Описание
Событие клавиатуры Генерируется в момент, когда пользователь производит нажатие/отпускание клавиши клавиатуры (в окне); В опрос включаются все клавиши, в том числе и управляющие (комбинации с клавишей ALT и комбинация Ctrl+C имеет специальную обработку). При нажатии клавиши член KeyEvent (то же структура типа KEY_EVENT_RECORD) заполняется следующей информацией:

  • Поле, указывающее, была ли клавиша нажата или отпущена;
  • Счетчик повторений нажатия;
  • Код клавиши (аппаратно-независимое значение);
  • Сканкод клавиши (аппаратно-зависимое значение);
  • UNICODE- или ANSI-символ клавиши
  • Состояние управляющих клавиш (Alt, Ctrl, Shift, NumLock, ScrollLock и CapsLock) и клавиш "дополнительной" клавиатуры (Ins, Del, Home, End, PgUp, PgDn, , , , , /, Enter);
Событие мыши Генерируется в момент, когда пользователь перемещает курсор мыши или нажимает/отпускает кнопки мыши (в окне). При получении событий мыши член MouseEvent (то же структура типа MOUSE_EVENT_RECORD) заполняется следующей информацией:

  • Координаты курсора мыши (структура COORD, содержащая X:Y - строка:столбец символьного знакоместа);
  • Состояние кнопок мыши. Поле dwButtonState имеет битовое соответствие для каждой кнопки мыши. Бит = 1 - кнопка нажата, = 0 - кнопка отпущена.
  • Состояние управляющих клавиш (Alt, Ctrl, Shift, NumLock, ScrollLock и CapsLock) и клавиш "дополнительной" клавиатуры (Ins, Del, Home, End, PgUp, PgDn, , , , , /, Enter);
  • Флаги деталей события. Конкретизирует, было ли событие нажатием/отпусканием, событием перемещения курсора или двойным щелчком. В поле dwEventFlags событие отпускания/нажатия кнопки мыши регистрируется как значение 0/1;
Событие изменения размера буфера Генерируется в момент, когда пользователь через меню консольного окна меняет размер [активного] экранного буфера. Событие изменения размеров буфера размещаются только в том случае, если режим ввода данных консоли соответствует ENABLE_WINDOW_INPUT (по умолчанию). При получении события изменения размера буфера член WindowBufferSizeEvent (то же структура типа WINDOW_BUFFER_SIZE_RECORD) заполняется информацией:

  • Новый размер экранного буфера консоли. Поле dwSize является структурой типа COORD и содержит размер в столбцах и строках символьных знакомест.

Буфер вывода (экранный буфер) и окно

Следующими важными объектами консольного приложения являются окно консоли и экранный буфер консоли. Система создает эти буферы всякий раз, когда она создает новую консоль.

Экранный буфер консоли (Screen buffer) -- двумерный массив данных (состоящий из структур CHAR_INFO: символ (UNICODE|ASCII) + атрибут(цвет символа|фона и прч.)), которые могут быть отображены в окне консольного приложения.

Экранный буфер консоли представляет собой координатную сетку, размер которой измеряется в знакоместах символов. Ширина буфера равна числу знакомест в строке, высота ― числу строк. И окно и буфер вывода имеют исходные размеры [по умолчанию], которые могут изменяться при помощи специализированных функций Win32 API для работы с консолью. По умолчанию размеры, определенные в операционной системе Windows:

console buffer size

  • размер буфера - 80х300 символов (300 строк по 80 символов длиной каждая);
  • размер экрана - 80х25 символов (25 строк по 80 символов длиной каждая);

Получается примерно следующая картина:

буфер и окно

Соответственно, каждый экранный буфер консоли ограничен [виртуальным] окном, определяющим расположение и размер прямоугольной области буфера рамках консольного окна. Это окно определяется при помощи [задания] координат верхней левой и нижней правой ячеек (в символьных знакоместах). Для экранного буфера справедливы следующие утверждения:

  • Размер экранного буфера консоли должен быть больше или равен размера окна консоли;
  • В случае, когда консольный буфер больше (по размеру) чем окно консоли, в окне отрисовываются ползунки горизонтальной и/или вертикальной полосы прокрутки.

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

Активный экранный буфер (active screen buffer) ― тот, который отображается на экране.

Вновь созданный экранный буфер не активен до тех пор, пока его дескриптор не будет определен через вызов функции SetConsoleActiveScreenBuffer.

Зачем нам иметь несколько выходных консольных буферов? Выскажу собственное предположение. Смотрите, каждый буфер имеет собственный двухмерный массив записей текстовой информации и к экранным буферам можно обращаться для чтения и записи, независимо от того, являются ли они активными или неактивными. В этом случае обеспечивается интересный алгоритм, знакомый системным программистам еще со времен MSDOS: предварительная подготовка данных в неактивном буфере и последующее назначение его активным (переключение между буферами).

Основные свойства экранного буфера:

  • Размерность экранного буфера (в строках и столбцах знакомест (символов));
  • Атрибуты текста (цвет символа, цвет фона);
  • Размер/расположение окна (прямоугольная область, отображаемая в консольном окне);
  • Курсор (позиция, внешний вид и видимость);
  • Режимы вывода (ENABLE_PROCESSED_OUTPUT и ENABLE_WRAP_AT_EOL_OUTPUT);

Задать (поменять) буферу консоли требуемый размер можно при помощи функции SetConsoleScreenBufferSize. Имеется так же возможно создать новый консольный экранный буфер, для этого предоставляется функция CreateConsoleScreenBuffer. При этом надо помнить, что каждый из созданных таким образом буферов имеет собственную отображаемую область окна, определяемую координатами верхнего левого и нижнего правого знакомест (символьных ячеек). Для определения отображаемой в консольном окне области экранного буфера (в числе прочих параметров), используется функция GetConsoleScreenBufferInfo.

Размер буферов консоли, их количество, размеры окна, текстовые атрибуты и внешний вид курсора определяются системными значениями по умолчанию (содержатся в параметрах реестра системы).

При создании экранного буфера он заполнен символами пробела, курсор буфера видим на консоли и находится в начале координат буфера (позиция X,Y: 0,0), отображаемое окно консольного приложения устанавливается в верхний левый угол буфера (в начало его координат). Для получения текущих значений разнообразных свойств экранного буфера консоли, используйте функции GetConsoleScreenBufferInfo, GetConsoleCursorInfo и GetConsoleMode.

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

Курсор

Традиционно, одним из объектов консольного приложения является курсор. Курсор представляет собой элемент интерфейса, указывающий на текущую позицию ввода/вывода. Курсор экранного буфера может находиться в двух состояниях: видимый и скрытый. Вид курсора может варьироваться: от одной горизонтальной линии внизу ячейки знакоместа символа, до полностью заполненного знакоместа. Чтобы получить информацию об атрибутах курсора (внешний вид, видимость), используется функция GetConsoleCursorInfo. Для задания внешнего вида/видимости курсора используется функция SetConsoleCursorInfo.
Символы (группы символов), выводимые посредством высокоуровневых консольных функций ввода/вывода, записываются в текущее местоположение курсора, изменяя позицию курсора к следующему знакоместу (традиционно: смещая направо). Чтобы получить текущую позицию курсора в системе координат экранного буфера, используется функция GetConsoleScreenBufferInfo. Для установки позиции курсора в экранном буфере используется функция SetConsoleCursorPosition. Подобным образом можно управлять позиционированием текста, отображаемого на экране консольного приложения высокоуровневыми консольными функциями ввода/вывода.

Атрибуты курсора (внешний вид, позиция и видимость) определяются [независимо] для каждого экранного буфера.

Атрибуты символов

Мы уже знаем, что каждое знакоместо в экранном буфере консольного приложения является структурой типа CHAR_INFO, которая, в свою очередь, представляет из себя следующее:

или:

Как мы видим, одна часть структуры является кодом символа (Unicode или ASCII, в зависимости от настроек кодовой страницы), другая же часть определяет атрибуты. Под атрибуты знакоместа выделяется слово (два байта). Младший байт атрибутов, в свою очередь, описывает цвет текста и фона ячейки (знакоместа): 4 старших бита - фон, 4 младших бита - символ. Приложение может комбинировать константы цвета текста и фона, которые я привел в таблице, чтобы получать разные цвета:

Наименование константы Значение Описание
FOREGROUND_BLACK 00000000b Черный цвет символа
FOREGROUND_BLUE 00000001b Синий цвет символа
FOREGROUND_GREEN 00000010b Зеленый цвет символа
FOREGROUND_CYAN 00000011b Циан цвет символа
FOREGROUND_RED 00000100b Красный цвет символа
FOREGROUND_MAGENTA 00000101b Фиолетовый цвет символа
FOREGROUND_YELLOW 00000110b Желтый цвет символа
FOREGROUND_WHITE 00000111b Белый цвет символа
FOREGROUND_INTENSITY 00001000b Повышенная интенсивность цвета символа
BACKGROUND_BLACK 00000000b Черный цвет фона
BACKGROUND_BLUE 00010000b Синий цвет фона
BACKGROUND_GREEN 00100000b Зеленый цвет фона
BACKGROUND_CYAN 00110000b Циан цвет фона
BACKGROUND_RED 01000000b Красный цвет фона
BACKGROUND_MAGENTA 01010000b Фиолетовый цвет фона
BACKGROUND_YELLOW 01100000b Желтый цвет фона
BACKGROUND_WHITE 01110000b Белый цвет фона
BACKGROUND_INTENSITY 10000000b Повышенная интенсивность цвета фона

Соответственно, перенеся приведенные в таблице определения в исходник в виде констант, вы можете комбинировать значения, просто складывая их.

Если фон символов экранного буфера не задан, то фон по умолчанию черный, если константа цвета текста не определена, то текст по умолчанию белый.

Каждое знакоместо экранного буфера консольного приложения сохраняет атрибуты цвета текста и фона. Соответственно, код приложения может задавать цвет/фон для каждого знакоместа индивидуально, сохраняя информацию в поле Attributes структуры CHAR_INFO для каждой ячейки. Текущие (заданные последними) атрибуты текста экранного буфера используются высокоуровневыми функциями для выводимых впоследствии символов, но установка атрибутов экранного буфера не оказывает влияния на представление символов, записанных перед этим. Для получения информации об используемых атрибутах экранного буфера используется функция GetConsoleScreenBufferInfo, для установки текстовых атрибутов экранного буфера используется функция SetConsoleTextAttribute. Заданные данной функцией атрибуты не распространяются на символы, выводимые низкоуровневыми консольными функциями WriteConsoleOutput или WriteConsoleOutputCharacter, поскольку последние самостоятельно устанавливают атрибуты для каждой ячейки, в которую производится запись.

Основные функции

Общие функции:

  • SetConsoleMode -- задает режим работы консольного буфера ввода и режим консольного экранного буфера вывода;
  • SetConsoleTitle -- меняет название (текст в шапке) текущего консольного окна;
  • GetStdHandle -- возвращает описатель указанного стандартного устройства (стандартный ввод, стандартный вывод, стандартная ошибка);
  • SetConsoleCP -- задает кодовую страницу ввода для консоли, закрепленной за текущим процессом;
  • GetConsoleCP -- возвращает кодовую страницу ввода для консоли, закрепленной за текущим процессом;
  • SetConsoleOutputCP -- задает кодовую страницу вывода для консоли, закрепленной за текущим процессом;
  • GetConsoieOutputCP -- возвращает кодовую страницу вывода для консоли, закрепленной за текущим процессом;
  • SetConsoleTextAttribute -- изменение атрибутов (цвета) текста/фона.
  • FillConsoleOutputAttribute -- изменение атрибутов символов для заданного количества знакомест (начиная с заданных координат экранного буфера).
  • SetConsoleCursorPosition -- задает позицию курсора для указанного консольного экранного буфера.

Функции для работы с буфером:

  • WriteConsole -- выводит символы в выходной экранный буфер консоли (в текущую позицию курсора);
  • ReadConsole -- производит (фильтрованное) чтение символов из входного буфера консоли (удаляя эти данные из буфера);
  • ReadConsoleInput -- производит чтение данных из входного буфера консоли (удаляя эти данные из буфера);
  • SetConsoleActiveScreenBuffer -- назначение активного (текущего) экранного буфера консоли;
  • SetConsoleScreenBufferSize -- задание (изменение) размера экранного буфера консоли;
  • CreateConsoleScreenBuffer -- создания нового (собственного) консольного буфера;
  • GetConsoleScreenBufferInfo -- запрос параметров экранного буфера;
  • GetNumberOfConsoleInputEvents -- Получает количество непрочитанных входных записей во входном буфере консоли;
  • WriteConsoleOutput -- записывает символ и атрибут цвета в заданную прямоугольную область консольного экранного буфера;
  • WriteConsoleOutputCharacter -- копирование заданного числа символов в последовательно располагающиеся ячейки консольного экранного буфера;

Функции для работы с окном:

  • SetConsoleWindowInfo -- перемещение (прокрутка) позиции отображения, изменение размера видимой области в рамках окна консоли.
  • GetLargestConsoleWindowSize -- получение максимально-возможного размера консольного окна.

По замыслу разработчиков, функции работы с консольным приложением (консольные функции) подразделяются на несколько уровней доступа к консоли:

  • Высокоуровневые консольные функции: позволяют читать данные из стандартного потока ввода (работа с клавиатурой), записывать в стандартный поток вывода/ошибки (работа с экранным буфером), обеспечивают переадресацию стандартных дескрипторов/потоков [ввода/вывода], управляют режимами работы консоли (для обеспечения разнообразных функциональных возможностей ввода-вывода).
  • Низкоуровневые консольные функции: дают возможность получить подробный отчет о событиях ввода данных с клавиатуры/мыши, о событиях взаимодействия пользователя с консольным окном, обеспечивают расширенные возможности управления выводом данных на экран.

Пример 1: вывод строки и ожидание нажатия клавиши

Настало время перейти непосредственно к практической части вопроса, и в качестве стартового примера вы приведем простейшее консольное приложение, выводящее в консоль одну-единственную строку "Hello World!" и ожидающую ввода любого символа.

В отличии от других компиляторов исходных кодов языка Ассемблер, для FASM характерно довольно существенное преимущество, а именно включение всех опций компиляции непосредственно в исходный код приложения. Напомню, что на данном примере мы разрабатываем примитивное консольное приложение, соответственно, первое что нам надо сделать:

для указания компилятору на то, что требуется собрать консольное приложение, надо в исходном тексте задать для директивы format определение console (см. строку 1).

Итак, переходим к секции кода. Как мы помним из теории, приведенной в начале статьи, для выполнения ввода-вывода в окно консольного приложения, нам сперва надо получить дескриптор (описатель) стандартных потоков. Windows API для этого предоставляется функция GetStdHandle, прототип которой выглядит следующим образом:

Первым (и единственным) параметром функции, указывается идентификатор интересующего нас стандартного потока (ввода/вывода/ошибки). Стандартные системные идентификаторы (эквиваленты) уже определены в недрах операционной системы и, соответственно, для удобства разработчика описаны в файлах включения компилятора FASM:

Имя Размер Значение Назначение
STD_INPUT_HANDLE DWORD -10 Стандартный ВВОД. Изначально является консольным буфером ввода (ассоциирован с клавиатурой).
STD_OUTPUT_HANDLE DWORD -11 Стандартный ВЫВОД. Изначально является консольным буфером вывода (ассоциирован с экраном).
STD_ERROR_HANDLE DWORD -12 Стандартная ОШИБКА. Изначально является буфером для вывода ошибок (ассоциирован с экраном).

В строках 10 и 11 нашего исходника происходит запрос дескрипторов стандартного вывода и стандартного ввода соответственно. Полученные описатели, возвращаемые функцией в регистре EAX, сохраняются в локальных переменных (stdout, stdin).
Далее по коду (строка 14) располагается функция вывода текста в окно консоли WriteConsole. Эта функция более сложная, и тут у нас используется уже целых пять входных параметров (слева-направо):

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

После вывода строки Hello, world! на в окно консоли, у нас организовано ожидание ввода символа посредством функции ReadConsole (строка 15). Это сделано с одной-единственной целью - воспрепятствовать автоматическому закрытию окна, поскольку консольное окно после выполнения кода приложения обычно закрывается. Функция ReadConsole принимает на вход пять параметров (слева-направо):

  1. дескриптор стандартного потока ввода
  2. указатель на буфер, получающий данные из потока ввода
  3. количество символов для чтения
  4. Указатель на переменную, которая получает количество фактически считанных символов
  5. (необязательный) Указатель на структуру прототипа CONSOLE_READCONSOLE_CONTROL, которая получает контрольный символ для сигнализации об окончании операции чтения. В нашем случае не используется, поэтому NULL.
Функция ReadConsole является "обрезанной", поэтому она отфильтровывает данные из входного буфера: отбрасывает все данные за исключениям событий клавиатуры. Функция может работать с символами юникода, либо с 8-разрядными символами текущей кодовой страницы консоли.

И под конец секции кода у нас выполняется функция ExitProcess (строка 17) с единственным аргументом, имеющим значение 0. Она производит завершение вызывающего процесса и всех его потоков. При завершении процесса, закрепленная за процессом консоль освобождается автоматически.

Характерным отличием этого примера является автоматическое создание консоли самой операционной системой.

Пример 2: собственная консоль и размеры видимой области

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

Алгоритм приведенного выше исходного кода можно описать следующим образом:

  1. Отключим (освободим) консоль по-умолчанию, ассоциированную с процессом;
  2. Создадим (назначим) процессу новую "собственную" консоль;
  3. Получим параметры текущего консольного экранного буфера
  4. На основе размеров экранного буфера выставим размеры [видимого] окна консоли: размер (ширина, высота) видимой области консоли = размер (ширина, высота) буфера, деленный на 4.
  5. По завершению работы приложения освобождаем созданную ранее консоль.

Как было уже сказано, операционная система в ходе загрузки консольного приложения на выполнение, автоматически создает для него "консоль по умолчанию", однако за консольным приложением (в один момент времени) может быть закреплена единственная консоль, и если мы хотим создать собственную консоль, то автоматически созданную нам потребуется закрыть. И вот тут возникает ряд дополнительных нюансов.

На просторах Сети можно встретить рекомендацию: в случае необходимости работы с собственной консолью и отсутствия уверенности в том, наследовало ли приложение консоль (передается консоль приложению) или нет, предлагается сначала "отвязывать" процесс от текущей консоли функцией FreeConsole, а затем уже назначать собственную консоль вызывающему процессу при помощи функции AllocConsole.

Функция FreeConsole (строка 10) выполняет отключение вызывающего процесса от связанной с вызывающим процессом консоли. После чего наш код осуществляет создание "собственной" консоли посредством вызова функции AllocConsole (строка 11). Все, собственная консоль создана. Затем, при помощи функции GetConsoleScreenBufferInfo (строка 17), производится запрос параметров активного, ассоциированного с консольным приложением экранного буфера. Полученные параметры (размеры, позиция курсора, атрибуты, позиции углов прямоугольника, образуемого буфером, максимальные размеры консольного окна) сохраняются в структуре под названием lpConsoleScreenBufferInfo, которая описана в нашем приложении и имеет прототип CONSOLE_SCREEN_BUFFER_INFO (строки 55-61).
Далее, мы имеем намерение поменять видимую область, то есть прямоугольник, в котором отображается часть содержимого экранного буфера. Осуществим это при помощи функции SetConsoleWindowInfo (строка 28), которая в качестве параметра использует структуру типа SMALL_RECT (строки 48-53). Но с назначением видимой области надо быть предельно аккуратным, поскольку:

Отображаемая в консольном окне область буфера не может быть больше размеров самого буфера.

Функция SetConsoleWindowInfo не меняет позиций консольного окна на рабочем столе, она предназначается для перемещения (прокручивания) позиции и изменения размеров отображения (отображаемого в консольном окне участка экранного буфера). Таким образом эта функция может быть использована для типовой задачи, связанной с программированием консольных приложений: "прокрутки" содержимого консольного экранного буфера путем смещения позиции отображаемого прямоугольника. А вот для непосредственного перемещения окна по экрану рекомендуется использовать типовую функцию перемещения окна MoveWindow.
В финальной части программы мы видим вызов функции ожидания Sleep (применяется для организации задержки), сразу после которой происходит "освобождение" консоли посредством функции FreeConsole и последующего выхода из программы через ExitProcess.

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

Пример 3: "альтернативная" консоль (ввод/вывод через msvcrt)

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

Удивительное дело, куда же у нас исчезли все привычные по приведенным выше примерам WinAPI-функции создания/закрытия консоли, функции ввода/вывода в консоль? А они в данном примере попросту не используются, поскольку тут у нас задействован иной подход к работе с консолью. И организован он посредством функций библиотеки времени выполнения языка C (C Runtime), являющейся частью стандартной библиотеки C++ msvcrt.dll. Как Вы видите, используется функция форматированной печати printf, которая производит вывод подготовленной в соответствии со входным спецификатором строки в стандартный экранный буфер (stdout). Так же задействована функция _getch, ожидающая ввода символа пользователем. Не скажу, что функции C++ обладают меньшими возможностями по управлению текстом в консоли, однако писать при помощи них программу все же попроще, не так ли?

Для вызова функций, использующих соглашения о вызовах, принятые в языках C/C++, используется макрос cinvoke.

Пример 4: кодировка

А вы когда-нибудь пробовали вывести в окно Windows-консоли текст, состоящий из символов кириллицы? Я вот тут попробовал намедни, просто переписал фразу из первого примера статьи на наш великий и могучий русский язык, а вот что у меня из всего этого получилось вы можете наблюдать на этом вот снимке экрана:

кодировка консоли

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

Причина этого явления кроется в том, что разработчики в операционных системах Windows придумали для кириллицы новую кодировку CP1251, при этом (в целях совместимости) сохранив и старую со времен MSDOS - CP866.

Текстовые редакторы или среды разработки, которыми пользуется немалое количество специалистов (например, notepad++, редактор из состава Far Commander, блокнот (notepad) и многие другие) работают с текстом в кодировке ANSI, что русской локализации ОС эквивалентно кодовой странице 1251 (Windows-1251, CP1251). Тем не менее, для консоли действует следующее правило:

По умолчанию окно консоли настроено на работу с растровыми (точечными) шрифтами (raster fonts), которые правильно отображают лишь кодовую страницу OEM (она же CP866).

Получается, что стандартные потоки ввода-вывода окна консоли в Windows функционируют в кодировке OEM (DOS-OEM, OEM-866, что соответствует кодовой странице 866 (CP866) для русского языка)? Вероятно это действительно так, поскольку, например, в ветви реестра HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage параметр OEMCP имеет значение по умолчанию 866. При подобного рода различиях кодовых страниц в функциях WinAPI и консоли, то есть фактически кодировок исходных данных и стандартных консольных потоков, результат очевиден.
Имеется несколько методов решения данной проблемы:

  1. Сменить кодировку самого редактора, используемого вами для редактирования исходного кода, на OEM. И, конечно же, перенабрать все локализованные строки, содержащие кириллицу. Но тут есть один нюанс: в OEM-кодировке должны быть входные данные (файлы) для нашего приложения, и в этой же OEM-кодировке будут и все выходные данные.
  2. Использовать функции, задающие кодировку стандартных потоков ввода-вывода консоли, такие как SetConsoleOutputCP и SetConsoleCP. Но тут одной сменой кодировки в исходном тексте не обойтись и придется сделать дополнительное телодвижение: в свойствах окна консоли (например cmd) необходимо единожды выбрать любой TrueType-шрифт (доступные в свойствах окна: Consolas и Lucida Console).
  3. Конвертировать выводимые данные "на лету". В этом случае отпадает необходимость смены кодировки основного исходного текста приложения. Реализуется это при помощи функций CharToOem (если данные в 1251) и WideCharToMultiByte (если данные в UNICODE).

Почему присутствуют функции определения кодировки для входных и выходных потоков консоли? Это объясняет тем, что:

С каждой консолью операционная система ассоциирует две кодовые таблицы — для ввода и вывода.

Соответственно, кодовые таблицы используются следующим образом:

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

Ну а вот второй из вышеописанных способов давайте опишем на примере исходного кода:

Тут все достаточно просто, в коде выделены все критические места: вызовы функций SetConsoleOutputCP и SetConsoleCP, а так же набранный в соответствующей кодировке русский текст.

Комментарии: 3

  1. Victor

    На фасме код не компилиться ворчит

    1. Victor

      Версия 1.73.16
      Ругает format, section и импортную таблицу
      Что делать ?

      1. shofixti

        Копипаст ошибки сделайте. путь до include проверили?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *