В процессе изучения основных правил работы оконных приложений, мы познакомились с некоторыми принципами программирования графических примитивов. Теперь самое время открыть для себя еще одну разновидность приложений под Windows - консольные приложения.
Во времена разработки первых операционных систем, в них не было такого понятия как графический интерфейс пользователя, был доступен только текстовый режим видеоадаптера с интерфейсом командной строки. Однако, в ходе развития ОС был создан графический интерфейс пользователя (GUI), роль которого со временем существенно возросла. Как раз во время создания GUI стало очевидно, что необходимость в консольных приложениях сохраняется, следствием чего было создание консоли Windows. Консоль, в зависимости от ОС, может работать как в привычных по старым операционным системам текстовых режимах (они еще поддерживаются на аппаратном уровне в современных видеоадаптерах), так и в режимах эмуляции текстового режима. Основная причина по которой консоль в Windows была выделена в отдельную разработку и продолжает своё существование, заключается в следующем:
Как вы знаете, большинство упомянутых системных утилит мигрировали к нам из ранних версий операционной системы Windows, куда они, в свою очередь, заимствовались как из той же MSDOS (фактически предка Windows), так и из сторонних операционных систем. Соответственно, в старых ОС, до определенного времени, интерфейс взаимодействия с пользователем (командная строка) ограничивался текстовым режимом, по этой причине и разрабатывались утилиты исключительно под него. Казалось бы, при переходе операционных систем к графическому режиму, стоило бы переписывать и системные утилиты под новые реалии? Тем не менее это не имело ни малейшего смысла, поскольку системные утилиты представляют собой обособленную группу программ, предназначенных для работы в командных сценариях, своеобразных программах, которые используют простые языки описания действий, в которых вывод одной программы может поступать на вход другой. То есть вся работа зачастую ведется с минимальным выводом статусных сообщений, либо и вовсе без какого-либо визуального оповещения пользователя. Очевидно, что использование графического интерфейса тут явно не к месту (избыточно). Описанная выше так называемая "преемственность" консольных утилит имеет под собой ряд весомых причин:
- возможность (для взаимодействия) использовать стандартные потоки ввода-вывода (будут описаны ниже).
- наличие потоков позволяет использовать консольные утилиты в сценариях (скриптах), где выходные потоки одних утилит могут перенаправляться во входные потоки других.
- оптимизация приложения по скорости выполнения (скорости вывода информации) и потреблению системных ресурсов.
- удобство программирования, обусловленное простотой и небольшим набором функций обслуживания консоли.
Помимо класса консольных утилит, в практике разработчика часто возникают задачи, в которых разворачивать оконный графический интерфейс соразмерно напрасной трате своего и процессорного времени. В подобного рода задачах вполне достаточным условием является использование текстового (для вывода диагностических сообщений) или вовсе неинтерактивного (вывод данных в файл) режимов. Действительно, зачем пытаться изображать графический интерфейс там, где он явно излишен или вовсе не обязателен, не проще ли в таких задачах от него отказаться вовсе? Поэтому, сегодня мы акцентируем внимание на особенностях языка Ассемблера (FASM) при написании консольного приложения на ассемблере под Windows, и темой данной статьи будет создание серии простейших консольных приложений, демонстрирующих основные алгоритмы взаимодействия с консолью. Разработанные шаблоны (в примерах) могут быть в дальнейшем использованы в качестве базовых в различного рода проектах.
Итак, в словосочетании консольное приложение на ассемблере присутствует ключевое слово "консоль", на которое стоит обратить особое внимание, поскольку именно оно даст нам понимание основных принципов работы. Скорее всего, люди уже знакомые с компьютером в общем и операционными системами в частности, знают что это такое, хотя могут понимать это в широком, так сказать, смысле этого слова: механизм для ввода информации с клавиатуры и вывода ее на экран. Поэтому, не лишним будет дать серию расширенных определений:
что является базой для:
stdin
), вывод (stdout
), ошибка (stderr
), а так же прямыми чтением/записью из буферов ввода-вывода. По умолчанию к потоку ввода "подключена" клавиатура (мышь), а к потоку вывода — экран (монитора). В частном случае может быть представлена в виде бесконечной бумажной ленты, прокручивающейся в обе стороны.что, в свою очередь, формирует понятие:
Консольное приложение операционной системы Windows обеспечивает взаимодействие с пользователем через так называемое окно консоли. Примером подобных консольных приложений могут являться: окно командной строки (cmd), файловые менеджеры (например, Far Commander), и ряд типовых системных консольных утилит:
Различия оконного и консольного приложений
Консольные приложения являются одним из типов исполняемых образов (приложений) Windows, наряду с типовыми оконными (GUI), библиотеками (DLL), драйверами (native) и некоторыми другими. По сути это полноценные приложениями, имеющие ряд специфических отличий:
- Значение поля Subsystem заголовка результирующего PE-файла равно
3
(IMAGE_SUBSYSTEM_WINDOWS_CUI). На основании значения данного поля, загрузчик образов при подготовке приложения к выполнению производит свойственную консольным приложениям последовательность загрузки. - При запуске консольного приложения, загрузчик образов пытается наследовать консоль от процесса-родителя (приложение, из-под которого был произведен запуск):
- Создается новая консоль, если родительский процесс не имеет консоли.
- Консоль наследуется, если процесс-родитель тоже является консольным приложением (окно командной строки и прочее).
- Консоль создается/наследуется с тремя стандартными потоками (объектами) ввода/вывода, каждый из которых имеет свой описатель (для организации программного обмена).
- [визуально] На основе параметров в ветви реестра HKEY_CURRENT_USER\Console, на рабочем столе создается/открывается отдельное окно с текстовым режимом отображения содержимого;
- [визуально] При запуске из проводника (explorer.exe), операционная система открывает окно консольного приложения, производит выполнение кода консольного приложения и затем закрывает окно консоли. То есть, если не предпринято дополнительных мер, окно консольного приложения очень быстро "промелькивает" на экране.
- Имеется возможность создания приложения, которое функционально ограничено единственной библиотекой kernel32.dll (использование функции работы с консолью и сохранение совместимости между версиями Windows). Это позволяет отказаться от подключения библиотеки user32.dll (функции работы с окнами), что, в свою очередь, предотвращает загрузку серии зависимых библиотек на этапе подготовки образа к выполнению, тем самым обеспечивается создание более оптимизированных по использованию памяти приложений.
Следует ли из всего вышеперечисленного, что все функциональные методы консольного приложения "заключены" внутри набора текстовых функций и им лишь и ограничиваются? Отнюдь, основная идея заключается в том, что отличие оконного приложения от консольного чисто условное, поскольку консольное приложение сохраняет возможность вызывать GUI-функции (то есть функции, работающие уже с графическими примитивами) программного интерфейса Win32, все зависит лишь от набора подключаемых библиотек. Да, в простейшем случае текстовый интерфейс использует интерфейс командной строки, тем не менее многие приложения могут создавать более дружественный пользователю интерфейс при помощи интерактивных элементов, тем самым приближаясь по удобству к полноценному оконному (графическому).
Иначе говоря, программы с текстовым интерфейсом могут имитировать оконный интерфейс. Поэтому можно сделать следующее обобщение: консольные приложения имеют возможность полноценно взаимодействовать с функциями Win32 API наравне с типовым оконным GUI-приложением, ведь в процессе написания исходного кода автор имеет возможность импортировать (подключать) любые функции любых доступных коду библиотек, каковые он сочтет нужными.
- conhost.exe – обработчик консольных окон режима пользователя (обеспечивает весь функционал работы с консолью);
- condrv.sys – драйвер режима ядра, обеспечивающий взаимодействие conhost и консольных приложений;
Объекты консольного приложения
Выводить текст на консоль, осуществлять ввод символов, а так же совершать любые иные действия с консолью можно лишь ассоциировав с ней некие системные сущности (объекты), посредством которых можно обеспечивать обмен данными. Каждая консоль состоит из следующих основных объектов:
- [единственный] входной буфер - область данных (события/сигналы/данные) для ввода (передачи на консоль);
- [несколько] экранный выходной буфер - область данных (символы/атрибуты) для вывода (отображения на экране);
- Окно консоли - область экрана, отображающая часть выходного буфера;
- Текущая позиция курсора - маркера вывода, обозначающий текущую позицию вывода;
Стандартные потоки (дескрипторы консоли)
На программном уровне для ввода/вывода информации консольные приложения используют три основных стандартных устройства ввода-вывода:
Наименование | Назначение |
---|---|
Стандартный ввод (stdin ) |
Поток данных, идущих в программу. |
Стандартный вывод (stdout ) |
Поток данных, идущих из программы. |
Стандартная ошибка (stderr ) |
Поток сообщений об ошибках, идущих из программы. |
Для кода любого консольного приложения стандартные потоки доступны через дескрипторы и используют последние для того, чтобы обратиться к стандартному вводу (буферу ввода данных) и стандартному выводу (экранным буферам) собственной консоли. Хотя этими потоками консольное приложение не ограничено, никто не запрещает ему открывать любые файлы, использовать сетевые соединения и совершать иные действия, доступные в выполняющей их среде.
Традиционно, со стандартным вводом ассоциирована клавиатура, а со стандартным выводом/ошибкой ассоциирован монитор (экран), таким образом вывод печатных символов в STDOUT и STDERR приводит к появлению этих символов на устройстве вывода и к получению их пользователем. В дополнение, потоки могут быть переопределены (перенаправлены) и на другие логические устройства (файл, ввод/вывод другой программы и прочее). Поэтому определение консольного приложения может быть расширено:
Тем не менее возникает резонный вопрос: обязательно ли наличие окна консоли у консольного приложения? Ведь, теоретически, консольные программы могут обходиться и без классического ввода (с клавиатуры) и вывода (в окно, на экран), поскольку объекты stdin
и stdout
могут быть связаны с файлами, потоками ввода/вывода других программ или иными объектами операционной системы? Тем не менее, стандартный сценарий использования консольного приложения в Windows подразумевает создание отдельного окна консоли.
В ходе запуска консольного приложения, система генерирует вышеперечисленные дескрипторы для вновь создаваемого процесса консоли. Процесс консольного приложения обычно использует функции GetStdHandle, CreateFile, CreateConsoleScreenBuffer для того, чтобы открыть один из вышеописанных дескрипторов. Функция GetStdHandle обеспечивает механизм получения кодом приложения дескрипторов стандартного ввода, стандартного вывода и стандартной ошибки, связываемых с процессом в момент создания. В случае необходимости имеется возможность переназначить стандартные дескрипторы через функцию SetStdHandle, изменяющую дескрипторы, связанные с STDIN, STDOUT или STDERR.
Стандартные дескрипторы родительского процесса всегда наследуются всеми создаваемыми дочерними процессами, поэтому вызовы функции GetStdHandle дочерними процессами возвращают переназначенный дескриптор. По этой причине, в зависимости от действий родительского процесса, дескриптор, возвращенный функцией GetStdHandle, может сослаться на что-либо, отличное от привычного нам консольного ввода-вывода. К примеру, родительский процесс может при помощи SetStdHandle изменить дескриптор какого-либо потока (например STDIN) перед созданием дочернего процесса. Затем, когда созданный дочерний процесс у себя в коде вызовет функцию GetStdHandle, он получает дескриптор переназначенного канала. Этим обеспечивается механизм управления родительским процессом стандартными дескрипторами дочернего процесса.
Буфер ввода
Каждое консольное приложение имеет буфер вводимых данных:
В момент, когда окно консольного приложения имеет фокус клавиатуры (является активным), консоль оформляет каждое событие ввода (типа нажатия/отпускания клавиши, перемещение указателя мыши или щелчка кнопки мыши) в качестве данных, которые помещаются в буфер вводимых данных консоли. Что такое эти самые данные? Запись данных о вводе ― структура Windows, содержащая в себе информацию о деталях события: тип, источник (клавиатура, мышь, размеры окна, фокус, меню) и прочих. Структура имеет внутренний тип INPUT_RECORD
и представляет собой следующее:
1 2 3 4 5 6 7 8 9 10 11 |
struct INPUT_RECORD EventType dw ? dw ? union KeyEvent KEY_EVENT_RECORD MouseEvent MOUSE_EVENT_RECORD WindowBufferSizeEvent WINDOW_BUFFER_SIZE_RECORD MenuEvent MENU_EVENT_RECORD FocusEvent FOCUS_EVENT_RECORD ends ends |
По замыслу разработчиков, структуры фокуса и события меню помещены для внутреннего использования [системой] и не должны использоваться прикладными программами. Давайте попробуем более подробно рассмотреть поля описанной выше структуры:
Событие | Описание |
---|---|
Событие клавиатуры | Генерируется в момент, когда пользователь производит нажатие/отпускание клавиши клавиатуры (в окне); В опрос включаются все клавиши, в том числе и управляющие (комбинации с клавишей ALT и комбинация Ctrl+C имеет специальную обработку). При нажатии клавиши член KeyEvent (то же структура типа KEY_EVENT_RECORD ) заполняется следующей информацией:
|
Событие мыши | Генерируется в момент, когда пользователь перемещает курсор мыши или нажимает/отпускает кнопки мыши (в окне). При получении событий мыши член MouseEvent (то же структура типа MOUSE_EVENT_RECORD ) заполняется следующей информацией:
|
Событие изменения размера буфера | Генерируется в момент, когда пользователь через меню консольного окна меняет размер [активного] экранного буфера. Событие изменения размеров буфера размещаются только в том случае, если режим ввода данных консоли соответствует ENABLE_WINDOW_INPUT (по умолчанию). При получении события изменения размера буфера член WindowBufferSizeEvent (то же структура типа WINDOW_BUFFER_SIZE_RECORD ) заполняется информацией:
|
Буфер вывода (экранный буфер) и окно
Следующими важными объектами консольного приложения являются окно консоли и экранный буфер консоли. Система создает эти буферы всякий раз, когда она создает новую консоль.
CHAR_INFO
: символ (UNICODE|ASCII) + атрибут(цвет символа|фона и прч.)), которые могут быть отображены в окне консольного приложения.Экранный буфер консоли представляет собой координатную сетку, размер которой измеряется в знакоместах символов. Ширина буфера равна числу знакомест в строке, высота ― числу строк. И окно и буфер вывода имеют исходные размеры [по умолчанию], которые могут изменяться при помощи специализированных функций Win32 API для работы с консолью. По умолчанию размеры, определенные в операционной системе Windows:
- размер буфера - 80х300 символов (300 строк по 80 символов длиной каждая);
- размер экрана - 80х25 символов (25 строк по 80 символов длиной каждая);
Получается примерно следующая картина:
Соответственно, каждый экранный буфер консоли ограничен [виртуальным] окном, определяющим расположение и размер прямоугольной области буфера рамках консольного окна. Это окно определяется при помощи [задания] координат верхней левой и нижней правой ячеек (в символьных знакоместах). Для экранного буфера справедливы следующие утверждения:
- Размер экранного буфера консоли должен быть больше или равен размера окна консоли;
- В случае, когда консольный буфер больше (по размеру) чем окно консоли, в окне отрисовываются ползунки горизонтальной и/или вертикальной полосы прокрутки.
По умолчанию за консольным приложением закреплен один экранный буфер, однако количество этих буферов может быть увеличено. Для создания дополнительных буферов в коде программы может использоваться функция CreateConsoleScreenBuffer. Тем не менее, при наличии множества буферов возникает проблема выбора текущего, поскольку в один момент времени содержимое лишь одного из них может отображаться в окне консоли. Отображаемый буфер называют активным экранным буфером (при этом другие являются неактивными).
Вновь созданный экранный буфер не активен до тех пор, пока его дескриптор не будет определен через вызов функции SetConsoleActiveScreenBuffer.
Основные свойства экранного буфера:
- Размерность экранного буфера (в строках и столбцах знакомест (символов));
- Атрибуты текста (цвет символа, цвет фона);
- Размер/расположение окна (прямоугольная область, отображаемая в консольном окне);
- Курсор (позиция, внешний вид и видимость);
- Режимы вывода (ENABLE_PROCESSED_OUTPUT и ENABLE_WRAP_AT_EOL_OUTPUT);
Задать (поменять) буферу консоли требуемый размер можно при помощи функции SetConsoleScreenBufferSize. Имеется так же возможно создать новый консольный экранный буфер, для этого предоставляется функция CreateConsoleScreenBuffer. При этом надо помнить, что каждый из созданных таким образом буферов имеет собственную отображаемую область окна, определяемую координатами верхнего левого и нижнего правого знакомест (символьных ячеек). Для определения отображаемой в консольном окне области экранного буфера (в числе прочих параметров), используется функция GetConsoleScreenBufferInfo.
При создании экранного буфера он заполнен символами пробела, курсор буфера видим на консоли и находится в начале координат буфера (позиция X,Y: 0,0), отображаемое окно консольного приложения устанавливается в верхний левый угол буфера (в начало его координат). Для получения текущих значений разнообразных свойств экранного буфера консоли, используйте функции GetConsoleScreenBufferInfo, GetConsoleCursorInfo и GetConsoleMode.
Курсор
Традиционно, одним из объектов консольного приложения является курсор. Курсор представляет собой элемент интерфейса, указывающий на текущую позицию ввода/вывода. Курсор экранного буфера может находиться в двух состояниях: видимый и скрытый. Вид курсора может варьироваться: от одной горизонтальной линии внизу ячейки знакоместа символа, до полностью заполненного знакоместа. Чтобы получить информацию об атрибутах курсора (внешний вид, видимость), используется функция GetConsoleCursorInfo. Для задания внешнего вида/видимости курсора используется функция SetConsoleCursorInfo.
Символы (группы символов), выводимые посредством высокоуровневых консольных функций ввода/вывода, записываются в текущее местоположение курсора, изменяя позицию курсора к следующему знакоместу (традиционно: смещая направо). Чтобы получить текущую позицию курсора в системе координат экранного буфера, используется функция GetConsoleScreenBufferInfo. Для установки позиции курсора в экранном буфере используется функция SetConsoleCursorPosition. Подобным образом можно управлять позиционированием текста, отображаемого на экране консольного приложения высокоуровневыми консольными функциями ввода/вывода.
Атрибуты символов
Мы уже знаем, что каждое знакоместо в экранном буфере консольного приложения является структурой типа CHAR_INFO
, которая, в свою очередь, представляет из себя следующее:
1 2 3 4 5 6 7 |
typedef struct _CHAR_INFO { union { WCHAR UnicodeChar; CHAR AsciiChar; } Char; WORD Attributes; } CHAR_INFO, *PCHAR_INFO; |
или:
1 2 3 4 5 6 7 8 |
struct CHAR AsciiChar db ?,? ends struct CHAR_INFO Char CHAR Attributes dw ? ends |
Как мы видим, одна часть структуры является кодом символа (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!" и ожидающую ввода любого символа.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
format PE console 4.0 ; Консольное приложение. entry start ; Точка входа include '%fasminc%\win32a.inc' ; Делаем стандартное включение описателей. ;--- секция кода --- section '.text' code readable executable start: invoke GetStdHandle, STD_OUTPUT_HANDLE mov [stdout], eax invoke GetStdHandle, STD_INPUT_HANDLE mov [stdin], eax invoke WriteConsole,[stdout],cMsg,13,NULL,NULL invoke ReadConsole,[stdin],lpBuffer,1,lpCharsRead,NULL exit: invoke ExitProcess, 0 ;--- секция данных --- section '.data' data readable writeable cMsg db 'Hello, world!' ; Текстовая строка. lpBuffer db 10 dup (0) lpCharsRead dd ? ; Количество фактически считанных символов stdin dd ? stdout dd ? ;--- секция (таблица) импорта --- section '.idata' import data readable writeable library kernel32,'KERNEL32.DLL' import kernel32,\ GetStdHandle,'GetStdHandle',\ WriteConsole,'WriteConsoleA',\ ReadConsole,'ReadConsoleA',\ ExitProcess,'ExitProcess' |
В отличии от других компиляторов исходных кодов языка Ассемблер, для FASM характерно довольно существенное преимущество, а именно включение всех опций компиляции непосредственно в исходный код приложения. Напомню, что на данном примере мы разрабатываем примитивное консольное приложение, соответственно, первое что нам надо сделать:
format
определение console (см. строку 1
).Итак, переходим к секции кода. Как мы помним из теории, приведенной в начале статьи, для выполнения ввода-вывода в окно консольного приложения, нам сперва надо получить дескриптор (описатель) стандартных потоков. Windows API для этого предоставляется функция GetStdHandle, прототип которой выглядит следующим образом:
1 2 3 |
HANDLE WINAPI GetStdHandle( _In_ DWORD nStdHandle ); |
Первым (и единственным) параметром функции, указывается идентификатор интересующего нас стандартного потока (ввода/вывода/ошибки). Стандартные системные идентификаторы (эквиваленты) уже определены в недрах операционной системы и, соответственно, для удобства разработчика описаны в файлах включения компилятора 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 6 7 |
BOOL WINAPI WriteConsole( _In_ HANDLE hConsoleOutput, _In_ const VOID *lpBuffer, _In_ DWORD nNumberOfCharsToWrite, _Out_ LPDWORD lpNumberOfCharsWritten, _Reserved_ LPVOID lpReserved ); |
- дескриптор стандартного потока вывода
- указатель на строку выводимых (печатаемых) символов
- число печатаемых символов
- указатель на локальную переменную, получающую число фактически выведенных символов
- зарезервированный параметр, резерв (зарезервировано для следующих версий);
После вывода строки Hello, world! на в окно консоли, у нас организовано ожидание ввода символа посредством функции ReadConsole (строка 15). Это сделано с одной-единственной целью - воспрепятствовать автоматическому закрытию окна, поскольку консольное окно после выполнения кода приложения обычно закрывается. Функция ReadConsole принимает на вход пять параметров (слева-направо):
1 2 3 4 5 6 7 |
BOOL WINAPI ReadConsole( _In_ HANDLE hConsoleInput, _Out_ LPVOID lpBuffer, _In_ DWORD nNumberOfCharsToRead, _Out_ LPDWORD lpNumberOfCharsRead, _In_opt_ LPVOID pInputControl ); |
- дескриптор стандартного потока ввода
- указатель на буфер, получающий данные из потока ввода
- количество символов для чтения
- Указатель на переменную, которая получает количество фактически считанных символов
- (необязательный) Указатель на структуру прототипа CONSOLE_READCONSOLE_CONTROL, которая получает контрольный символ для сигнализации об окончании операции чтения. В нашем случае не используется, поэтому NULL.
И под конец секции кода у нас выполняется функция ExitProcess (строка 17
) с единственным аргументом, имеющим значение 0. Она производит завершение вызывающего процесса и всех его потоков. При завершении процесса, закрепленная за процессом консоль освобождается автоматически.
Пример 2: собственная консоль и размеры видимой области
В случае с консольным приложением из предыдущего примера, окно консоли создается и отображается системой в автоматическом режиме. Но что бы получить возможность гибко управлять различными аспектами консоли, нам потребуется создать так называемую собственную консоль, и в данном примере мы будем попытаемся это реализовать.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
format PE console 4.0 include '%fasminc%\win32ax.inc' entry start ;--- секция кода --- section '.code' code readable executable start: invoke FreeConsole ; Освободить существующую консоль invoke AllocConsole ; Назначить новую консоль invoke GetStdHandle, STD_OUTPUT_HANDLE ; Получить дескриптор стандартного потока вывода mov [stdout], eax ; Сохранить его invoke GetStdHandle, STD_INPUT_HANDLE ; Получить дескриптор стандартного потока ввода mov [stdin], eax ; Сохранить его invoke GetConsoleScreenBufferInfo,[stdout],lpConsoleScreenBufferInfo mov [ConsoleWindow.Left], 0 mov [ConsoleWindow.Right], 0 mov ax, word [lpConsoleScreenBufferInfo.srWindow.Right] shr ax,2 mov word [ConsoleWindow.Right], ax mov ax, word [lpConsoleScreenBufferInfo.srWindow.Bottom] shr ax,2 mov word [ConsoleWindow.Bottom], ax invoke SetConsoleWindowInfo,[stdout],TRUE,ConsoleWindow invoke Sleep, 10000 invoke FreeConsole exit: invoke ExitProcess, 0 ;--- секция данных --- section '.data' data readable writeable stdout dd ? stdin dd ? struct COORD x dw ? y dw ? ends struct SMALL_RECT Left dw ? Top dw ? Right dw ? Bottom dw ? ends struct CONSOLE_SCREEN_BUFFER_INFO dwSize COORD ; размеры буфера в строках и столбцах символов dwCursorPosition COORD ; координаты (позиция) курсора в буфере wAttributes dw ? ; атрибуты символов srWindow SMALL_RECT ; координаты верхнего левого и нижнего правого углов буфера dwMaximumWindowSize COORD ; максимальные размеры консольного окна ends lpConsoleScreenBufferInfo CONSOLE_SCREEN_BUFFER_INFO ConsoleWindow SMALL_RECT dwSize COORD ;--- секция импорта --- section '.idata' import data readable writeable library kernel32,'KERNEL32.DLL' import kernel32,\ AllocConsole,'AllocConsole',\ GetConsoleScreenBufferInfo,'GetConsoleScreenBufferInfo',\ SetConsoleWindowInfo,'SetConsoleWindowInfo',\ FreeConsole,'FreeConsole',\ GetStdHandle,'GetStdHandle',\ Sleep,'Sleep',\ ExitProcess,'ExitProcess' |
Алгоритм приведенного выше исходного кода можно описать следующим образом:
- Отключим (освободим) консоль по-умолчанию, ассоциированную с процессом;
- Создадим (назначим) процессу новую "собственную" консоль;
- Получим параметры текущего консольного экранного буфера
- На основе размеров экранного буфера выставим размеры [видимого] окна консоли: размер (ширина, высота) видимой области консоли = размер (ширина, высота) буфера, деленный на 4.
- По завершению работы приложения освобождаем созданную ранее консоль.
Как было уже сказано, операционная система в ходе загрузки консольного приложения на выполнение, автоматически создает для него "консоль по умолчанию", однако за консольным приложением (в один момент времени) может быть закреплена единственная консоль, и если мы хотим создать собственную консоль, то автоматически созданную нам потребуется закрыть. И вот тут возникает ряд дополнительных нюансов.
Функция FreeConsole (строка 10
) выполняет отключение вызывающего процесса от связанной с вызывающим процессом консоли. После чего наш код осуществляет создание "собственной" консоли посредством вызова функции AllocConsole (строка 11
). Все, собственная консоль создана. Затем, при помощи функции GetConsoleScreenBufferInfo (строка 17
), производится запрос параметров активного, ассоциированного с консольным приложением экранного буфера. Полученные параметры (размеры, позиция курсора, атрибуты, позиции углов прямоугольника, образуемого буфером, максимальные размеры консольного окна) сохраняются в структуре под названием lpConsoleScreenBufferInfo, которая описана в нашем приложении и имеет прототип CONSOLE_SCREEN_BUFFER_INFO (строки 55
-61
).
Далее, мы имеем намерение поменять видимую область, то есть прямоугольник, в котором отображается часть содержимого экранного буфера. Осуществим это при помощи функции SetConsoleWindowInfo (строка 28
), которая в качестве параметра использует структуру типа SMALL_RECT (строки 48
-53
). Но с назначением видимой области надо быть предельно аккуратным, поскольку:
Функция SetConsoleWindowInfo не меняет позиций консольного окна на рабочем столе, она предназначается для перемещения (прокручивания) позиции и изменения размеров отображения (отображаемого в консольном окне участка экранного буфера). Таким образом эта функция может быть использована для типовой задачи, связанной с программированием консольных приложений: "прокрутки" содержимого консольного экранного буфера путем смещения позиции отображаемого прямоугольника. А вот для непосредственного перемещения окна по экрану рекомендуется использовать типовую функцию перемещения окна MoveWindow.
В финальной части программы мы видим вызов функции ожидания Sleep (применяется для организации задержки), сразу после которой происходит "освобождение" консоли посредством функции FreeConsole и последующего выхода из программы через ExitProcess.
Пример 3: "альтернативная" консоль (ввод/вывод через msvcrt)
Зачастую при написании приложения у нас нет необходимости во всех этих красочных возможностях по управлению консолью, не надо задавать цвет текста/фона, управлять выводом по координатам знакомест, переключать экранные буфера, выводить форматированный текст и пользоваться иными расширенными возможностями консоли в Windows. А стоит перед нами простая задача совсем иного рода, например, организовать элементарный вывод на экран в каком-нибудь просмотрщике файлов. Для подобных случаев в системе Windows есть отличная альтернатива описанным выше консольным функциям:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
format PE console 4.0 include '%fasminc%\win32ax.inc' entry start CR = 0Dh LF = 0Ah ;--------------------------------------------------------------------------- section '.text' code readable executable start: cinvoke printf,outfmt,msg1 ; вывод сообщения cinvoke printf,outfmt,msg2 ; вывод сообщения cinvoke _getch exit: invoke ExitProcess, 0 ;----------------------------------------------------------------------------------- section '.data' data readable writeable msg1 db 'hello, world!!',CR,LF,0 ; строка для вывода в экранный буфер msg2 db 'press any key to exit',CR,LF,0 ; строка для вывода в экранный буфер outfmt db '%s',0 ; строка символов ;----------------------------------------------------------------------------------- section '.idata' import data readable library kernel32,'KERNEL32.DLL',\ msvcrt,'MSVCRT.DLL' include '%fasminc%\api\kernel32.inc' import msvcrt,\ printf,'printf',\ _getch,'_getch' |
Удивительное дело, куда же у нас исчезли все привычные по приведенным выше примерам WinAPI-функции создания/закрытия консоли, функции ввода/вывода в консоль? А они в данном примере попросту не используются, поскольку тут у нас задействован иной подход к работе с консолью. И организован он посредством функций библиотеки времени выполнения языка C (C Runtime), являющейся частью стандартной библиотеки C++ msvcrt.dll. Как Вы видите, используется функция форматированной печати printf, которая производит вывод подготовленной в соответствии со входным спецификатором строки в стандартный экранный буфер (stdout). Так же задействована функция _getch, ожидающая ввода символа пользователем. Не скажу, что функции C++ обладают меньшими возможностями по управлению текстом в консоли, однако писать при помощи них программу все же попроще, не так ли?
Пример 4: кодировка
А вы когда-нибудь пробовали вывести в окно Windows-консоли текст, состоящий из символов кириллицы? Я вот тут попробовал намедни, просто переписал фразу из первого примера статьи на наш великий и могучий русский язык, а вот что у меня из всего этого получилось вы можете наблюдать на этом вот снимке экрана:
Становится очевидным, что в консольных приложениях существует проблема кодировки символов при использовании национальных алфавитов.
Текстовые редакторы или среды разработки, которыми пользуется немалое количество специалистов (например, notepad++, редактор из состава Far Commander, блокнот (notepad) и многие другие) работают с текстом в кодировке ANSI, что русской локализации ОС эквивалентно кодовой странице 1251 (Windows-1251, CP1251). Тем не менее, для консоли действует следующее правило:
Получается, что стандартные потоки ввода-вывода окна консоли в Windows функционируют в кодировке OEM (DOS-OEM, OEM-866, что соответствует кодовой странице 866 (CP866) для русского языка)? Вероятно это действительно так, поскольку, например, в ветви реестра HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage параметр OEMCP
имеет значение по умолчанию 866. При подобного рода различиях кодовых страниц в функциях WinAPI и консоли, то есть фактически кодировок исходных данных и стандартных консольных потоков, результат очевиден.
Имеется несколько методов решения данной проблемы:
- Сменить кодировку самого редактора, используемого вами для редактирования исходного кода, на OEM. И, конечно же, перенабрать все локализованные строки, содержащие кириллицу. Но тут есть один нюанс: в OEM-кодировке должны быть входные данные (файлы) для нашего приложения, и в этой же OEM-кодировке будут и все выходные данные.
- Использовать функции, задающие кодировку стандартных потоков ввода-вывода консоли, такие как SetConsoleOutputCP и SetConsoleCP. Но тут одной сменой кодировки в исходном тексте не обойтись и придется сделать дополнительное телодвижение: в свойствах окна консоли (например cmd) необходимо единожды выбрать любой TrueType-шрифт (доступные в свойствах окна: Consolas и Lucida Console).
- Конвертировать выводимые данные "на лету". В этом случае отпадает необходимость смены кодировки основного исходного текста приложения. Реализуется это при помощи функций CharToOem (если данные в 1251) и WideCharToMultiByte (если данные в UNICODE).
Почему присутствуют функции определения кодировки для входных и выходных потоков консоли? Это объясняет тем, что:
Соответственно, кодовые таблицы используются следующим образом:
- Входная кодовая таблица: для трансляции ввода с клавиатуры в соответствующее символьное значение.
- Выходная кодовая таблица: для перекодировки кодов символов, поступающих в качестве входных параметров различных функций вывода, в символ, отображаемый в окне консоли (на экране).
Ну а вот второй из вышеописанных способов давайте опишем на примере исходного кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
format PE console 4.0 ; Консольное приложение. entry start ; Точка входа include '%fasminc%\win32a.inc' ; Делаем стандартное включение описателей. include '%fasminc%\ENCODING\WIN1251.INC' ; Подключить кодировку WIN1251 ;--- секция кода --- section '.text' code readable executable start: invoke SetConsoleOutputCP,1251 invoke SetConsoleCP,1251 invoke GetStdHandle, STD_OUTPUT_HANDLE mov [stdout], eax invoke GetStdHandle, STD_INPUT_HANDLE mov [stdin], eax invoke WriteConsole,[stdout],cMsg,12,NULL,NULL invoke ReadConsole,[stdin],lpBuffer,1,lpCharsRead,NULL exit: invoke ExitProcess, 0 ;--- секция данных --- section '.data' data readable writeable cMsg db 'Привет, мир!' ; Текстовая строка. lpBuffer db 10 dup (0) lpCharsRead dd ? ; Количество фактически считанных символов stdin dd ? stdout dd ? ;--- секция (таблица) импорта --- section '.idata' import data readable writeable library kernel32,'KERNEL32.DLL' import kernel32,\ GetStdHandle,'GetStdHandle',\ SetConsoleOutputCP,'SetConsoleOutputCP',\ SetConsoleCP,'SetConsoleCP',\ WriteConsole,'WriteConsoleA',\ ReadConsole,'ReadConsoleA',\ ExitProcess,'ExitProcess' |
Тут все достаточно просто, в коде выделены все критические места: вызовы функций SetConsoleOutputCP и SetConsoleCP, а так же набранный в соответствующей кодировке русский текст.
На фасме код не компилиться ворчит
Версия 1.73.16
Ругает format, section и импортную таблицу
Что делать ?
Копипаст ошибки сделайте. путь до include проверили?