Консольний додаток на асемблері | Блог по Windows

Об’єкти консольного застосування

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

  • [Єдиний] вхідний буфер – область даних (події / сигнали / дані) для введення (передачі на консоль);
  • [Кілька] екранний вихідний буфер – область даних (символи / атрибути) для виведення (відображення на екрані);
  • Вікно консолі – область екрану, що відображає частину вихідного буфера;
  • Поточна позиція курсору – маркера виведення, що позначає поточну позицію виведення;

стандартні потоки

З консоллю в операційній системі Windows закріплено три основні потоки введення-виведення:

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

Стандартний потік введення-виведення – об’єкт процесу консольного застосування, який призначається для організації обміну даними.

Традиційно, зі стандартним введенням асоційована клавіатура, а зі стандартним висновком асоційований монітор (екран), хоча потоки можуть бути перевизначені (перенаправлені) і на інші логічні пристрої (файл, введення / висновок іншої програми та інше). Внаслідок усього цього виникає резонне питання: чи обов’язково наявність вікна консолі у консольного застосування? Адже, теоретично, консольні програми можуть обходитися і без класичного введення (з клавіатури) і виведення (в вікно, на екран), оскільки об’єкти stdin і stdout можуть бути пов’язані з файлами, потоками введення / виведення інших програм або іншими об’єктами операційної системи? Проте, стандартний сценарій використання консольного застосування в Windows має на увазі створення окремого вікна консолі.

Вікно і буфери

Як уже зазначалося, одними з основних об’єктів консольного застосування є вікно консолі, вхідний буфер консолі і екранний буфер консолі.

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

І вікно і буфер мають власні [вихідні] розміри, які можуть змінюватися за допомогою функцій Win32 API. За замовчуванням розмірів, визначених в операційній системі Windows:

  • розмір буфера – 80х300 символів (300 рядків по 80 символів довжиною кожна);
  • розмір екрану – 80х25 символів (25 рядків по 80 символів довжиною кожна);

Виходить приблизно наступна картина:

Відповідно, з побаченого ми можемо зробити кілька висновків:

  • Розмір екранного буфера консолі повинен бути більше або дорівнює розміру вікна консолі;
  • У разі, коли консольний буфер більше (за розміром) ніж вікно консолі, у вікні отрісовиваємих повзунки горизонтальній і / або вертикальної смуги прокрутки.

З одним консольним додатком може бути асоційоване кілька екранних буферів, тобто за одним консольним додатком може бути закріплений цілий набір екранних буферів. Кількість буферів визначається в параметрах реєстру і доступно для настройки у властивостях вікна консолі. Проте, при наявності безлічі буферів виникає проблема вибору поточного, оскільки в один момент часу вміст лише одного з них може відображатися у вікні консолі. Відображається буфер називають активним екранним буфером (при цьому інші є неактивними). Для визначення, який саме з буферів буде активним (відображатися у вікні консолі) в той чи інший момент часу, призначається функція SetConsoleActiveScreenBuffer.

Навіщо нам кілька вихідних консольних буферів? Справа в тому, що різноманіттям буферів забезпечується цікавий алгоритм, знайомий системним програмістам ще з часів MSDOS: попередня підготовка даних в неактивному буфері і подальше призначення його активним (перемикання між буферами).

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

Основні функції

Загальні функції:

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

Функції для роботи з буфером:

  • WriteConsole – виводить символи в вихідний екранний буфер консолі (в поточну позицію курсору);
  • ReadConsole – виробляє (фільтроване) читання символів з вхідного буфера консолі (видаляючи ці дані з буфера);
  • SetConsoleActiveScreenBuffer – призначення активного (поточного) екранного буфера консолі;
  • SetConsoleScreenBufferSize – завдання (зміна) розміру екранного буфера консолі;
  • CreateConsoleScreenBuffer – створення нового (власного) консольного буфера;
  • GetConsoleScreenBufferInfo – запит параметрів екранного буфера;
  • WriteConsoleOutput – записує символ і атрибут кольору в задану прямокутну область консольного екранного буфера;
  • WriteConsoleOutputCharacter – копіювання заданого числа символів в послідовно розташовані осередки консольного екранного буфера;

Функції для роботи з вікном:

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

Приклад 1: висновок рядки і очікування натискання клавіші

