Шестнадцатибитный Ассемблер. Печать строки (без операционной системы) [перевод]

Дата публикации 30 дек 2022 | Редактировалось 31 дек 2022
Ассемблер несомненно - фантастический язык программирования, который мы можем использовать для работы (в оригинале контроля компонентов) с различными электронными компонентами, это язык близкий к машинному языку. В начале компьютерной эры многие разработчики использовали Ассемблер для компиляции кода. Затем записывали на дискеты. Так, например, компания Microsoft выпустила систему MSDOS.

Из этой статьи вы узнаете, как напечатать строку (вывод символов без операционной системы. прим. переводчика), используя 16-битный Ассемблер и о том, как такая программа спроектирована.

Требования

Перед тем как мы начнём углубляться в суть вопроса, нам потребуется найти инструменты для нашей задачи.

Необходимые инструменты

  • NASM - для компиляции кода
  • QEMU - для эмуляции скомпилированного кода

Создание кода
Существует два пути. Создайте файл example1.asm и откройте его. Добавим некоторые инструкции в первые строки:

Код (Text):
  1. ORG 0x7C00
  2. BITS 16
  3.  
ORG устанавливает загрузочный адрес, значение которого 0x7C00. Любопытно, что
это первый адрес, к которому компьютер обращается при загрузке. Этот адрес также известен как MBR (Master Boot Record - главная загрузочная запись). Также любопытно, что ёмкость этой записи 512 байт.
BITS означает, что мы пишем 16-битный код.

Продолжим писать программу:

Код (Text):
  1. mov ah, 0x0E
  2. mov al, «O"
  3. int 0x10
  4.  
mov ah, 0x0E: мы будем использовать функцию MOV, чтобы установить AH 0x0E (AH = 0x0E). AH — старший байтовый регистр, используется для возврата значений из вызываемых функций. 0x0E это функция БИОСа для вывода символов в режиме TTY (Teletype. Телетайп).
mov al, "0":установим значение 0 для (AL=0). AL похож на AH, разница в том, что это регистр младшего байта. Мы используем его для хранения символа, который будет напечатан.
int 0x10: добавим для осуществления вызова. INT - инструкция, представленная в процессорах x86, её функция в том, чтобы вызвать прерывание, переведённое в байтовое значение.

Теперь запишем последние строки:

Код (Text):
  1. jmp $
  2. times 510 - ($-$$) db 0
  3. dw 0xAA55
jmp $: перейдём к символу «$», - это говорит о том, что мы зацикливаем программу. Она будет работать без остановки.
times 510 — ($-$$) db 0: заполним наш код в 512 байт (видимо, имеется виду заполнение всего загрузочного сектора. прим. переводчика) для корректной работы.
dw 0xAA55: запишем в конец нашего кода «магическое» число 0xAA55.
Запомним, что этот код хранит загрузочный сектор (bootloader - загрузчик).

Полный код выглядит так:

Код (Text):
  1. ORG 0x7C00
  2. BITS 16
  3. mov ah, 0x0E
  4. mov al, «O"
  5. int 0x10
  6. jmp $
  7. times 510 - ($-$$) db 0
  8. dw 0xAA55
Сохраните этот файл и скомпилируйте, используя следующую команду:

Код (Text):
  1. $ nasm -f bin -o example1.bin example1.asm
После компиляции запустите код следующей командой:

Код (Text):
  1. qemu-system-x86_64 example1.bin
Если хотите добавить больше символов, можно добавить ещё три строки с символами и изменить символ.

Пример:

Мы можем улучшить вывод строки. Ведь не очень приятно повторять код и делать изменения. Поэтому автоматизируем печать.

Создайте файл example2.asm и запишите в него следующий код:

Код (Text):
  1. ORG 0x7C00
  2. BITS 16
  3. mov si, msg
  4. call PrintStr
  5. jmp $
  6.  
У нас появились еще две строки, которые мы опишем ниже.
mov si, msg: регистр SI мы выделим для сообщения, - это переменная, в которой мы будем хранить сообщение. Запишем остальное.
call PrintStr: мы создадим функцию, которая напечатает нашу строку (переменную msg) и вызовем её.
Соединив все строки вместе, получим "PrintStr(msg);".

Теперь напишем саму функцию:

Код (Text):
  1.  
  2. PrintStr:
  3.     mov ah, 0x0E
  4.     mov al, [si]
  5.     psloop:
  6.         int 0x10
  7.         inc si
  8.         mov al, [si]
  9.         cmp al, 0
  10.         jne psloop
  11.     ret
  12. ret
  13. msg db "test", 13, 10, 0
  14.  
Изучим наш код. В последней строке мы определяем переменную msg, которая хранит строку «test». Она будет добавлена в регистр SI, как упоминалось выше, поэтому значение хранится в SI.
Первые две строки хранят код, который определяет значение в AH и AL (вызовы BIOS). Регистр SI помещается в AL. Заметим, что SI имеет квадратные скобки. Это означает, что он передает первый символ из SI в AL, а не все символы. Скомпилировать код без скобок невозможно.
Создадим подпрограмму psloop - это цикл для вывода символов переменной SI. Внутри мы начнем выполнение с 0x10 (switch), который напечатает первый символ.

