Эта серия статей посвящена изучению и практике программирования на языке ассемблера.

Материал рассчитан на новичков в ассемблере, студентов, которым пришлось столкнуться с «динозавром» в виде MS-DOS, и может быть интересен тем, кто хочет немного узнать как функционировали операционные системы на заре своего существования.

Писать мы будем резидентную программу (TSR), которая при завершении возвращает управление оболочке операционной системы, но остается в памяти и продолжает своё существование. В современном мире близкий аналог этого понятия — демон/служба.

Программа будет выполнять следующие функции:

  • вывод текста вниз экрана по таймеру,

  • переключение режима отображения шрифта: italic/normal,

  • русификация,

  • запрет на ввод прописных русских букв,

  • резидентные часы,

  • вывод бинарного представления символа.

Предисловие

Материал не претендует на полноту, здесь будут рассматриваться базовые концепции и приемы программирования на языке ассемблера, необходимые для написания программ.

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

Немного оговорок. Далее под ассемблером будет пониматься язык ассемблера, а не программа компилятор. MS-DOS часто будет заменяться на dos/дос.

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

Про MS-DOS. Всех, наверное, пугает это слово в современном мире. Операционная система, которая уже как 20 лет мертва, но не все так однобоко как кажется на первый взгляд. Минусы понятны: изучение технологии, которая уже сгнила и разложилась, не используемая модель памяти. Но что насчет положительных моментов:

  • Ассемблер он и в Африке ассемблер, основные концепции программирования на нем будут везде одинаковы, да где-то будут расширенные регисты, где-то другой интерфейс по работе с операционной системой.

  • MS-DOS очень простая операционная система, которая в начале своего существования умещалась в 50 тысяч строк кода, причем ассемблерных (Майкрософт выложила исходники 2-х версий на github). График ее изучения имеет дно, в отличие от современных операционных систем. Аналогией может служить C и C++, последний, наверное, не знает в полной мере со всеми тонкостями ни один человек в мире.

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

  • Из предыдущего пункта понятно, что систему легко сломать, например, переписать адрес аппаратного прерывания по работе с клавиатурой, но в режиме эмуляции dos очень быстро запускается, что очень удобно в таких случаях

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

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

  • Ассемблер актуален в MS-DOS, и это радует, когда работаешь в ней, потому что иных средств разработки программ не так много там. Но в настоящее время ассемблер используется только в виде вставок в языке Си или в микроконтроллерах.

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

Немного про компилятор. Использоваться будет NASM, хотя логичнее было бы использовать досовские компиляторы TASM, MASM, но они не поддерживают мою рабочую операционную систему Линукс, а разрабатываться хочется все-таки в удобстве, поэтому взят nasm. Он популярный, современный, кроссплатформенный (запускается везде, компилируется подо все, включая дос), более умный — позволяет опускать какие-то вещи в синтаксисе, имеет фичи в виде локальных меток, контекстов, всяких других директив.

Настройка

Для начала нам потребуется эмулятор операционной системы DOS под названием DOSBox. Скачать можно здесь, версия 0.74-3. После установки и запуска вы увидите, что-то похожее на это:

Стартовый экран DOSBox
Стартовый экран DOSBox

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

Windows: Z:\> mount c: C:\Users\Username\Desktop\dos

Linux: Z:\> mount c: /home/username/dos

Получаем сообщение Drive C is mounted...Теперь все содержимое папочки dos будет отображаться в диске С: в эмуляторе. Перейти в диск C с диска Z можно командой Z:\>c:. Это действие придется делать каждый раз при запуске эмулятора, поэтому мы можем положить эту команду в файл конфигурации в секцию autoexec. На линуксе файл находится в /home/username/.dosbox. На виндовс C:\Users\Username\AppData\Local\DOSbox. Открываем файл dosbox-0.74-3.conf и в конец прописываем команду монтирования и перехода в диск C вот таким образом:

