Современную консоль можно рассматривать как франшизу на DOS. В документации Microsoft она числится в розысках под кличкой CUI, или "Console User Interface". В отличии от программ с графическим фейсом GUI, консольные приложения выполняются намного быстрее и абсолютно не требовательны к системным ресурсам, что привлекает внимание программистов всех мастей. В софтвейрном боксе этого интерфейса есть где развернуться – оперировать им позволяет набор из 65 специальных функций под общим названием
В данной статье мы попробуем сломать устоявшиеся стереотипы и рассмотрим немного продвинутые методы программирования консольных окон, а в качестве пруфа РОС напишем небольшое приложение, в котором первая половина окна будет статичной (в данном случае покажем информацию о физ.памяти системы), а во-вторую часть будем динамически выводить дамп содержимого всей пользовательской памяти. Трюк известен как "прокрутка (скроллинг) произвольной части экранного буфера консоли".
Под капотом:
1. История консоли Win
Чтобы у читателя не возникло лишних вопросов, для начала пройдём факультативный курс и ознакомимся с основными терминами этого направления. Обратив историю вспять и вернувшись на несколько десятилетий в прошлое, нужно будет рассмотреть архитектурную составляющую терминала Win – это фундаментальная база, без которой прогресс развития современной консоли останется за толстой стеной. Значит если коротко..
На заре компьютерной эры, терминалом назывался набор устройств, при помощи которых оператор управлял основной ЭВМ исполинских размеров. Как правило это были телетайпы TTY (дословно Tele-TYpe), со скоростью передачи 100 bps (10-симв/сек). Позже появились более совершенные девайсы класса VT100. Они были оснащены уже встроенным процессором i8080 для решения элементарных задач, примитивным дисплеем и клавиатурой – скорость сразу возросла до 1900 символов/сек. Их ещё называли интеллектуальным, унижая достоинства только появившихся на свет глупых "Dumb-терминалов".
Для передачи команд хосту, VT100 так-же использовал обычные символы ASCII, но добавились и управляющие Escape-последовательности, которыми заправлял институт ANSI (American National Standards Institute). Управляющие коды позволяли активно использовать графические возможности дисплея – это мерцание текста, смена шрифта на жирный, инверсия цветов, подчёркивание, перемещение курсора в произвольную область, и многое другое. Все эти надстройки хранились во-встроенной памяти процессора VT100, а набор его управляющих Esc-кодов и по сей-день является стандартом для всех эмуляторов терминалов, вплоть до Win-10.
В наше время, всё это хозяйство отправилось уже на свалку истории. Теперь компьютеры имеют встроенную видеокарту и контролёр клавиатуры, однако работа проприетарного терминала по-прежнему эмулируется средствами ОС, позволяя работать в текстовом режиме, когда в форточках Win нет особой нужды. В системной архитектуре к эмуляторам привязан термин PTY, или псевдо-терминал. Он представляет собой пару виртуальных устройств Master/Slave. Ведомый Slave программно эмулирует TTY, а Master – это наш консольный софт, который и управляет ведомым псевдо-девайсом.
• Командная строка – парсер символов, роль которого в MS-DOS исполнял символьный процессор Сommand.com, а на системах с графическим фейсом Win он мутировал в файл cmd.exe. Интерпретатор ком-строки проверяет юзерский ввод и исполняет связанные с ним команды. В 2006 году Microsoft представила объектно-ориентированную оболочку ком-строки под названием "PowerShell".
• Консоль – инфраструктура Windows для работающих в текстовом режиме TUI приложений. Каждый экземпляр консоли имеет свой буфер ввода/вывода. Системы XP и Win10 могут менять режим видеоадаптера с графического на текстовый VGA, что позволяет комбинацией клавиш [Alt+Enter] раскрывать консоль на весь экран. В полноэкранной реализации, Win10 задействует собственную подсистему рендеринга шрифтов, поэтому консоль может иметь столько символов в строке, сколько вмещает экран. Этим не может похвастаться XP, где имеется ограничение в 25-строк по 80-символов в каждой. Системы Vista,7,8 не поддерживают полноэкранные режимы консоли.
2. Консольные буферы ввода/вывода
Как упоминалось выше, к каждой консоли (а их может быть сразу несколько) привязываются собственные буферы ввода-вывода. На физическом уровне, буфер представляет собой обычную структуру в виртуальной памяти системы. Экземпляр консоли может иметь только один буфер-ввода с клавиатуры, зато неограниченное число буферов-вывода на экран. Мы можем создавать доп.буферы вывода функцией CreateConsoleScreenBuffer() и заполнять их разной информацией. Однако в каждый момент времени только один из них может быть активным, который выбирается через SetConsoleActiveScreenBuffer(). В то-же время, к буферам можно обращаться на RW в любое время, хоть активный он, хоть нет.
Экранный буфер-вывода (ScreenBuff) оформлен в памяти в виде "двумерной матрицы символов". Кол-во символов в одной строке пропорционально размеру самого буфера, изменить который можно функцией SetConsoleScreenBufferSize(). Под каждый символ в этой матрице отводится по 4-байта – два на символ, два на его атрибут (цвет-фон). Эту единицу описывает структура CHAR_INFO:
По умолчанию консоль работает в режиме ANSI, где код символа кодируется одним байтом. Однако изменением кодовой страницы, консоль можно перевести и в режим Unicode, где каждый символ кодируется уже двумя байтами. В этом случае, в структуре CHAR_INFO поле "UnicodeChar" расширяется до пары байт, отнимая второй у "AsciiChar". Атрибут-же всегда остаётся статичным и так-же кодируется вордом, хотя и использует только младший из двух байтов по такой схеме:
Как видим, младшая тетрада байта задаёт цвет текста Foreground (передний план), а старшая – цвет фона. Итого восемь бит. Комбинируя каждые из 3-х бит можно получить всего 8 основных цветов, однако если учитывать ещё и яркость, то получаем 16 для фона и столько-же для текста. Например если установить чёрный фон и добавить к нему старший бит интенсивности, то получим серый фон. Все эти константы я собрал в инклуде "Сonsole.inc" (см.скрепку). Вот как можно программно вывести на консоль ярко-жёлтый текст, на красном фоне. Эти настройки будут действительны до тех пор, пока мы вновь не восстановим исходные атрибуты:
Таким образом, к свойствам консольного окна относятся следующие характеристики:
3. Информация об окне и буфере
Система запускает каждый экземпляр консоли с прописанными в реестре дефолтными настройками – как правило это 80х25, причём размер высоты буфера выставляется на 300 строк. Когда размеры окна и буфера не совпадают, в консольном окне появляются соответствующие полосы прокрутки "ScrollBar". Вот как это выглядит на моей Win7:
В большинстве случаях ширины в 80-символов вполне хватает, но если нужно больше, можно расширить окно ещё на несколько символов. К примеру в практической части я добавляю ещё 20-символов, итого 100. Манипулировать размером окна позволяет функция SetConsoleWindowInfo() с таким прототипом:
Судя по этому описанию, размер окна нужно передать здесь через структуру SMALL_RECT. На первый взгляд она немного мутная, но именно её расширенные возможности позволяют нам разделить экранный буфер консоли хоть на 10 частей, и прокручивать их при помощи ScrollConsoleScreenBuffer() независимо. На что здесь следует обратить особое внимание, так это на сноску, которой нас предупреждает Microsoft:
Другими словами, пока размер текущего буфера равен 80х300 (см.скрин выше), мы не сможем изменить ширину окна на 100х30. Чтобы SetConsoleWindowInfo() отработала успешно, нужно сначала увеличить ширину экранного буфера до 100, и только потом менять размер окна. Более того, в засаду можно попасть проигнорировав нюанс в замечании "..ширина экранного буфера -1". Дело в том, что отсчёт размера буфера почему-то начинается с единицы, а размер окна – с нуля. Таким образом, если и ширину буфа и ширину окна выставить на 100, то функция будет возвращать ошибку, т.к. окно будет шире буфера на 1. Короче, очередная дурацкая шутка от индусов.
Функция SetConsoleScreenBufferSize() позволяется менять размер экранного буфера. У неё всего два аргумента – дескриптор вывода, и непосредственное значение кол-ва строк и столбцов, в виде структуры COORD:
Аргумент
4. Функция прокрутки экранного буфера
На данный момент имеем буфер и окно произвольного размера, и теперь можем прокручивать информацию в любой части заполненного буфера хоть вверх, хоть вниз. Такие возможности предоставляет нам функция ScrollConsoleScreenBuffer() с привязанной к ней структурой SMALL_RECT. В итоге получится приблизительно следующая картина:
Посмотрим на прототип этой функции – она требует некоторых пояснений:
Как видим, в аргументах находятся целых два указателя на уже знакомые нам структуры SMALL_RECT.
Если мы хотим прокрутить не весь буфер, а лишь отдельный его фрейм (как в демонстрации выше), то аргумент
Другой важной фигурой является аргумент
Теперь, запихав всё это хозяйство в цикл и функцией SetConsoleCursorPosition() каждый раз возвращая курсор на место, на выходе получим забавный эффект авто-прокрутки содержимого консоли – вот пример скроллинга на строку вверх:
5. Практика: информация о системной памяти
Ну и под занавес, соберём всё сказанное под один капот и напишем демку..
Как упоминалось в начале статьи, я решил вывести на консоль информацию о физической памяти компьютера. Левый блок из 38-ми столбцов будет статичным, куда и сбоксим нарытую инфу. В правый-же динамический фрейм буду читать и отображать всю виртуальную память своего процесса. Системный лог об ОЗУ поможет вытянуть функция GetPerformanceInfo() из библиотеки Psapi.dll, которая запрашивает данные у внушающих доверие счётчиков производительности ядра. Она возвращает выхлоп в структуру PERF_INFO следующего характера:
Эта функция удобна тем, что возвращает информацию не в байтах, а в страницах виртуальной памяти. В результате, даже будучи запущенной на 32-битной платформе с лимитом в 4 Gb, она может вернуть объём памяти 64-битных систем. Для этого берём из данной структуры значение поля "PageSize", и делаем его множителем для всех остальных полей. Правда для арифметики придётся использовать сопр FPU, по ходу конвертируя байты в мегабайты.
Проблема может возникнуть при последовательном чтении виртуальной памяти процесса. Дело в том, что система не выделяет нашему процессу сразу всю доступную память в диапазоне
Поэтому хорошей идеей будет установить в программу свой обработчик исключений SEH, который и будет отлавливать "блох". Внутри обработчика мы просто прыгаем на следующую страницу и возвращаем управление обратно коду. Для того, чтобы вести учёт ошибок страниц, я добавил в обработчик инкрементный счётчик, с отображением его в статический фрейм консоли.
Нужно сказать, что чтение получилось на очень большой скорости и если его не угомонить, то наблюдать содержимое дампа будет просто невозможно. По этой причине, было решено включить в алгоритм настройку скорости, в виде функции Sleеp() после отображения каждой строки дампа. Так у юзера появилась возможность регулировать задержку вывода по своему усмотрению, в диапазоне 0 (скорость реактивного гепарда), или 500 = 0.5 сек и выше. Вот собственно и все нюансы..
Подключив фантазию, по аналогичной схеме можно организовать блок "бегущей строки" в шапке или в подвале консоли, вывести небольшую заставку из фильма "Матрица" перед запуском основной программы, вращать угасающий атрибутами текст по периметру окна, и многое другое. Одним словом: была-бы гайка, а болт на неё найдётся.
6. Постскриптум
За всё время существования консольных терминалов, управляющие Escape-команды отлично себя зарекомендовали, т.к. не требовали громоздкой поддержи высокого уровня. Однако Microsoft никогда не признавала этого, и всегда старалась выдвигать на передний план свои Win32-API. Поэтому ни достопочтенный DOS, ни остальные их детища сроду не имели штатных средств реализации управления консолью кодами Escape, и приходилось устанавливать в систему сторонний драйвер ANSI.SYS.
Всё-что выводилось на консоль в текстовом режиме, этот дров проверял на 2-байтную последовательность символов Esc[ (коды 27,91) и если обнаруживал таковые, то несколько последующих байт воспринимал как аргументы управляющей команды. Лист возможных аргументов довольно внушительный, и позволяет оперировать консолью на всех уровнях, от переназначения клавиш, до манипуляции курсором, цветом и всем остальным.
Перед релизом Win-10 в 2018 году, у себя на форуме мелкософт подняла вопрос, мол "что-бы вы хотели видеть в нашей/новой системе?". Мировое сообщество программистов отреагировало на это весьма оригинально. Подняв транспаранты, огромной армией они вышли с призывом "Даёшь нормальную консоль для народа!", ..и нужно сказать, это не осталось без внимания. Так, начиная именно с Win-10 в системе появился доселе невиданный зверь – виртуальный терминал ConPTY.
Абсолютно свежее архитектурное решение наконец-то официально приняло Escape-команды на свой борт, хотя и в этом случае непосредственное исполнение их в конечном счёте возложено опять на Console-API. Поддержка реализована на уровне ОС в виде виртуального терминала Conhost.exe и драйвера режима ядра Сondrv.sys. С увесистым списком управляющих кодов и примерами их использования можно
В скрепке валяется исполняемый файл для тестов,
а так-же пара инклуд с описанием необходимых данному коду, структур.
Всем удачи, пока!
Ссылка скрыта от гостей
. Однако в своей массе мы привыкли использовать только часть из них, в результате чего виндозная Con приобретает достаточно унылый вид.В данной статье мы попробуем сломать устоявшиеся стереотипы и рассмотрим немного продвинутые методы программирования консольных окон, а в качестве пруфа РОС напишем небольшое приложение, в котором первая половина окна будет статичной (в данном случае покажем информацию о физ.памяти системы), а во-вторую часть будем динамически выводить дамп содержимого всей пользовательской памяти. Трюк известен как "прокрутка (скроллинг) произвольной части экранного буфера консоли".
Под капотом:
1. История командной строки Win – вводный курс;
2. Экранные буферы ввода/вывода;
3. Запрос информации об окне и буфере;
4. Функции прокрутки буфера;
5. Практика: информация о системной памяти;
6. Постскриптум.
------------------------------------------------------------1. История консоли Win
Чтобы у читателя не возникло лишних вопросов, для начала пройдём факультативный курс и ознакомимся с основными терминами этого направления. Обратив историю вспять и вернувшись на несколько десятилетий в прошлое, нужно будет рассмотреть архитектурную составляющую терминала Win – это фундаментальная база, без которой прогресс развития современной консоли останется за толстой стеной. Значит если коротко..
На заре компьютерной эры, терминалом назывался набор устройств, при помощи которых оператор управлял основной ЭВМ исполинских размеров. Как правило это были телетайпы TTY (дословно Tele-TYpe), со скоростью передачи 100 bps (10-симв/сек). Позже появились более совершенные девайсы класса VT100. Они были оснащены уже встроенным процессором i8080 для решения элементарных задач, примитивным дисплеем и клавиатурой – скорость сразу возросла до 1900 символов/сек. Их ещё называли интеллектуальным, унижая достоинства только появившихся на свет глупых "Dumb-терминалов".
Для передачи команд хосту, VT100 так-же использовал обычные символы ASCII, но добавились и управляющие Escape-последовательности, которыми заправлял институт ANSI (American National Standards Institute). Управляющие коды позволяли активно использовать графические возможности дисплея – это мерцание текста, смена шрифта на жирный, инверсия цветов, подчёркивание, перемещение курсора в произвольную область, и многое другое. Все эти надстройки хранились во-встроенной памяти процессора VT100, а набор его управляющих Esc-кодов и по сей-день является стандартом для всех эмуляторов терминалов, вплоть до Win-10.
В наше время, всё это хозяйство отправилось уже на свалку истории. Теперь компьютеры имеют встроенную видеокарту и контролёр клавиатуры, однако работа проприетарного терминала по-прежнему эмулируется средствами ОС, позволяя работать в текстовом режиме, когда в форточках Win нет особой нужды. В системной архитектуре к эмуляторам привязан термин PTY, или псевдо-терминал. Он представляет собой пару виртуальных устройств Master/Slave. Ведомый Slave программно эмулирует TTY, а Master – это наш консольный софт, который и управляет ведомым псевдо-девайсом.
• Командная строка – парсер символов, роль которого в MS-DOS исполнял символьный процессор Сommand.com, а на системах с графическим фейсом Win он мутировал в файл cmd.exe. Интерпретатор ком-строки проверяет юзерский ввод и исполняет связанные с ним команды. В 2006 году Microsoft представила объектно-ориентированную оболочку ком-строки под названием "PowerShell".
• Консоль – инфраструктура Windows для работающих в текстовом режиме TUI приложений. Каждый экземпляр консоли имеет свой буфер ввода/вывода. Системы XP и Win10 могут менять режим видеоадаптера с графического на текстовый VGA, что позволяет комбинацией клавиш [Alt+Enter] раскрывать консоль на весь экран. В полноэкранной реализации, Win10 задействует собственную подсистему рендеринга шрифтов, поэтому консоль может иметь столько символов в строке, сколько вмещает экран. Этим не может похвастаться XP, где имеется ограничение в 25-строк по 80-символов в каждой. Системы Vista,7,8 не поддерживают полноэкранные режимы консоли.
2. Консольные буферы ввода/вывода
Как упоминалось выше, к каждой консоли (а их может быть сразу несколько) привязываются собственные буферы ввода-вывода. На физическом уровне, буфер представляет собой обычную структуру в виртуальной памяти системы. Экземпляр консоли может иметь только один буфер-ввода с клавиатуры, зато неограниченное число буферов-вывода на экран. Мы можем создавать доп.буферы вывода функцией CreateConsoleScreenBuffer() и заполнять их разной информацией. Однако в каждый момент времени только один из них может быть активным, который выбирается через SetConsoleActiveScreenBuffer(). В то-же время, к буферам можно обращаться на RW в любое время, хоть активный он, хоть нет.
Экранный буфер-вывода (ScreenBuff) оформлен в памяти в виде "двумерной матрицы символов". Кол-во символов в одной строке пропорционально размеру самого буфера, изменить который можно функцией SetConsoleScreenBufferSize(). Под каждый символ в этой матрице отводится по 4-байта – два на символ, два на его атрибут (цвет-фон). Эту единицу описывает структура CHAR_INFO:
C-подобный:
struct CHAR_INFO ;//<--- чтение/запись в экранный буфер консоли
UnicodeChar db 0 ;// символ в кодировке Юникод
AsciiChar db 0 ;// обычный символ Ascii
Attributes dw 0 ;// смотри ниже..
ends
По умолчанию консоль работает в режиме ANSI, где код символа кодируется одним байтом. Однако изменением кодовой страницы, консоль можно перевести и в режим Unicode, где каждый символ кодируется уже двумя байтами. В этом случае, в структуре CHAR_INFO поле "UnicodeChar" расширяется до пары байт, отнимая второй у "AsciiChar". Атрибут-же всегда остаётся статичным и так-же кодируется вордом, хотя и использует только младший из двух байтов по такой схеме:
Как видим, младшая тетрада байта задаёт цвет текста Foreground (передний план), а старшая – цвет фона. Итого восемь бит. Комбинируя каждые из 3-х бит можно получить всего 8 основных цветов, однако если учитывать ещё и яркость, то получаем 16 для фона и столько-же для текста. Например если установить чёрный фон и добавить к нему старший бит интенсивности, то получим серый фон. Все эти константы я собрал в инклуде "Сonsole.inc" (см.скрепку). Вот как можно программно вывести на консоль ярко-жёлтый текст, на красном фоне. Эти настройки будут действительны до тех пор, пока мы вновь не восстановим исходные атрибуты:
C-подобный:
;// запросить в EAX дескриптор вывода на консоль
invoke GetStdHandle,STD_OUTPUT_HANDLE
;// меняем атрибуты текста
invoke SetConsoleTextAttribute,eax, BACKGROUND_RED + FOREGROUND_YELLOW + FOREGROUND_INTENSITY
cinvoke printf, <10,' Press any key for exit...',0> ;// жёлтый на красном фоне
Таким образом, к свойствам консольного окна относятся следующие характеристики:
• размер экранного буфера (строк и столбцов);
• цвет символа/фона (атрибуты);
• размер консольного окна (может быть меньше буфера, но не больше);
• позиция и видимость курсора;
• режимы вывода данных – задаётся во флагах функции SetConsoleMode().
3. Информация об окне и буфере
Система запускает каждый экземпляр консоли с прописанными в реестре дефолтными настройками – как правило это 80х25, причём размер высоты буфера выставляется на 300 строк. Когда размеры окна и буфера не совпадают, в консольном окне появляются соответствующие полосы прокрутки "ScrollBar". Вот как это выглядит на моей Win7:
В большинстве случаях ширины в 80-символов вполне хватает, но если нужно больше, можно расширить окно ещё на несколько символов. К примеру в практической части я добавляю ещё 20-символов, итого 100. Манипулировать размером окна позволяет функция SetConsoleWindowInfo() с таким прототипом:
C-подобный:
;// Устанавливает текущий размер и положение окна экранного буфера консоли
;// ***********************************************************************
BOOL SetConsoleWindowInfo ;// нуль = ошибка
hStdOutput dd 0 ;// дескриптор вывода
absCoords dd 0 ;// 1 = абсолютный (новый) размер, 0 = относительно текущего
lpSmallRect dd 0 ;// указатель на структуру SMALL_RECT -+
;// |
;// |
struct SMALL_RECT ;// <----------------------------------+
Left dw 0 ;// столбец(Х) верхнего/левого угла консоли
Top dw 0 ;// строка(Y) верхнего/левого угла
Right dw 0 ;// столбец(Х) нижнего/правого угла
Bottom dw 0 ;// строка(Y) нижнего/правого угла
ends
Судя по этому описанию, размер окна нужно передать здесь через структуру SMALL_RECT. На первый взгляд она немного мутная, но именно её расширенные возможности позволяют нам разделить экранный буфер консоли хоть на 10 частей, и прокручивать их при помощи ScrollConsoleScreenBuffer() независимо. На что здесь следует обратить особое внимание, так это на сноску, которой нас предупреждает Microsoft:
Замечание:
Функция SetConsoleWindowInfo() завершается ошибкой, если указанный прямоугольник окна выходит за границы экранного буфера консоли. Это означает, что элементы Top/Left не могут быть меньше нуля. Точно так же элементы Bottom/Right не могут быть больше, чем высота и ширина экранного буфера -1. Для консолей с более чем одним экранным буфером, изменение расположения окна для одного экранного буфера не влияет на расположение окон других экранных буферов.
Другими словами, пока размер текущего буфера равен 80х300 (см.скрин выше), мы не сможем изменить ширину окна на 100х30. Чтобы SetConsoleWindowInfo() отработала успешно, нужно сначала увеличить ширину экранного буфера до 100, и только потом менять размер окна. Более того, в засаду можно попасть проигнорировав нюанс в замечании "..ширина экранного буфера -1". Дело в том, что отсчёт размера буфера почему-то начинается с единицы, а размер окна – с нуля. Таким образом, если и ширину буфа и ширину окна выставить на 100, то функция будет возвращать ошибку, т.к. окно будет шире буфера на 1. Короче, очередная дурацкая шутка от индусов.
Функция SetConsoleScreenBufferSize() позволяется менять размер экранного буфера. У неё всего два аргумента – дескриптор вывода, и непосредственное значение кол-ва строк и столбцов, в виде структуры COORD:
C-подобный:
.data
struct COORD
x dw 0 ;// горизонталь (столбцов)
y dw 0 ;// вертикаль (строк)
ends
struct SMALL_RECT
Left dw 0 ;// левый столбец,
Top dw 0 ;// ..и строка
Right dw 0 ;// правый столбец,
Bottom dw 0 ;// ..и строка
ends
.code
;// Меняем размер экранного буфера на 100х300
;//*********************************************
invoke GetStdHandle,STD_OUTPUT_HANDLE ;// EAX = дескриптор
mov [COORD.x],100 ;// символов в строке
mov [COORD.y],300 ;// строк в буфере
invoke SetConsoleScreenBufferSize,eax,dword[COORD]
;// Меняем размер окна консоли на 100х30
;//*********************************************
invoke GetStdHandle,STD_OUTPUT_HANDLE
mov [SMALL_RECT.Left] ,0
mov [SMALL_RECT.Top] ,0
mov [SMALL_RECT.Right] ,100-1 ;// отсчёт с нуля
mov [SMALL_RECT.Bottom], 30-1
invoke SetConsoleWindowInfo, eax, 1, SMALL_RECT
Аргумент
TRUE=1
при вызове функции говорит о том, что мы собираемся передать абсолютные координаты окна. Если-же указать FALSE=0
, то можно просто прибавить требуемое кол-во строк и столбцов, относительно текущих значений – иногда это удобно и даже необходимо. Запрашивает эту инфу очередная функция GetConsoleScreenBufferInfo(), которая сбрасывает лог в одноимённую структуру с таким содержимым:
C-подобный:
struct CONSOLE_SCREEN_BUFFER_INFO
dwSize COORD ;// размер экранного буфера
dwCursorPosition COORD ;// текущая позиция курсора (резерв = 0)
wAttributes dw 0 ;// фон и цвет символов при вызове функции
szWindow SMALL_RECT ;// размер окна консоли
dwMaximumWindowSize COORD ;// макс.возможный размер окна
ends
4. Функция прокрутки экранного буфера
На данный момент имеем буфер и окно произвольного размера, и теперь можем прокручивать информацию в любой части заполненного буфера хоть вверх, хоть вниз. Такие возможности предоставляет нам функция ScrollConsoleScreenBuffer() с привязанной к ней структурой SMALL_RECT. В итоге получится приблизительно следующая картина:
Посмотрим на прототип этой функции – она требует некоторых пояснений:
C-подобный:
;// Мотает часть буфера (или весь) выше/ниже
;// h = Handle, lp = указатель LongPointer, dw = значение DWORD
;//*********************************************************************
BOOL ScrollConsoleScreenBuffer ;//<--- Возвращает нуль при ошибке!
hStdout dd 0 ;// дескриптор вывода StdOut
lpScrollRect dd 0 ;// SMALL_RECT = прямоугольник, который необходимо переместить
lpClipRect dd 0 ;// SMALL_RECT = прямоугольник, на который влияет прокрутка
dwDestOrigin COORD ;// верхний/левый угол нового расположения ScrollRect
lpFill dd 0 ;// CHAR_INFO = символ, цвет символа/фона
Как видим, в аргументах находятся целых два указателя на уже знакомые нам структуры SMALL_RECT.
Если мы хотим прокрутить не весь буфер, а лишь отдельный его фрейм (как в демонстрации выше), то аргумент
lpScrollRect
для нас в приоритете. В его структуре SMALL_RECT нужно указать координаты прямоугольника "Rectangle", на который будет действовать прокрутка "Scroll". В структуре-же второго аргумента lpClipRect
определяются координаты всего, что осталось снаружи прокручиваемого фрейма, т.е. размер глобального экранного буфера – с этим аргументом можно вообще не заморачиваться, а просто выставить его в нуль.Другой важной фигурой является аргумент
dwDestOrigin
– его координаты x/y определяют, в какую сторону производить прокрутку буфера. Возможные варианты: вверх, вниз, влево, вправо. Если перевести дословно, то это "Destination", или адрес получателя. Ну и последний аргумент – указатель на структуру CHAR_INFO, в которую нужно будет предварительно поместить атрибут и символ для заполнения строки, освободившейся в результате скрола. Как-правило символом является пробел, чтобы родилась пустая строка под новую инфу. Теперь, запихав всё это хозяйство в цикл и функцией SetConsoleCursorPosition() каждый раз возвращая курсор на место, на выходе получим забавный эффект авто-прокрутки содержимого консоли – вот пример скроллинга на строку вверх:
C-подобный:
;// Задаём размер активного прямоугольника.
;// Текущий размер окна и буфера = 100 колонок, и 30 строк.
;// Оставив слева 38 колонок и сверху 4 строки, будем прокручивать вверх остальные
;//********************************************************************************
mov [scrollRect.Left],38 ;// левый/верхний угол прямоугольника для скрола
mov [scrollRect.Top],4 ;// ...38=столбец(Х), 4=строка(Y)
mov [scrollRect.Right],100-1 ;// правый/нижний угол прямоугольника
mov [scrollRect.Bottom],23-1 ;// ...100=столбец(Х), 23=строка(Y)
mov [newPos.x],38 ;// новая позиция прямоугольника
mov [newPos.y],3 ;// 4-1 = 3 (сместить на одну строку вверх)
mov [charFill.Attributes],7 ;// атрибут символов = дефолт
mov [charFill.AsciiChar],' ' ;// символы новой строки (забивать пробелами)
invoke ScrollConsoleScreenBuffer,[StdOut],scrollRect,0,dword[newPos],charFill
5. Практика: информация о системной памяти
Ну и под занавес, соберём всё сказанное под один капот и напишем демку..
Как упоминалось в начале статьи, я решил вывести на консоль информацию о физической памяти компьютера. Левый блок из 38-ми столбцов будет статичным, куда и сбоксим нарытую инфу. В правый-же динамический фрейм буду читать и отображать всю виртуальную память своего процесса. Системный лог об ОЗУ поможет вытянуть функция GetPerformanceInfo() из библиотеки Psapi.dll, которая запрашивает данные у внушающих доверие счётчиков производительности ядра. Она возвращает выхлоп в структуру PERF_INFO следующего характера:
C-подобный:
struct PERFORMANCE_INFORMATION
cb dd sizeof.PERFORMANCE_INFORMATION
CommitTotal dd 0 ;// зарезервировано страниц
CommitLimit dd 0 ;// макс.зарезервировано
CommitPeak dd 0 ;// пик резервных страниц
PhysicalTotal dd 0 ;// всего физ.памяти в страницах
PhysicalAvailable dd 0 ;// доступно физ.памяти
SystemCache dd 0 ;// кэш-памяти в страницах
KernelTotal dd 0 ;// всего ядерной
KernelPaged dd 0 ;// выгружаемый пул ядра в страницах
KernelNonpaged dd 0 ;// невыгружаемый пул ядра
PageSize dd 0 ;// размер вирт.страницы в байтах
HandleCount dd 0 ;// всего дескрипторов
ProcessCount dd 0 ;// кол-во процессов в системе
ThreadCount dd 0 ;// общее кол-во потоков
ends
Эта функция удобна тем, что возвращает информацию не в байтах, а в страницах виртуальной памяти. В результате, даже будучи запущенной на 32-битной платформе с лимитом в 4 Gb, она может вернуть объём памяти 64-битных систем. Для этого берём из данной структуры значение поля "PageSize", и делаем его множителем для всех остальных полей. Правда для арифметики придётся использовать сопр FPU, по ходу конвертируя байты в мегабайты.
Проблема может возникнуть при последовательном чтении виртуальной памяти процесса. Дело в том, что система не выделяет нашему процессу сразу всю доступную память в диапазоне
0...80000000h
, а только в первоначальном кол-ве Commit. Дальше, если нам потребуется ещё, мы должны динамически запрашивать её у системы посредством VirtualAlloc(). Так-что при чтение последовательных адресов вполне можно нарваться на исключение 0xC0000005 = AccessViolation (нарушение прав доступа).Поэтому хорошей идеей будет установить в программу свой обработчик исключений SEH, который и будет отлавливать "блох". Внутри обработчика мы просто прыгаем на следующую страницу и возвращаем управление обратно коду. Для того, чтобы вести учёт ошибок страниц, я добавил в обработчик инкрементный счётчик, с отображением его в статический фрейм консоли.
Нужно сказать, что чтение получилось на очень большой скорости и если его не угомонить, то наблюдать содержимое дампа будет просто невозможно. По этой причине, было решено включить в алгоритм настройку скорости, в виде функции Sleеp() после отображения каждой строки дампа. Так у юзера появилась возможность регулировать задержку вывода по своему усмотрению, в диапазоне 0 (скорость реактивного гепарда), или 500 = 0.5 сек и выше. Вот собственно и все нюансы..
C-подобный:
format PE console
include 'win32ax.inc'
include 'equates\memory.inc'
include 'equates\console.inc'
entry start
;//-----------
.data
perfInfo PERFORMANCE_INFORMATION
buffInfo CONSOLE_SCREEN_BUFFER_INFO
conBuff COORD
curPos COORD
scrollRect SMALL_RECT
charFill CHAR_INFO
newPos COORD
StdIn dd 0
StdOut dd 0
speed dd 0
counter dd 0
nextOffs dd 0x00010000 ;//<----- нижний порог доступного юзеру адреса
mByte dd 1024*1024 ;// переменные для FPU
Result dq 0
pageSize dd 0
buff db 0
;//-----------
.code
start: invoke SetConsoleTitle,<'*** MemRead & Information ***',0>
;// Ставим свой обработчик исключений SEH
push DropException
push dword[fs:0]
mov dword[fs:0],esp
;// Берём в переменные дискрипторы ввода/вывода консоли
invoke GetStdHandle,STD_INPUT_HANDLE
mov [StdIn],eax
invoke GetStdHandle,STD_OUTPUT_HANDLE
mov [StdOut],eax
;// Меняем размер консольного буфера
mov [conBuff.x],100
mov [conBuff.y],100
invoke SetConsoleScreenBufferSize,[StdOut],dword[conBuff]
;// Запрашиваем текущий размер окна, и подгоняем его под буфер
invoke GetConsoleScreenBufferInfo,[StdOut],buffInfo
mov esi,buffInfo.szWindow
mov word[esi+SMALL_RECT.Right],100-1
invoke SetConsoleWindowInfo,[StdOut],1,esi
;//*****************************************************************
;// Запрашиваем инфу о сис.памяти в структуру "perfInfo",
;// сохраняем размер вирт.страницы памяти, и инициализируем сопр FPU
invoke GetPerformanceInfo,perfInfo,sizeof.PERFORMANCE_INFORMATION
mov eax,[perfInfo.PageSize]
mov dword[pageSize],eax
finit
;// Выводим на консоль инфу о системной памяти (физ/вирт).
;// Функция возвращает её в страницах, поэтому переводим страницы в Мб.
push [perfInfo.PhysicalTotal]
call Page2Mbyte
cinvoke printf, <10,' SYSTEM MEMORY INFORMATION',\
10,' """"""""""""""""""""""""""""""',\
10,' Physical total...: %5.0f Mb',0>,dword[Result],dword[Result+4]
push [perfInfo.PhysicalAvailable]
call Page2Mbyte
cinvoke printf, <10,' Physical free....: %5.0f Mb',10,0>,dword[Result],dword[Result+4]
cinvoke printf, <10,' Virtual page size: %5d byte',0>,[perfInfo.PageSize]
push [perfInfo.CommitTotal]
call Page2Mbyte
cinvoke printf, <10,' Virtual current..: %5.0f Mb',0>,dword[Result],dword[Result+4]
push [perfInfo.CommitLimit]
call Page2Mbyte
cinvoke printf, <10,' Virtual limit....: %5.0f Mb',10,0>,dword[Result],dword[Result+4]
push [perfInfo.SystemCache]
call Page2Mbyte
cinvoke printf, <10,' System cache.....: %5.0f Mb',0>,dword[Result],dword[Result+4]
push [perfInfo.KernelTotal]
call Page2Mbyte
cinvoke printf, <10,' Kernel total.....: %5.0f Mb',0>,dword[Result],dword[Result+4]
push [perfInfo.KernelPaged]
call Page2Mbyte
cinvoke printf, <10,' Kernel paged.....: %5.0f Mb',0>,dword[Result],dword[Result+4]
push [perfInfo.KernelNonpaged]
call Page2Mbyte
cinvoke printf, <10,' Kernel nonpaged..: %5.0f Mb',10,0>,dword[Result],dword[Result+4]
cinvoke printf, <10,' Total process....: %5d',0>,[perfInfo.ProcessCount]
cinvoke printf, <10,' Total thread.....: %5d',0>,[perfInfo.ThreadCount]
cinvoke printf, <10,' Total handle.....: %5d',0>,[perfInfo.HandleCount]
;//*****************************************************************
;// Тест на чтение пользовательской памяти ОЗУ.
;// Запрашиваем скорость вывода дампа, и ставим счётчик "ошибок-доступа" к страницам.
cinvoke printf, <10,10,' """"""""""""""""""""""""""""""',\
10,' Type dump speed (0..500): ',0>
cinvoke scanf,<'%d',0>,speed
cinvoke printf, <' Page exception count....: 0',0>
invoke SetConsoleTextAttribute,[StdOut], BACKGROUND_RED + FOREGROUND_YELLOW + FOREGROUND_INTENSITY
cinvoke printf, <10,' Press space-key for exit....',0> ;// жёлтый на красном фоне
;// Тест на чтение пользовательской памяти ОЗУ
invoke SetConsoleTextAttribute,[StdOut], FOREGROUND_YELLOW + FOREGROUND_INTENSITY
invoke Sleep,1000
mov [curPos.x],38
mov [curPos.y],1
invoke SetConsoleCursorPosition,[StdOut],dword[curPos]
cinvoke printf,<'Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ',0>
inc [curPos.y]
invoke SetConsoleCursorPosition,[StdOut],dword[curPos]
cinvoke printf,<'"""""""" """"""""""""""""""""""""""""""""""""""""""""""" ',0>
inc [curPos.y]
;// Цикл чтения и вывода памяти на консоль
@dump: call PrintMemoryString ;// распечатать 16-байт на консоль
add [nextOffs],16 ;// сместить указатель на сл.16-байт
invoke Sleep,[speed] ;// пауза, чтобы осмотреться
inc [curPos.y] ;// курсор на сл.строку
cmp [curPos.y],23 ;// проверить на нижнюю границу окна
jne @dump ;// повторить, если нет..
;// ИНАЧЕ:
;// Задаём размер прямоугольника для прокрутки
dec [curPos.y] ;// возвратить курсор на место (строка 23)
mov ax,[buffInfo.wAttributes] ;// берём атрибуты символа
mov [charFill.Attributes],ax ;// делаем их-же текущими для заполнения
mov [charFill.AsciiChar],' ' ;// символы новой строки (забивать пробелами)
mov [scrollRect.Left],38 ;// левый/верхний угол прямоугольника для скрола
mov [scrollRect.Top],4 ;// ...38=столбец(Х), 4=строка(Y)
mov [newPos.x],38 ;// новая позиция прямоугольника
mov [newPos.y],3 ;// ...4-1 = 3 (скрол на одну строку вверх)
mov [scrollRect.Right],100-1 ;// правый/нижний угол прямоугольника
mov [scrollRect.Bottom],23-1 ;// ...96=столбец(Х), 22=строка(Y)
;// Прокрутка буфера консоли =======================================
@scrollBuff:
mov eax,[nextOffs] ;// тест на чтение текущего адреса (SEH отловит ошибку)
invoke GetKeyState,VK_SPACE ;// проверить нажатие клавиши "пробел",
test eax,1 ;// ..для выхода из цикла прокрутки.
jnz @exit ;//
invoke ScrollConsoleScreenBuffer,[StdOut],scrollRect,0,dword[newPos],charFill
invoke SetConsoleCursorPosition,[StdOut],dword[curPos]
call PrintMemoryString ;// распечатать 16-байт на консоль
invoke Sleep,[speed] ;// пауза..
add [nextOffs],16 ;// сл.16-байт для вывода
cmp [nextOffs],0x7FFE0000 ;// тест на верхнюю границу юзерской памяти
jnz @scrollBuff ;// повторить, если не достигли её..
@exit: cinvoke getch
cinvoke exit,0
;//**** ВСПОМОГАТЕЛЬНЫЕ ПРОЦЕДУРЫ *****************************************************
;// При помощи FPU переводит кол-во страниц в Мегабайты
proc Page2Mbyte info
fild [info] ;// взять очередную инфу из struct "PERFORMANCE_INFORMATION"
fimul [pageSize] ;// умножить на размер вирт.страницы
fidiv [mByte] ;// перевести байты в Мбайты
fstp [Result] ;// сохранить результ в переменной!
ret
endp
;// Вывод дампа памяти на консоль
proc PrintMemoryString
invoke SetConsoleCursorPosition,[StdOut],dword[curPos]
mov esi,[nextOffs] ;// ESI = очередной адрес в памяти
push esi ;// +
cinvoke printf,<'%08X ',0>,esi ;// распечатать адрес!
pop esi ;// -
mov ecx,16 ;// кол-во байт в строке дампа (16 = параграф)
@str: xor eax,eax ;// EAX = 0
lodsb ;// AL = очередной байт из ESI
push esi ecx
cinvoke printf,<'%02x ',0>,eax ;// распечатать байт!
pop ecx esi
loop @str ;// повторить ECX-раз..
ret
endp
;// SEH - пользовательский обработчик исключений
proc DropException pRecord, pFrame, pContext, pParam
inc [counter] ;// считаем недоступные на R/W страницы
invoke SetConsoleCursorPosition,[StdOut],0x0015001b ;// курсор в статичную область буфера
cinvoke printf,<'%d'>,[counter] ;// вписать туда счётчик найденных страниц!
mov esi,[pContext] ;// ESI = указатель на контекст регистров
mov dword[esi+0xb8],@scrollBuff ;// 0xb8 = CONTEXT.RegEip (запись в EIP адреса перехода)
mov eax,[pageSize] ;// EAX = размер страницы вирт.памяти
add [nextOffs],eax ;// уйти на сл.страницу
cmp [nextOffs],0x7FFE0000 ;// тест на потолок юзера
jnz @f ;// пропустить, если не дошли до макс..
mov dword[esi+0xb8],@exit ;// иначе: регистр EIP = выход из программы
@@: xor eax,eax ;// возврат управления программе!!!
ret ;//
endp
;//**********************************************
section '.idata' import data readable
library kernel32,'kernel32.dll',user32,'user32.dll',\
msvcrt,'msvcrt.dll',psapi,'psapi.dll'
import msvcrt,printf,'printf',scanf,'scanf',getch,'_getch',exit,'exit'
import psapi, GetPerformanceInfo,'GetPerformanceInfo'
include 'api\kernel32.inc'
include 'api\user32.inc'
Подключив фантазию, по аналогичной схеме можно организовать блок "бегущей строки" в шапке или в подвале консоли, вывести небольшую заставку из фильма "Матрица" перед запуском основной программы, вращать угасающий атрибутами текст по периметру окна, и многое другое. Одним словом: была-бы гайка, а болт на неё найдётся.
6. Постскриптум
За всё время существования консольных терминалов, управляющие Escape-команды отлично себя зарекомендовали, т.к. не требовали громоздкой поддержи высокого уровня. Однако Microsoft никогда не признавала этого, и всегда старалась выдвигать на передний план свои Win32-API. Поэтому ни достопочтенный DOS, ни остальные их детища сроду не имели штатных средств реализации управления консолью кодами Escape, и приходилось устанавливать в систему сторонний драйвер ANSI.SYS.
Всё-что выводилось на консоль в текстовом режиме, этот дров проверял на 2-байтную последовательность символов Esc[ (коды 27,91) и если обнаруживал таковые, то несколько последующих байт воспринимал как аргументы управляющей команды. Лист возможных аргументов довольно внушительный, и позволяет оперировать консолью на всех уровнях, от переназначения клавиш, до манипуляции курсором, цветом и всем остальным.
Перед релизом Win-10 в 2018 году, у себя на форуме мелкософт подняла вопрос, мол "что-бы вы хотели видеть в нашей/новой системе?". Мировое сообщество программистов отреагировало на это весьма оригинально. Подняв транспаранты, огромной армией они вышли с призывом "Даёшь нормальную консоль для народа!", ..и нужно сказать, это не осталось без внимания. Так, начиная именно с Win-10 в системе появился доселе невиданный зверь – виртуальный терминал ConPTY.
Абсолютно свежее архитектурное решение наконец-то официально приняло Escape-команды на свой борт, хотя и в этом случае непосредственное исполнение их в конечном счёте возложено опять на Console-API. Поддержка реализована на уровне ОС в виде виртуального терминала Conhost.exe и драйвера режима ядра Сondrv.sys. С увесистым списком управляющих кодов и примерами их использования можно
Ссылка скрыта от гостей
, и Microsoft настоятельно рекомендует использовать в приложениях именно виртуальные последовательности, а не Console-API. Может быть это и к лучшему..В скрепке валяется исполняемый файл для тестов,
а так-же пара инклуд с описанием необходимых данному коду, структур.
Всем удачи, пока!