В этом посте:

  • Описание понятия системного вызова
  • Таблица системных вызовов СМ-1800

Что такое системные вызовы?

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

Для взаимодействия с внешними устройствами у Intel 8080 есть инструкции IN и OUT. Они позволяют передавать значения по одному байту между процессором и внешним устройством.

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

Из-за этого производитель компьютера записывает в ПЗУ несколько подпрограмм, которые выполняют часто используемые задачи – например, “вывести строку текста по этому адресу на экран” или “ждать, пока не будет нажата клавиша на клавиатуре”. Эти подпрограммы называются системными вызовами, потому что они являются частью системы и их можно вызывать инструкцией CALL.

Это название я использую по аналогии с системными вызовами в современных операционных системах, хотя в современных ОС они имеют очень другой механизм работы. Можно также провести аналогию с вызовами-прерываниями BIOS, но они также имеют другой механизм работы.

Для целей этой серии статей мы будем называть системным вызовом процедуры, которые находятся в ПЗУ (эмулятора) СМ-1800 и позволяют управлять внешним оборудованием.

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

Пример получения возвращаемого значения:

; Сейчас регистр A имеет неизвестное значение...
CALL 0x0055  ; Вызываем подпрограмму для считывания символа с клавиатуры
; Сейчас регистр A имеет значение -- код символа, который был нажат на клавиатуре
; Используем это значение...

Пример передачи аргумента:

; Сейчас регистр A имеет неизвестное значение...
MVI A, 0x55  ; Записываем в регистр A значение 0x55
CALL 0x0061  ; Вызываем подпрограмму для вывода значения регистра A в шестнадцатиричном формате в консоль
; Теперь в консоли написано "55"

Замечание по поводу названий системных вызовов

Названия системных вызовов для СМ-1800 придуманы мной – они могут не соответствовать каким-то другим источникам. Это является основной причиной, по которой я начал писать эту серию статей – я не смог найти никакой документации про это, и поэтому для удобства объяснения я придумал свои названия для этих системных вызовов.

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

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

Замечание по поводу уверенности

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

Чтобы узнать более подробно, что именно делает конкретный системный вызов, нужно их изучать: либо проверять их поведение, либо изучать их исходный код из дизассемблированного образа ПЗУ. Только используя оба подхода можно быть уверенным в том, что именно делает конкретный системный вызов.

Уверенность помечается одним из следующих символов:

  • про подпрограмму ничего не известно, или описание взято из внешнего источника: 🤔
  • исходный код подпрограммы прочитан: 👀
  • подпрограмма была опробована на практике и ее поведение соответствует описанию: 🛠
  • подпрограмма была прочитана и опробована: 🔬

Таблица системных вызовов

Версия таблицы: 0.0 (2022-03-07)

Проверяйте версию своего include-файла прежде чем использовать эти названия!

Адрес Название Простое название Категория Входные аргументы Выходные аргументы Описание Уверенность
0x003D ??? ??? ??? ??? ??? ??? 🤔
0x0040 ??? ??? ??? ??? ??? ??? 🤔
0x0043 ??? ??? ??? ??? ??? ??? 🤔
0x0046 ??? ??? ??? ??? ??? ??? 🤔
0x0049 printNewLine PNL ??? Переводит курсор на новую строку 🛠
0x004C printStr PSTR ??? BC Выводит строку по адресу 🛠
0x004F printStrNL PSNL ??? BC Выводит строку по адресу и переводит курсор на новую строку 🛠
0x0052 ??? ??? ??? ??? ???    
0x0055 inputChar IKCHR ??? ??? A Ожидает ввода символа с клавиатуры и возвращает его 🛠
0x0058 ??? ??? ??? ??? ??? ??? 🤔
0x005B ??? ??? ??? ??? ??? ??? 🤔
0x005E ??? ??? ??? ??? ??? ??? 🤔
0x0061 printHex PAXX ??? A Выводит аргумент на экран как шестнадцатиричное число 🛠
0x0064 ??? ??? ??? ??? ??? ??? 🤔
0x0067 ??? ??? ??? ??? ??? ??? 🤔
0x006A ??? ??? ??? ??? ??? ??? 🤔
0x006D ??? ??? ??? ??? ??? ??? 🤔
0x0070 ??? ??? ??? ??? ??? ??? 🤔
0x0073 ??? ??? ??? ??? ??? ??? 🤔
0x0076 ??? ??? ??? ??? ??? ??? 🤔
0x0079 ??? ??? ??? ??? ??? ??? 🤔
0x007C ??? ??? ??? ??? ??? Устанавливает A в 0 👀
0x007E/0x007F ??? ??? ??? ??? ??? Устанавливает A в 0 👀
0x0081/0x0082 ??? ??? ??? ??? ??? ??? 🤔
0x0085 ??? ??? ??? ??? ??? ??? 🤔
0x0088 ??? ??? ??? ??? ??? ??? 🤔
0x008B ??? ??? ??? ??? ??? ??? 🤔
0x008E ??? ??? ??? ??? ??? ??? 🤔
0x0091 ??? ??? ??? ??? ??? ??? 🤔
0x0094 ??? ??? ??? ??? ??? ??? 🤔
0x0097 ??? ??? ??? ??? ??? ??? 🤔

Описание системных вызовов

printNewLine

Эта подпрограмма переводит курсор на новую строку.

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

printStr

Эта подпрограмма принимает в комбинированном регистре BC адрес в памяти. Строка, располагающаяся по этому адресу, выводится на экран.

Строка – это последовательность байтов, в конце которой находится байт 0x00.

Рассмотрим этот сегмент памяти:

Адрес Значение Смысл в ASCII
0x5000 0x48 H
0x5001 0x65 e
0x5002 0x6C l
0x5003 0x6C l
0x5004 0x6F o
0x5005 0x20 ` `
0x5006 0x77 w
0x5007 0x6F o
0x5008 0x72 r
0x5009 0x6C l
0x500A 0x64 d
0x500B 0x00  
0x500C 0x4F O
0x500D 0x4B K
0x500E 0x00  
0x500F 0x43 C
0x5010 0x61 a
0x5011 0x6E n
0x5012 0x63 c
0x5013 0x65 e
0x5014 0x6C l
0x5015 0x00  

Здесь есть три строки: одна начинается по адресу 0x5000 и значит Hello world, одна начинается по 0x500C и значит OK, и одна начинается по 0x500F и значит Cancel. Каждая из этих строк заканчивается байтом, равным 0x00 – он не считается частью строки и не выводится на экран.

Можно использовать части строк, у которых изменена позиция начала. Например, если начать читать с адреса 0x5003, то прочитается (и будет выведена) строка lo world.

printStrNL

Эта подпрограмма делает то же, что и printStr. После вывода строки, курсор переводится на новую строку.

inputChar

Эта подпрограмма останавливает выполнение программы до тех пор, пока на клавиатуре не будет введен символ с клавиатуры. После этого код введенного символа помещается в регистр A.

Символ также отображается на экране.

printHex

Эта подпрограмма выводит на экран два символа – шестнадцатиричное значение регистра A.