Часть 5: Основы программирования на ассемблере
В этом посте:
- …
Программирование на ассемблере
В этой статье я буду описывать, как решить определенные задачи, которые часто появляются при программировании, на ассемблере. Ожидается, что вы уже понимаете код на каком-то императивном языке программирования – примеры кода будут на 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: добавить больше примеров
Если вам помогла эта статья, вы можете поддержать автора: