Седов Иван Алексеевич
Рязанский политехнический колледж
ASSEMBLER 8-bit SIMULATOR
Занятие #1: основные понятия и простейшая программа на языке ассемблер

https://e1m7.github.io/work/
*за исключением вывода данных на экран
ВВЕДЕНИЕ
Мы будем изучать симулятор 8-битного ассемблера, написанный Марко Швайхаузером на JavaScript и доступный по адресу clck.ru/Wc4uD
Симулятор полностью идентичен обычному ассемблеру*, он может собрать исходный текст программы в машинный код и запустить его на виртуальной машине. Симулятор идеально подходит для первоначального знакомства с ассемблером, после работы на нем можно приступать к изучению обычного ассемблера (например, FASM), уроки по ссылке clck.ru/WciXq
СВЕДЕНИЯ
У процессора* есть несколько фрагментов памяти, называемые регистрами. Один регистр содержит один байт памяти, то есть он может хранить значение 0-255 (в 10-тичной системе) или 00-FF (в 16-ричной системе). (На рисунке A, B, C, D, IP и SP.)

*здесь и далее речь про данный абстрактный 8-битный процессор, он похож на "обычный", но сильно упрощен(!)
У процессора есть флаги (тоже часть памяти), каждый из которых содержит один бит памяти, которые (флаги) используются для представления логических значений. Каждый флаг в любой момент времени может быть равен либо TRUE(1) либо FALSE (0). (На рисунке Z, C и F.)
Регистры и флаги дают вместе внутреннее состояние процессора в любой момент времени, то есть полностью характеризуют его. Основные регистры процессора A,B,C,D (регистры общего назначения) универсальны, программист сам решает что в них будет "лежать", они полностью открыты для чтения/записи.

У процессора есть два регистра специального назначения
1) IP указатель команд
2) SP указатель стека
Это регистры-указатели, они содержат (хранят) значения из оперативной памяти, то есть они указывают (ссылаются) на некое место в оперативной памяти. Они получают значения автоматически и их не надо "трогать" без нужды.

Указатель команд IP указывает на следующую инструкцию в памяти (на ее адрес), которая должна быть выполнена (она вычисляется как местоположение исполнителя в памяти + длина текущей команды). Указатель вершины стека SP указывает на вершину стека (о нем будет сказано позже).
Процессор имеет три флага
1) Z нулевой флаг (сначала FALSE)
2) C флаг переноса (сначала FALSE)
3) F флаг ошибки (сначала FALSE)
Флаги хранят результаты результатов выполнения операций
1) Z=1, если внутри программы будет операция (типа 10-10)
2) C=1, если внутри программы был перенос (типа 200+200)
3) F=1, если была ошибка (типа 10/0)


Это оперативная память 256 байт

Этот машинный код простейшей программы Hello World! надо переписать в тетрадь. Желательно как-то выделить команды (синий фон).
К процессору* прикреплен блок оперативной памяти объемом 256 байт, у каждого байта есть свой адрес от 00 до FF (0-255). Это оперативная память (ее визуальное представление), в которую загружается машинный код, который получается из нашей программы на языке ассемблер. Программист может читать и записывать в эту память данные в произвольном порядке (оперативную память называют "память с произвольным доступом").
В эмуляторе память раскрашена для удобства программиста
1) на белом фоне данные
2) на синем фоне команды
3) на оранжевом фоне голова стека
4) на сером фоне специальные байты вывода
*эмулятору процессора на сайте, разумеется...
"Серые байты"*, которых ровно 24 штуки в самом конце "оперативной памяти" представляют собой специальную форму для вывода информации на экран. В любом реальном ассемблере вывод на экран информации (строки, символы, числа) не простая задача, но в данном эмуляторе есть дисплей из 24 символов, который связан с 24 крайними ячейками оперативной памяти.
Чтобы вывести на экран любой символ (буква, цифра и проч.) достаточно переслать его ASCII-код в любую ячейку оперативной памяти по адресу E8-FF (ячейки 232-255).
H[48] e[65] l[6C] l[6C] o[6F] пробел[20]
W[57] o[6F] r[72] l[6C] d[64] ![21]
*в реальном ассемблере ничего подобного нет...