Настав час перейти безпосередньо до практичної частини питання, і в якості стартового прикладу ви наведемо найпростіше консольний додаток, що виводить в консоль одну-єдину рядок “Hello World!” і очікує введення будь-якого символу.

formatconsole; Консольне пріложеніе.entrystart; Точка входаinclude ‘% fasminc% \ win32a.inc’; Робимо стандартне включення описателей.; — секція коду — section’.text’readableexecutable start invokeGetStdHandleOUTPUTHANDLEstdout invokeGetStdHandleINPUTHANDLEstdin invokeWriteConsolestdout invokeReadConsolestdinlpBufferlpCharsRead exit invokeExitProcess; — секція даних — section’.data’readablewriteable’Hello, world! ‘; Текстова строка.lpBufferlpCharsRead; Кількість фактично лічених сімволовstdinstdout; — секція (таблиця) імпорту — section’.idata’importreadablewriteablelibrarykernel32’KERNEL32.DLL’importkernel32 GetStdHandle’GetStdHandle ‘WriteConsole’WriteConsoleA’ ReadConsole’ReadConsoleA ‘ExitProcess’ExitProcess’

На відміну від інших компіляторів вихідних кодів мови Асемблер, для FASM характерно досить суттєву перевагу, а саме включення всіх опцій компіляції безпосередньо у вихідний код програми. Наприклад, в даному прикладі ми розробляємо консольний додаток, відповідно, для вказівки даного факту нам зовсім не обов’язково ставити якісь опції командного рядка, досить в початковому тексті, як параметр директиви format вказати визначення console (Див. Рядок 1).
Отже, переходимо до секції коду. Як ми пам’ятаємо з теорії, наведеної на початку статті, для виконання введення-виведення у вікно консольного застосування, нам спершу треба отримати дескриптор (описувач) стандартних потоків. Windows API для цього надається функція GetStdHandle, прототип якої виглядає наступним чином:

HANDLEWINAPIGetStdHandle _In_DWORDnStdHandle

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

ім’я Розмір значення призначення
STD_INPUT_HANDLE DWORD -10 Стандартний Enter. Спочатку є консольним буфером введення (асоційований з клавіатурою).
STD_OUTPUT_HANDLE DWORD -11 Стандартний ВИСНОВОК. Спочатку є консольним буфером виведення (асоційований з екраном).
STD_ERROR_HANDLE DWORD -12 Стандартна ПОМИЛКА. Спочатку є буфером для виведення помилок (асоційований з екраном).

У рядках 10 і 11 нашого исходника відбувається запит дескрипторів стандартного виводу і стандартного введення відповідно. Отримані описатели, що повертаються функцією в регістрі EAX, зберігаються в локальних змінних (stdout, stdin).
Далі за кодом (рядок 14) розташовується функція перегляду тексту у вікно консолі WriteConsole. Ця функція більш складна, і тут у нас використовується вже цілих п’ять вхідних параметрів (зліва-направо):

WINAPIWriteConsole _In_ HANDLEhConsoleOutput _In_ constlpBuffer _In_ DWORDnNumberOfCharsToWrite _Out_ LPDWORDlpNumberOfCharsWritten _Reserved_ LPVOIDlpReserved
  1. дескриптор стандартного потоку виводу
  2. покажчик на рядок виводяться (друкованих) символів
  3. число друкованих символів
  4. покажчик на локальну змінну, яка одержує число фактично виведених символів
  5. зарезервований параметр, резерв (зарезервовано для наступних версій);

Потік символів, що виводяться відображається в вікно консолі починаючи з поточної позиції курсора. Позиція курсора зсувається в міру виведення символів.

Після виведення рядка Hello, world! на у вікно консолі, у нас організовано очікування введення символу за допомогою функції ReadConsole (рядок 15). Це зроблено з однією-єдиною метою – перешкодити автоматичного закриття вікна, оскільки консольне вікно після виконання коду програми зазвичай закривається. Функція ReadConsole приймає на вхід п’ять параметрів (зліва-направо):