[autoexec]
# Lines in this section will be run at startup.
# You can put your MOUNT lines here.

mount c: /home/username/dos
c:

Hello world

Напишем первую программу на ассемблере, которая будет выводить на экран избитую фразу hello world:

org 100h

mov ah, 09h
mov dx, message
int 21h
int 20h

message: db 'Hello, world!$'

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

nasm hello_world.asm -o hello_world.com

Бинарный .com файл нужно положить в нашу папочку dos, перезапустить дос или запустить в работующем эмуляторе команду rescan, чтобы дос подхватил изменения. Запустить команду можно, начав вводить первые символы имени файла и нажав Tab. Вводить название файла целиком самостоятельно не стоит, потому что долго и потому что с файлами, у которых в имени больше 8 символов, начинаются проблемы. Регистр букв не важен. После запуска, на экране можно будет увидеть фразу Hello, world!.

Теперь о том, что делает каждая строчка, 1-я строка org 100h это указание компилятору на смещение начала инстукций, будет понятно, что это означает, когда мы рассмотрим устройство .com файла и механизм работы процессора в реальном режиме.

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

3-5 строки подготовка для вызова прерывания 21h и непосредственно сам вызов, прерывание мы обсудим в 3-ей части, в нашем случае это попросту вызов функции операционной системы. В строке 3 мы помещаем число 09h (h значит шестнадцатиричное) в регистр ah. 09h — это номер функции.

В строке 4 записываем в dx адрес начала строки, которую хотим вывести на экран. Теперь понятно зачем нужен $, начало строки дос знает, конец нет.

В строке 5 передаем управление операционной системе с помощью прерывания, по номеру функции дос понимает, что нужно сделать (вывести строку на экран).

В строке 6 используем прерывание 20h для завершения программы, этот способ не совсем корректный, но он простой и хорошо подходит для .com программ.

Не думаю, что стало сильно понятно. Поэтому в следующих частях мы рассмотрим теоретические аспекты: сегментную адресацию памяти, формат файла .com, дебаггер, интерфейс вызовов функций дос, прерывания и снова вернемся к примеру с hello world.

До встречи!