Это внешний вид эмулятора сразу после выполнения простейшей программы (надпись на дисплее Hello World!)
Программа
Программа на ассемблере представляет собой последовательность инструкций, которые "говорят" процессору что делать. Инструкция (обычно) состоит из операции и (0-1-2) операндов. Каждая инструкция (операция) представляет собой готовую функцию, которая встроена (вшита) в процессор, она выполняется сразу же как до нее доходит очередь. Каждая инструкция (операция) имеет мнемоническую команду (краткое название, 3-4 символа).
Операнды команды работают как аргументы функции, они могут быть регистрами, местом в (оперативной) памяти или непосредственно данными (числами).
MOV C, hello
MOV D, 232
CALL print
HLTВ любом случае, все команды и операнды переводятся в (16-ричные) числа, вместо имен переменных подставляются адреса (по которым они располагаются), вместо имен процедур подставляются адреса (по которым они располагаются), сами команды (мнемоника) заменяются на байты, которые обозначают данные команды.
Эмулятор (слава богу) написан так, что 1 команда превращается в 1 байт, 1 параметр превращается в 1 байт.
байт байт байт
байт байт байт
байт байт
байт JMP start ; [1] 1F 02
start: ; [2]
MOV A, 65 ; [3] 06 00 41
MOV D, 232 ; [4] 06 03 E8
MOV [D], A ; [5] 05 03 00
HLT ; [6] 001) Переход (прыжок) на точку входа в программу (start)
2) Точка входа (между 1-2 обычно находятся данные)
3) Положить в регистр A 10-тичное число 65
4) Положить в регистр D 10-тичное число 232
5) Положить по адресу, который находится в регистре D значение, которое находится в регистре A (адрес 232=число 65)
6) Остановить выполнение программы
Вывод буквы A


Адреса и стек
В качестве параметров командам (операторам) используются числа. В ассемблере есть три способа ссылаться к физическому значению параметра (к числу).
1) Немедленная адресация значение задается после операции непосредственно числом (MOV A, 100 или MOV A, 0x64), значение записывается в машинный код сразу после кода операции (06 00 64), так писали наши деды (я не шучу!)
2) Прямая адресация значение находится где-то в памяти, но вместо него указывается адрес памяти, по которому находится нужное значение (самый популярный способ)
JMP start ; [1] 1F 08
hello: DB "Hello" ; [2] 48 65 6C 6C 6F
value: DB 123 ; [3] 7B
start: ; [4]
MOV C, hello ; [5] 06 02 02
MOV A, value ; [6] 06 00 07
HLT ; [7] 00
Строка hello и число value попадают в код как 02 и 07 соответственно (их значения находятся в памяти, а в коде находятся адреса, с которых они начинаются)
3) Косвенная адресация значение находится где-то в памяти, а адрес этого значения тоже находится где-то в памяти. Для получения числа надо указать адрес адреса.
Стек программы (куча) это специальная структура данных типа LIFO (last in first out последний пришел первый вышел). Стек представляет собой последовательность значений, которые "лежат" в стеке + указатель стека SP. Регистр SP всегда содержит то, что считается головой (вершиной) стека. Стек может расти вниз/вверх от старших адресов к младшим (и наоборот), это не имеет значения и зависит от реализации.
Добавить элемент в стек (PUSH x): 1) х копируется туда, куда указывает SP, 2) SP увеличивается (или уменьшается) до следующего адреса. В данной реализации уменьшается(!)
Взять элемент из стека (POP x): 1) скопировать значение, которое находится по адресу, на который указывает SP, 2) уменьшить (или увеличить) SP до следующего адреса. В данной реализации увеличивается(!)
Этот процессор имеет (слава богу) один стек. При старте программы указатель стека автоматически = E7(231). Стек растет вниз, то есть при PUSH он уменьшается, а при POP увеличивается. Для использования стека знать куда он растет не важно.