WINAPIReadConsole _In_HANDLEhConsoleInput _Out_LPVOIDlpBuffer _In_DWORDnNumberOfCharsToRead _Out_LPDWORDlpNumberOfCharsRead _In_opt_LPVOIDpInputControl
  1. дескриптор стандартного потоку введення
  2. покажчик на буфер, який одержує дані з потоку введення
  3. кількість символів для читання
  4. Покажчик на змінну, яка отримує кількість фактично лічених символів
  5. Покажчик на структуру прототипа CONSOLE_READCONSOLE_CONTROL, яка отримує контрольний символ для сигналізації про закінчення операції читання. У нашому випадку не використовується, тому NULL.

На низькому рівні функції WriteConsole і ReadConsole фільтрують дані відповідно вихідного і вхідного буферів так, щоб видалити з них дані про події миші, клавіатури, змін розмірів консольного вікна. Відповідно, в слідстві даного факту ці функції є як би “високорівневими”, забезпечуючи простий висновок символів у вікно консолі і введення символів з клавіатури.

І під кінець секції коду у нас виконується функція ExitProcess (рядок 17) з єдиним аргументом, що має значення 0. Вона виробляє завершення викликає процесу і всіх його потоків. При завершенні процесу, закріплена за процесом консоль звільняється автоматично.

Характерною відмінністю цього прикладу є автоматичне створення консолі самою операційною системою.

Приклад 2: власна консоль і розміри видимої області

У випадку з консольним додатком з попереднього прикладу, вікно консолі створюється і відображається системою в автоматичному режимі. Але що б отримати можливість гнучко управляти різними аспектами консолі, нам буде потрібно створити так звану власну консоль, і в даному прикладі ми будемо спробуємо це реалізувати.

formatconsoleinclude ‘% fasminc% \ win32ax.inc’entrystart; — секція коду — section’.code’readableexecutable start invokeFreeConsole; Звільнити існуючу консоль invokeAllocConsole; Призначити нову консоль invokeGetStdHandleOUTPUTHANDLE; Отримати дескриптор стандартного потоку виводаstdout; Зберегти його invokeGetStdHandleINPUTHANDLE; Отримати дескриптор стандартного потоку вводаstdin; Зберегти його invokeGetConsoleScreenBufferInfostdoutlpConsoleScreenBufferInfoConsoleWindowConsoleWindowRightlpConsoleScreenBufferInfosrWindowRightConsoleWindowRightlpConsoleScreenBufferInfosrWindowBottomConsoleWindowBottom invokeSetConsoleWindowInfostdoutConsoleWindow invokeSleep10000 invokeFreeConsole exit invokeExitProcess; — секція даних — section’.data’readablewriteablestdoutstdinstructCOORDstructSMALL Left Top Right BottomstructCONSOLESCREENBUFFER dwSizeCOORD; розміри буфера в рядках і стовпцях символів dwCursorPositionCOORD; координати (позиція) курсора в буфері wAttributes; атрибути символів srWindowSMALL; координати верхнього лівого і нижнього правого кутів буфера dwMaximumWindowSizeCOORD; максимальні розміри консольного окнаlpConsoleScreenBufferInfoCONSOLESCREENBUFFERConsoleWindowSMALLdwSizeCOORD; — секція імпорту — section’.idata’importreadablewriteablelibrarykernel32’KERNEL32.DLL’importkernel32 AllocConsole’AllocConsole ‘GetConsoleScreenBufferInfo’GetConsoleScreenBufferInfo’ SetConsoleWindowInfo’SetConsoleWindowInfo ‘FreeConsole’FreeConsole’ GetStdHandle’GetStdHandle ‘Sleep’Sleep’ ExitProcess ‘ExitProcess’

Алгоритм наведеного вище вихідного коду можна описати таким чином:

  1. Відключимо (звільнимо) консоль по-замовчуванню, асоційовану з процесом;
  2. Створимо (призначимо) процесу нову “власну” консоль;
  3. Отримаємо параметри поточного консольного екранного буфера
  4. На основі розміром буфера виставимо розміри видимого вікна консолі: розмір (ширина, висота) видимій області консолі = розмір (ширина, висота) буфера, поділений на 4.
  5. Звільнимо створену раніше консоль.

Як було вже сказано, операційна система в ході завантаження консольного застосування на виконання, автоматично створює для нього “консоль за замовчуванням”, однак за консольним додатком (в один момент часу) може бути закріплена єдина консоль, і якщо ми хочемо створити власну консоль, то автоматично створену нам буде потрібно закрити. І ось тут виникає ряд додаткових нюансів.