inc si: INC означает инкремент, эта команда нужная, чтобы с помощью SI поместить следующий символ. Это будет символ «e», первым был «t».
mov al, [si]: AL установит символ в из SI (символ «t»).
cmp al, 0: в этой команде 0 сравнивается с AL , это означает, что если значение регистра AL не равно 0, то выполнение перейдет в подпрограмму «psloop». Это сравнение нужно для проверки всех символов в строке. Если символы есть, значение будет равно 1, если символов не осталось, то значение будет равно 0.

На другом языке программирования этот код выглядел бы так:

Код (Text):
  1.  
  2. if (al != 0) {
  3.  psloop();
  4. }
  5.  
Если описывать функцию кратко, она напечатает первый символ, потом перейдет к следующему символу. Затем проверит, есть ли ещё символы в переменной. Если символы есть, то вернётся обратно в подпрограмму и напечатает символ, который был определён перед сравнением.

Финальный код:

Код (Text):
  1. ORG 0x7C00
  2. BITS 16
  3. mov si, msg       ; char* si = msg;
  4. call PrintStr     ; PrintStr(si);
  5. jmp $
  6.  
  7. PrintStr:
  8.     mov ah, 0x0E
  9.     mov al, [si]
  10.     psloop:
  11.         int 0x10
  12.         inc si
  13.         mov al, [si]
  14.         cmp al, 0
  15.         jne psloop
  16.     ret
  17. ret
  18.  
  19. msg db "test", 13, 10, 0      ; char msg[] = "test";
  20.  
  21. times 510 - ($-$$) db 0
  22. dw 0xAA55

Компиляция:
Код (Text):
  1. $ nasm -f bin -o example2.bin example2.asm
Выполнение

Код (Text):
  1. $ qemu-system-x86_64 example2.bin
  2.  
В итоге мы напечатали сообщение.

Автор: Mr Empy
Источник: https://mrempy.medium.com/assembly-16-bits-printing-strings-a114c72f6e43
Перевод: Иван Гаврюшин, dcc0@yandex.ru

Бонус, ссылка на интересный репозитарий, в нём ОС с линковщиком (из трёх файлов: загрзчик, линковщик, ядро,):
https://github.com/cirosantilli/x86-bare-metal-examples/blob/master/c_hello_world/run

Post Scriptum
Результаты моих доработок - микросистема
:
https://gitflic.ru/project/dcc0/mix-c-89-php/blob?file=cat_on_boot.asm
С наступающим 2023 годом!

2 1.599
dcc0

dcc0
Member

Регистрация:
22 дек 2022
Публикаций:
2

Комментарии


      1. dcc0 30 дек 2022
        Немножко поскрёб по сусекам и нашёл, как добавить интерактивности: сообщений теперь 2. Второе появляется по нажатии клавиши. В системе, так сказать, теперь можно оперировать:

        Код (Text):
        1.  
        2. ;Найдено здесь (Found here): https://mrempy.medium.com/assembly-16-bits-printing-strings-a114c72f6e43
        3. ;Добавлена спящая кошка, которая появляется по нажатию клавиши 0 by dcc0 30.12.2022
        4. ORG 0x7C00
        5. BITS 16
        6. ;Загружаем название
        7. mov di, msg       ; char* di = msg;
        8. call PrintStr     ; PrintStr(si);
        9.  
        10. ;Переходим к циклу ожидания нажатия клавиши
        11. jmp if_char
        12. first:
        13. mov di, cat       ; char* di = msg;
        14. call PrintStr     ; PrintStr(si);
        15.  
        16. ;Ожидание клавиши
        17. if_char:                      ;закр. метка
        18.       mov ah,0h           ;вызов обработки клавиши
        19.       int 16h                 ;вызываем 16 прерывание функции 0h
        20.       cmp al,0h
        21.       cmp ah,0Bh          ;код клавиши '0'
        22. jmp first
        23.  
        24. jmp $
        25. PrintStr:
        26.     mov ah, 0x0E ;Установим режим телетайп
        27.     mov al, [di] ;Добавим  элемент массива в al
        28.  
        29.     psloop:    ; Цикл
        30.         int 0x10 ; Систменый вызов BIOS
        31.         inc di ; инкремент
        32.         mov al, [di]; пишем след. значение
        33.         cmp al, 0; Если равен 0, выходим
        34.         jne psloop; Возврат в цикл
        35.     ret
        36. ret
        37. msg db "Operating System Sleeping Cat' ! Press Key 0", 13, 10, 0
        38. cat  db  20h, 5Eh, 5Eh, 0x0A,0x0D, 28h,  2Dh, 2Dh, 29h, 0x0A,0x0D, 5Ch, 2Fh, 20h, 5Ch, 0x0A,0x0D, 20h, 7Ch, 7Ch, 0x0A, 0x0D, 20h,  20h, 5Ch, 5Ch;
        39. times 510 - ($-$$) db 0; Заполняем нулями до 510 байта
        40. dw 0xAA55              ; Последние два байта сектора, к которым обращается BIOS
        41.