Статья ASM – скроллинг части консольного буфера Win

Современную консоль можно рассматривать как франшизу на DOS. В документации Microsoft она числится в розысках под кличкой CUI, или "Console User Interface". В отличии от программ с графическим фейсом GUI, консольные приложения выполняются намного быстрее и абсолютно не требовательны к системным ресурсам, что привлекает внимание программистов всех мастей. В софтвейрном боксе этого интерфейса есть где развернуться – оперировать им позволяет набор из 65 специальных функций под общим названием . Однако в своей массе мы привыкли использовать только часть из них, в результате чего виндозная 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". Атрибут-же всегда остаётся статичным и так-же кодируется вордом, хотя и использует только младший из двух байтов по такой схеме:


Attributes.png


Как видим, младшая тетрада байта задаёт цвет текста 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:


80x25.png


В большинстве случаях ширины в 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. В итоге получится приблизительно следующая картина:


ConProperties.gif


Посмотрим на прототип этой функции – она требует некоторых пояснений:

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'

Scroll.gif


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


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. Может быть это и к лучшему..

В скрепке валяется исполняемый файл для тестов,
а так-же пара инклуд с описанием необходимых данному коду, структур.
Всем удачи, пока!
 

Вложения

  • ConsoleScroll.zip
    4,8 КБ · Просмотры: 327

yamakasy

Green Team
30.10.2020
158
113
BIT
0
всегда интересовал вопрос, почему консоль в винде настолько не популярна и впринципе не нужна, в отличии от тойже консоли в unix системах
 

wooolff

Green Team
19.02.2017
233
36
BIT
0
Отличная статья

Мое мнение, что именно после базового обучения программирования, то с этого и нужно начинать, а именно консоль, ее структуры и методы)))
 
  • Нравится
Реакции: Marylin

Crazy Jack

Well-known member
08.07.2017
573
89
BIT
35
Мое мнение, что именно после базового обучения программирования, то с этого и нужно начинать, а именно консоль, ее структуры и методы)))
Это понятно, не сразу же оконные приложения писать. Оконные вызовы в основном нада знать для реверся имхо.
 

SearcherSlava

Red Team
10.06.2017
943
1 262
BIT
224
Братья и сестры, здравы будьте!

Про консоль погуглить не забудьте:

Windows Command Reference pdf
Windows Command Line Administration Instant Reference pdf
Windows PowerShell 2.0. Справочник администратора pdf
Введение в Windows PowerShell pdf
Введение в C/C++ программирование консоли pdf
Основы программирования на C#. Консольные приложения pdf

И помните, консоль - юзера хлеб и соль.
 

Crazy Jack

Well-known member
08.07.2017
573
89
BIT
35
Братья и сестры, здравы будьте!

Про консоль погуглить не забудьте:

Windows Command Reference pdf
Windows Command Line Administration Instant Reference pdf
Windows PowerShell 2.0. Справочник администратора pdf
Введение в Windows PowerShell pdf
Введение в C/C++ программирование консоли pdf
Основы программирования на C#. Консольные приложения pdf

И помните, консоль - юзера хлеб и соль.
Консоль - хакера и админа хлеб и соль ;)
 

wooolff

Green Team
19.02.2017
233
36
BIT
0
[Alt+Enter] раскрывать консоль на весь экран
напомните, а как программно раскрывать консоль на весь экран?
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!