Стек используется для многих целей: для временного сохранения значений, для передачи аргументов между функциями, для отслеживания адресов возврата.
Выполнение программы
При исполнении программы (уже в машинных кодах) процессор выполняет инструкции одну за одной. Сначала он ищет инструкцию, на которую указывает указатель команд (IP), по коду операции определяет сколько у нее операндов (берет их) и исполняет ее. Выполнение инструкции приводит (возможно) к некоторым изменениям (оперативной) памяти и (возможно 99%) изменению внутреннего состояния. Далее все начинается снова...
При старте программы IP=0 (поэтому если в начале программы есть данные, то их надо перепрыгнуть командой JMP startpoint), далее он увеличивается и процессор будет работать пока не встретит команду HLT (HALT). При желании можно выполнять инструкции по шагам. Увеличение скорость процессора позволяет варьировать скорость шагов. Переключение вида чисел в регистрах (10/16) позволяет лучше понимать происходящее в них.

- Команда представляет собой мнемонику на языке ассемблера + операнды, разделенные запятой
- Регистры называются A,B,C,D и их можно использовать по этим именам в программе (в 1 регистр помещается 1 байт)
- Вместо явного указания адресов памяти можно использовать метки (например, start), это позволяет не думать об адресе, а подставлять метку в коде там, где нужен переход в определенное место кода (или значение)
- В программу можно включать произвольные данные с помощью директивы DB (hello: DB "Hello World!"), это на самом деле инструкция ассемблеру вставлять данные в программу как байты, а не собирать в этом месте код
- После ; идут комментарии, которые не попадают в конечный машинный код (после ; до конца строки ассемблер не рассматривает данные вообще...)
Дополнительно
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
1) JMP start | 1F 0F
Безусловный переход на метку start. Метка start маркирует команду MOV C, hello строка 5, хотя сама написана на строке 4. Фактически первая команда находится на строке 5, все* до нее это данные. Метка start называется точка входа.
* Кроме JMP start
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
2) hello: DB "Hello World!"
48 65 6C 6C 6F 20 57 6F 72 6C 64 21
Именованные данные, строка текста под меткой hello, идет в программу без компиляции как набор кодов символов, из которых состоит. Начинается с адреса 02.
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
3) DB 0 | 00
Не именованные данные, нулевой байт, находится сразу за строкой текста и служит для указания на ее окончание. Начинается с адреса 0E(14). Добавлять в конце строк байт-ноль очень частая практика при программировании на ассемблере.
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET4) start: | нет кода
Метка start, служит для маркировки начала программы (строка 5). У метки нет кода, просто ассемблер создает таблицу соответствий метки = адреса

JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
5) MOV C, hello | 06 02 02
Инициализация регистра С. В данном случае в него кладется адрес 02. Метка hello указывала на некие данные, которые начинаются с адреса 02, поэтому сейчас в регистре С находится 02, то есть адрес этих данных. Говорят, что "регистр С настроен (указывает) на строку hello".
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
6) MOV D, 232 | 06 03 E8
Инициализация регистра D непосредственно числом E8(232). Эти настройки нужны нам для того, чтобы
- C указывал на hello
- D указывал на 232
Все, что теперь нужно: пройти в цикле по строке hello и занести каждый ее байт по адресу 232+смещение(0,1,2,3,...)
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
7) CALL print | 38 18
Вызов процедуры print. Видно, что адрес процедуры 0x18 (24) и мы переносимся на строку 10, где процедура исполняется до команды RET (return, возврат). При "нырянии" в процедуру в стек записывается следующая после вызова процедуры команда (ее адрес), она равен 0x17(23). Стек начинает расти к младшим адресам...
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
10) PUSH A | 32 00
Сохранение в стеке значения регистра A. Это делается для каждого регистра, который "портится" в процедуре при выполнении команд. В начале процедуры PUSH, в конце процедуры обратный POP. В данном случае А=0, но важен принцип(!)
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
11) PUSH B | 32 01
Сохранение в стеке значения регистра B. Обратим внимание на то, что машинный код сохранения в стек регистра = 32, а номера регистров
- A=00
- B=01
- C=02
- D=03
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
12) MOV B, 0 | 06 01 00
Положить в регистр B число 0. В данном случае в B будет лежать байт-ноль, который нам нужен для поиска момента, когда при движении по строке вы наткнемся на такой же байт-ноль на ее конце.
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
14) MOV A, [C] | 03 02 02
Начало цикла .loop (метка). Цикл начинается в 14-ой строке и заканчивается в 19-ой строке. В первой строке цикла в регистр A мы кладем значение, на которое указывает адрес, который лежит в регистре С. Регистр А 1-байтовый, сейчас в нем символ "H" (или данное по адресу 02), говорят, что "регистр А настроен на H".
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
15) MOV [D], A | 05 03 00
По адресу, который хранится в регистре D надо положить то, что хранится в регистре A. В D хранился E8(232, первый серый адрес), а в А хранилось 48 (первый байт hello, символ "H"). После этой команды по адресу 232 будет положено 48 и (из-за реализации эмулятора) в первом квадратике дисплея появился символ "H".
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
16) INC C | 12 02
Увеличить значение в регистре C на 1. В регистре С было 02, а станет 03. Регистр С указывал на "H", а станет указывать на "e".
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
17) INC D | 12 03
Увеличить значение в регистре D на 1. В регистре D было E8, а станет E9. Регистр D указывал на ячейку 232, а станет указывать на ячейку 233.
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
18) CMP B, [C] | 15 01 02
Провести сравнение двух аргументов: 1) значения в регистре B (там байт-ноль) и 2) значение, которое хранится по адресу в С. Если аргументы равны, то поднимется (установится, зажжется) флаг Z=1. Сравнение в строке 18 даст либо Z=1 либо не тронет его и Z=0. Двенадцать раз флаг Z останется в 0, а потом станет 1.
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
19) JNZ .loop | 27 1F
Переход на метку .loop, если результат предыдущего сравнения не равен нулю. Мы перейдем на адрес 1F(31), если конец строки не достигнут (байт-ноль не достигнут). По адресу 0x1F(31) как раз находится строка 14 (MOV A, [C]).
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
20) POP B | 36 01
На эту строку мы попадем после окончания вывода строки, когда в 19-ой строке не произошел переход на начало цикла.
Вернуть из стека значение регистра B. Программа вернет 00 (это не важно), указатель головы стека SP получит приращение на 1 байт и переместится вперед по адресам.
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
21) POP A | 36 00
Вернуть из стека значение регистра A. Программа вернет 00 (это не важно), указатель головы стека SP получит приращение на 1 байт и переместится вперед по адресам.
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
22) RET | 39
Вернуться из процедуры. При этом из стека будет извлечен адрес следующей после команды CALL print команды (этой был адрес 0x17). После строки 22 стек вернулся в свое исходное состояние: SP указывает на E7, а указатель команд получил IP=0x17 и будет выполнять последнюю строку программы.
JMP start
hello: DB "Hello World!"
DB 0
start:
MOV C, hello
MOV D, 232
CALL print
HLT
print:
PUSH A
PUSH B
MOV B, 0
.loop:
MOV A, [C]
MOV [D], A
INC C
INC D
CMP B, [C]
JNZ .loop
POP B
POP A
RET
8) HLT | 00
Завершение программы. Исполнение кода прекращается, значения в регистрах замораживаются, в регистре IP=0x17 (адрес крайней исполненной команды).

assembler8_01
By Ivan Sedov
assembler8_01
- 614