На просторах Мережі можна зустріти рекомендацію: якщо буде потреба роботи з власної консоллю і відсутності впевненості в тому, наслідувало чи додаток консоль (передається консоль з додатком) чи ні, пропонується спочатку “відв’язувати” процес від поточної консолі функцією FreeConsole, а потім вже призначати власну консоль зухвалому процесу за допомогою функції AllocConsole.

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

Видима в командному вікні область буфера не може бути більше розмірів самого буфера.

Функція SetConsoleWindowInfo не змінює позицій консольного вікна на робочому столі, вона призначається для переміщення (прокручування) позиції і зміни розмірів відображення (відображуваного в командному вікні ділянки екранного буфера). Таким чином ця функція може бути використана для типового завдання, пов’язаної з програмуванням консольних додатків: “прокрутки” вмісту консольного екранного буфера шляхом зміщення позиції відображуваного прямокутника. А ось для безпосереднього переміщення вікна по екрану рекомендується використовувати типову функцію переміщення вікна MoveWindow.
У фінальній частині програми ми бачимо виклик функції очікування Sleep (застосовується для організації затримки), відразу після якої відбувається “звільнення” консолі за допомогою функції FreeConsole і подальшого виходу з програми через ExitProcess.

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

Приклад 3: кодування

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

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

Причина цього явища криється в тому, що розробники, в операційних системах лінійки Windows, придумали для кирилиці нову кодування CP1251, при цьому (в цілях сумісності) зберігши і стару з часів MSDOS – CP866.

Текстові редактори або середовища розробки, якими користується чимала кількість фахівців (наприклад, notepad ++, редактор зі складу Far Commander, блокнот (notepad) і багато інших) працюють з текстом в кодуванні ANSI, що для російської локалізації еквівалентно кодової сторінці тисячі двісті п’ятьдесят одна (Windows-1251, CP1251). Проте, для консолі діє наступне правило:

За замовчуванням вікно консолі налаштоване на роботу з растровими (точковими) шрифтами (raster fonts), які правильно відображають лише кодову сторінку OEM (вона ж cp866).

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

  1. Змінити кодування самого редактора, який використовується вами для редагування вихідного коду, на OEM. І, звичайно ж, перенабіть все локалізовані рядки, що містять кирилицю. Але тут є один нюанс: у OEM-кодуванні повинні бути вхідні дані (файли) для нашого застосування, і в цій же OEM-кодуванні будуть і всі вихідні дані.
  2. Використовувати функцій, що задають кодування стандартних потоків введення-виведення консолі, такі як SetConsoleOutputCP і SetConsoleCP. Але тут однією зміною кодування в початковому тексті не обійтися і доведеться зробити додаткове тілорух: у властивостях вікна консолі (наприклад) необхідно один раз вибрати будь-який TrueType-шрифт (доступні у властивостях вікна: Consolas і Lucida Console).
  3. Конвертувати виведені дані “на льоту”. У цьому випадку відпадає необхідність зміни кодування основного вихідного тексту програми. Реалізується це за допомогою функцій CharToOem (якщо текст в 1251) і WideCharToMultiByte (якщо текст в UNICODE).
  4. (Спробувати) Використовувати “пряму” запис в буфер замість роботи через функції, які використовують стандартні потоки. На практиці не перевіряв !!

Чому присутні функції визначення кодування для вхідних і вихідних потоків консолі? Це пояснює тим, що:

З кожною консоллю операційна система асоціює дві кодові таблиці – для введення і виведення.

Відповідно, кодові таблиці використовуються наступним чином:

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

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

formatconsole; Консольне пріложеніе.entrystart; Точка входаinclude ‘% fasminc% \ win32a.inc’; Робимо стандартне включення описателей.; — секція коду — section’.text’readableexecutable start invokeSetConsoleOutputCP invokeSetConsoleCP invokeGetStdHandleOUTPUTHANDLEstdout invokeGetStdHandleINPUTHANDLEstdin invokeWriteConsolestdout invokeReadConsolestdinlpBufferlpCharsRead exit invokeExitProcess; — секція даних — section’.data’readablewriteable’Прівет, світ! ‘ ; Текстовий рядок.
Ссылка на основную публикацию