Комментарии (17)


  1. softi
    21.03.2022 13:33
    -8

    на языке ассемблера.

    Что-о-о? Что за язык ассемблера? Может, вы еще с большой буквы хотели написать "ассемблера", думая, что это имя? Нет "языка ассемблера"! Есть "язык ассемблер"!


    1. Druj
      21.03.2022 14:01
      +10

      Что за язык ассемблер? Это язык текст на котором сам что-то «ассемблит»? Нет, это язык на котором пишется код для ассемблера, который, в свою очередь, собирает ваш код. Так что «язык ассемблера»


  1. addewyd
    21.03.2022 14:10
    +5

    Своевременная статья. В закладки, однозначно.


    1. mSnus
      22.03.2022 03:46

      Возвращает в молодость, так сказать. 640 Кб памяти хватит на всё!


  1. kovserg
    21.03.2022 14:14
    +4

    В строке 6 используем прерывание 20h для завершения программы, этот способ не совсем корректный, но он простой и хорошо подходит для .com программ.

    Всегда использовал обычный ret


    1. webkadiz Автор
      21.03.2022 21:22

      Да, ret тоже вполне себе допустимый способ, внутри он использует все тот же int 20h, так как возврат идет на адрес PSP:0000, по которому лежит инструкция int 20h. Мне показался этот способ избыточным для дальнейшего описания его работы. Но упомянут он, безусловно, будет, так же как и функция 4ch превывания 21h.


  1. baldr
    21.03.2022 15:34
    +6

    При всей моей любви к MS-DOS, позволю себе усомниться в какой-либо практической пользе от изучения x86 ассемблера, особенно для реального режима.

    Ассемблер он и в Африке ассемблер

    Эмм, ну, не совсем. Как раз для каждого процессора он разный. В этом и смысл был изобретать языки более высокого уровня.

    но они не поддерживают мою рабочую операционную систему Линукс, а разрабатываться хочется все-таки в удобстве

    так-так-так.. Купим раритетный Москвич, но ездить будем на Тойоте. Компилировать тоже под Linux будем?

    Сама статья - это набор каких-то допущений, много умных слов, предлагается тупо набить текст программы..

    используем прерывание 20h

    Ах ну да. Прерывание. Чтобы прервать. Нет уж, придумайте как объяснить все связно либо просто оставьте ссылку на самую лучшую книжку: Зубков С. В.


    1. HardWrMan
      21.03.2022 16:07

      О, да, Зубков - сила! Сначала купил его книжку, а потом как-то даже пересёкся с ним в ФИДО! Как же давно это было... Кстати, есть она в формате CHM, но там не хватает некоторых глав (либо издание младшее - не знаю).


    1. addewyd
      21.03.2022 17:37
      +3

      позволю себе усомниться в какой-либо практической пользе от изучения x86 ассемблера, особенно для реального режима.


      Да вы оптимист. Похвально, конечно, но…


  1. dmitryvolochaev
    21.03.2022 20:35

    Так же как и реальный режим процессора, исключительно ради начальной загрузки до сих пор поддерживается текстовый режим экрана с возможностью из реального режима писать в видеопамять, PC Speaker, прерывание 9 для клавиатуры и много чего еще


  1. AWE64
    21.03.2022 22:42

    А пацаны говорят, что js сейчас в моде.


    1. addewyd
      22.03.2022 07:10
      +1

      Заграницв нам поможет: github.com/SuperIlu/jSH


  1. axe_chita
    22.03.2022 04:33

    /зануда вкл
    Программа преобразующая исходный текст на языке ассемблера называется «транслятором», а не компилятором. Транслятор, в отличии от компилятора, не выполняет ни какой оптимизации получаемого кода. К примеру встретит транслятор 30 килобайт NOP-ов в исходнике, он их как есть перенесет в получаемый файл, а не оставит один NOP, а остальные выкинет. Проще говоря «тупо поставленная задача, тупо выполнена».
    /зануда выкл


    1. HardWrMan
      22.03.2022 07:44

      Технически, мнемоники языка ассемблера просто попытка придания смысла для опкода, чтобы облегчить программисту понимание машинного кода. Для некоторых несложных процессоров при достаточном опыте иногда этот промежуточный уровень становится лишним. Например, я в своё время ещё на кружке плотно имел дело с КР580ВМ80А (i8080), так через некоторое время заметил, что могу читать опкоды прямо по хексодампу.

      Hidden text

      0000 21 00 00 11 00 90 7E 12 23 13 7A FE C0 C2 06 00

      0010 C3 00 C8

      Но если с ванильным i8086 это ещё возможно, то с нынешним x86 (а тем более с x64) это маловероятно. Но часто используемые мнемоники вполне поддаются запоминанию.


      1. RealTeodor
        22.03.2022 11:48
        +1

        Во времена, когда часто приходилось менять 75h на EBh - я тоже HEX довольно успешно читал. Хотя и HIEW'ом не брезговал. Потом навык был подутрачен. Сейчас патчу BIOS'ы для ноутов - VEN/DEV в видеомодулях ещё на глаз можно, а вот что-то посерьёзнее - без отладчика с визуализацией - никуда.


      1. axe_chita
        23.03.2022 06:23

        Совершенно верно, когда долго вглядываешься в бездну хекс с ассемблерным листингом, код начинает укоризненно смотреть на тебя.
        Если серьезно, то во времена MSDOS хорошим тоном для наСильника было генерить файл листинга, для последующей оптимизации/ускорения узких мест.
        Ну а коды 90 CD CB 9A 70-7F в подкорке сохранились.

        Минутка ностальгии
        image


  1. Svbakulin
    23.03.2022 05:50
    -2

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