В этом посте:

Программирование на ассемблере

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

Для того, чтобы лучше понять, как писать программы на ассемблере, мне очень помогла игра Human Resource Machine от Tomorrow Corporation. Я высоко рекомендую ее всем, кто начинает программировать на ассемблере.

Как запустить программу

У вашей программы должна быть определенная точка, с которой нужно начать работу – какой-то адрес, где начинается код. Пользователь вашей программы, загрузив ее в память эмулятора, использует команду G, чтобы перейти на эту точку, и оттуда ваша программа начинает работать.

Во всех примерах ниже код начинается по адресу 0x4000. Чтобы запустить программу, после того как она загружена в память, нужно набрать G4000.

Как завершить программу

Чтобы прервать выполнение вашей программы и вернуть компьютер в исходное состояние, можно использовать инструкцию RST 0. Эта инструкция эквивалентна JMP 0x0000 – переход в начало памяти. Код, который находится там, сбрасывает состояние системы и запускает программу-монитор. Состояние памяти при этом остается неизменным.

Также существует инструкция HLT, которая полностью останавливает процессор. Когда эта инструкция выполнена, окно эмулятора перестает что-либо делать. Можно исправить это состояние, нажав на определенные клавиши на панели, или полностью сбросив эмулятор (включая содержимое памяти).

В примерах кода ниже будет использоваться инструкция RST 0, когда нужно завершить работу программы. Инструкция HLT будет вставлена в тех местах, которые никогда не выполняются.

Вывод заданной строки текста

Почти любая программа имеет сообщения, которые она выводит на экран.

print("Это моя первая программа!")
print("Она просто выводит ", end='')  # этот и следующий вывод будут на одной строке
print("несколько строк текста!")
print("Hello, world!")

Для того, чтобы вывести текст на экран, нужно использовать системный вызов printStr и printStrNL. Перед вызовом подпрограммы (с помощью инструкции CALL) нужно, чтобы в паре регистров BC находился адрес строки. (Строка – это последовательность символов, которая оканчивается байтом со значением 0.)

Строки и другие данные следует задавать с помощью директивы DB (define bytes).

Директива DB принимает несколько элементов (строк и чисел) через запятую. Из-за этого строки, заданные с помощью директивы DB, не могут содержать символ запятой:

; это не будет работать
fail: DB 'Hello, world!', 0

Эта проблема будет решена в будущих версиях программы. Пока это не решено, вы можете вводить символ запятой как отдельный аргумент директивы DB. Запятая имеет код 0x2c.

; будет работать
ok: DB 'Hello', 0x2C, ' world!', 0

Задав строку с помощью директивы DB, нужно установить метку в этом месте. Эта метка будет обозначать адрес начала строки. Эту метку затем можно передать как аргумент инструкции LXI B

Программа-ассемблер Suite8080-CM1800 поддерживает английский и русский язык для строк текста. Эмулятор СМ-1800 использует для вывода текста кодировку КОИ-7 Н2, которая реализована в этой программе-ассемблере.

Эта кодировка редко используется, и в других программах-ассемблерах она скорее всего не реализована. Там можно будет использовать только заглавные латинские буквы и пунктуацию – это те символы, которые совпадают между КОИ-7 и ASCII.

ORG 0x4000
; начало кода
LXI BC, msg1  ; загружаем в BC адрес строки
CALL printStrNL ; выводим строку и переводим курсор в начало следующей строки
LXI BC, msg2
CALL printStr  ; выводим строку, но НЕ переводим курсор
LXI BC, msg3
CALL printStrNL ; ...потому что следующее сообщение нужно написать сразу после предыдущего
LXI BC, msg4
CALL printStrNL
RST 0

ORG 0x5000
; область для сообщений
msg1: DB 'Это моя первая программа!', 0
msg2: DB 'Она просто выводит ', 0
msg3: DB 'несколько строк текста!', 0
msg4: DB 'Hello', 0x2C, ' world!', 0

