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

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

С разработкой оконных приложений мы кое-как разобрались, познакомившись с основными принципами программирования графических примитивов. И тут дернула меня нечистая написать простую программку для решения специфической микрозадачи. Понятное дело, что использовать оконный интерфейс для подобного случая совершенно не хотелось, было решено сделать приложение консольным .. лучше бы я в этот огород вообще не лез! :) Ну уж, как говорится "взялся за гуж, не говори что не дюж".
Было бы нелепо отрицать, что в последние десятилетия роль графического интерфейса существенно возросла, тем не менее в новейших операционных системах текстовые режимы всё еще сохраняются в виде разнообразных графических подсистем, в частности - текстового пользовательского интерфейса (окна командной строки/терминала). Из этого следует, что если уж текстовый режим до сей поры сохранился в операционных системах, значит он не является исключительно аппаратным рудиментом, поддержка которого существует лишь на уровне видеоадаптера, а является актуальным и используемым механизмом. Другими словами, если текстовый режим всё еще существует, значит это кому-то нужно. В происхождении любого феномена важно понять причину, и в случае с текстовым интерфейсом пользователя она заключается в следующем:

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

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

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

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

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

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

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

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

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

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

консоль cmd

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

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

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

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

Ограничение консольных приложений текстовым режимом в терминах операционной системы Windows, достаточно условное.

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

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

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

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

Стандартные потоки

С консолью в операционной системе Windows закреплено три основных потока ввода-вывода:

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

Традиционно, со стандартным вводом ассоциирована клавиатура, а со стандартным выводом ассоциирован монитор (экран), хотя потоки могут быть переопределены (перенаправлены) и на другие устройства (файл, ввод/вывод другой программы и прочее). Вследствие всего этого возникает резонный вопрос: обязательно ли наличие окна консоли у консольного приложения? Ведь, теоретически, консольные программы могут обходиться и без классического ввода (с клавиатуры) и вывода (в окно, на экран), ведь объекты stdin и stdout могут быть связаны с файлами, потоками ввода/вывода других программ или иными объектами операционной системы? Тем не менее, стандартный сценарий использования консольного приложения в Windows подразумевает создание отдельного окна консоли.

Окно и буферы

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

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

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

console buffer size

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

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

буфер и окно

Соответственно, из увиденного мы можем сделать несколько выводов:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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