Бесконечный цикл

print('Hello ', end='')  # Вывести слово 'Hello' один раз, затем
while True:
    print('hello ', end='')  # бесконечно выводить слово 'hello' в одну строку

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

ORG 0x4000
; начало кода

LXI BC, msg1
CALL printStr  ; выводим первую строку один раз

loop:
; только что создали метку в начале блока кода для повторения
LXI BC, msg2
CALL printStr  ; выводим вторую строку 
JMP loop  ; переходим к началу блока кода

HLT  ; процессор никогда не дойдет сюда, потому что цикл никогда не прекратится

ORG 0x5000
; область для сообщений
msg1: DB 'Hello ', 0
msg2: DB 'hello ', 0

Пропуск куска кода

print('Hello ', end='')  # Вывести слово 'Hello' один раз, затем
while False:             # не делать ничего здесь
    print('...')
    exit()
    ...

print('world!')

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

ORG 0x4000
; начало кода

LXI BC, msg1
CALL printStr  ; выводим первое сообщение
JMP msg1end    ; если бы этого не было, то мы бы начали выполнять текст как код

msg1:
DB 'Hello ', 0
HLT   ; любой код, который здесь, не будет выполнен

msg1end:
; Здесь закончилась строка msg1, дальше мы продолжаем выполнять код
; В предыдущий раз мы положили текст после вызова `CALL`, а теперь положим текст перед вызовом
JMP msg2end  ; пропускаем строку msg2

msg2: 
; здесь начинается строка msg2
DB 'world!', 0

msg2end:
; здесь закончилась строка msg2, дальше мы продолжаем выполнять код
LXI BC, msg2
CALL printStrNL  ; выводим второе сообщение

RST 0

Переменные

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

Это делается с помощью инструкции MOV, а также регистра HL. В регистре HL находится адрес памяти, и этот адрес – то, что подразумевается под “регистром” M в инструкции MOV.

Инструкцию MOV следует читать похоже на оператор присваивания. MOV A, B значит A := B; после этого A стал равен B.

LXI HL, 0x1234  ; записать адрес 0x1234 в регистр HL
MVI A, 0xAB     ; записать значение 0xAB в регистр A
MOV M, A        ; записать значение из регистра A в память по адресу 0x1234
; Теперь по адресу 0x1234 находится значение 0xAB
LXI HL, 0x1234  ; записать адрес 0x1234 в регистр HL
MOV A, M        ; считать значение из памяти по адресу 0x1234 в регистр A
; Теперь в регистре A находится значение, которое было в памяти по адресу 0x1234

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

ORG 0x4000
LXI BC, msg  ; подгатавливаем адрес строки для вывода
CALL printStrNL  ; выводим строку в исходном виде

LXI HL, symb     ; записываем в регистр HL адрес одного символа из строки
MVI A, 0x46      ; записываем в регистр A код символа 'F'
MOV M, A         ; записываем внутрь строки новый символ
CALL printStrNL  ; выводим строку заново (значение в BC осталось тем же, но строка изменилась)

MVI A, 0x76      ; записываем в регистр A код символа 'Ж'
MOV M, A         ; записываем внутрь строки новый символ
CALL printStrNL  ; выводим строку заново

MVI A, 0x00      ; записываем в регистр A нулевой байт
MOV M, A         ; записываем внутрь строки нулевой байт
CALL printStrNL  ; выводим строку заново
; нулевой байт -- это знак конца строки, поэтому все, что после нулевого байта, не будет выведено.

RST 0


ORG 0x5000
msg:
; пишем текст в начале сообщения
    DB 'На этом месте сейчас стоит буква '
    symb:
        ; метка здесь указывает на адрес того символа, который будет выведен после пробела в предыдущей строке
        DB '$'  ; сначала здесь будет знак доллара, но это изменится
    DB ' - вот была буква', 0  ; здесь конец строки

TODO: добавить больше примеров