awesome-everything EN
↑ Обратно к восхождению

Базовый CS с нуля

Игрушечный CPU

Суть Собираем всё вместе: игрушечный CPU с двумя регистрами и системой из четырёх инструкций. Проходим маленькую программу — загрузить два числа, сложить их, сохранить результат — через цикл выборки–декодирования–исполнения шаг за шагом.
◷ 25 min

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

В этом уроке ты построишь мысленную модель полного — пусть и крошечного — CPU. У него два регистра, система из четырёх инструкций и небольшая память. Затем ты проследишь через него настоящую программу: загрузить два числа из памяти, сложить их, сохранить результат. Каждый шаг выборки, декодирования и исполнения показан явно. К концу CPU перестанет быть чёрным ящиком. Это будет машина, которую можно запустить в уме.

Цель

После этого урока ты сможешь описать компоненты минимального CPU (регистры, СК, память, АЛУ), читать и писать программы на игрушечной системе команд и пошагово трассировать цикл выборки–декодирования–исполнения для небольшой полной программы.

1

Компоненты игрушечного CPU. Наш игрушечный CPU содержит следующее:

  • Два регистра общего назначения: R0 и R1. Каждый хранит одно 8-битное значение (0–255).
  • Счётчик команд (СК): хранит адрес памяти следующей инструкции для выборки. Тоже 8-битный, поэтому может адресовать ячейки памяти 0–255.
  • Память: 256 ячеек, каждая хранит одно 8-битное значение. Адреса 0–7 будут хранить нашу программу (инструкции). Адреса 200, 201 и 202 — данные (входные числа и результат).
  • Арифметико-логическое устройство (АЛУ): схема, которая складывает два 8-битных значения и сохраняет результат в регистре.

Игрушечный CPU не умеет много — он создан, чтобы продемонстрировать основы, а не быть практичным. Реальные CPU имеют 16–31 регистр общего назначения, могут адресовать гигабайты памяти и системы команд с сотнями опкодов. Но принципы те же.

2

Игрушечная система команд. Наш CPU понимает ровно четыре инструкции. Каждая инструкция занимает ровно два байта: байт инструкции и байт операнда. СК продвигается на 2 после каждой не-JUMP инструкции.

Структура байта инструкции (байт 0):

Биты 7–6Бит 5Биты 4–0Смысл
опкодрегистр0 (не используется)опкод выбирает операцию; бит 5 выбирает регистр (0 = R0, 1 = R1)

Байт операнда (байт 1): 8-битное значение (0–255) — адрес памяти для LOAD/STORE/JUMP, или 0x00 (не используется) для ADD.

Четыре опкода:

Опкод (биты 7–6)Бит 5Байт 1НазваниеСмысл
00регадрес (0–255)LOAD Ra, addrЗагрузить память[addr] в Ra
01регадрес (0–255)STORE addr, RaСохранить Ra в память[addr]
1000x00 (не используется)ADD R0, R1R0 ← R0 + R1. Бит 5 и байт 1 игнорируются.
110адрес (0–255)JUMP addrУстановить СК на addr. Бит 5 игнорируется.

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

LOAD R0, 200:
  Байт 0 = 00_0_00000 = 0x00   (опкод 00 = LOAD, бит 5 = 0 → R0)
  Байт 1 = 11001000  = 0xC8    (десятичное 200 — адрес памяти)

LOAD R1, 201:
  Байт 0 = 00_1_00000 = 0x20   (опкод 00 = LOAD, бит 5 = 1 → R1)
  Байт 1 = 11001001  = 0xC9    (десятичное 201)

ADD R0, R1:
  Байт 0 = 10_0_00000 = 0x80   (опкод 10 = ADD; бит 5 и байт 1 не используются)
  Байт 1 = 00000000  = 0x00    (не используется, всегда 0)

STORE 202, R0:
  Байт 0 = 01_0_00000 = 0x40   (опкод 01 = STORE, бит 5 = 0 → R0)
  Байт 1 = 11001010  = 0xCA    (десятичное 202 — адрес назначения)

Поскольку байт 1 — это полное 8-битное поле, он может адресовать любую из 256 ячеек памяти — в том числе ячейки 200, 201 и 202. Вся наша программа укладывается в этот диапазон.

Почему это работает

Почему только четыре инструкции? В реальных системах команд сотни опкодов. Но для демонстрации всего существенного достаточно горстки: перемещение данных (LOAD/STORE), вычисление (ADD) и управление потоком (JUMP). Каждая более сложная инструкция — это комбинация или расширение этих идей. Курс nand2tetris использует столь же минимальную систему команд (называемую «Hack») для построения полностью рабочего компьютера с нуля; весь язык умещается в 16 бит.

3

Начальное состояние памяти. Перед запуском программы ОС загружает байты инструкций в память, начиная с адреса 0, и помещает входные данные по адресам 200 и 201. СК устанавливается на 0.

Каждая инструкция занимает 2 байта, поэтому четыре инструкции занимают адреса 0–7:

Адрес  Содержимое    Смысл
0      0x00          LOAD R0, 200 — байт 0 (байт инструкции)
1      0xC8            ← байт 1 (операнд: адрес 200)
2      0x20          LOAD R1, 201 — байт 0
3      0xC9            ← байт 1 (операнд: адрес 201)
4      0x80          ADD R0, R1 — байт 0
5      0x00            ← байт 1 (не используется)
6      0x40          STORE 202, R0 — байт 0
7      0xCA            ← байт 1 (операнд: адрес 202)
...
200    25              ← первое входное число
201    17              ← второе входное число
202    0               ← слот для результата (пусто)

Регистры до выполнения: R0 = 0, R1 = 0, СК = 0.

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

4

Счётчик команд управляет всем. Прежде чем подробно трассировать каждый цикл, обрати внимание на ключевую роль СК:

  • СК начинается с 0 — значит, первая выборка начинается по адресу 0.
  • После каждой не-JUMP инструкции СК увеличивается на 2 (поскольку каждая инструкция в нашем игрушечном CPU занимает 2 байта).
  • Если программа содержала бы JUMP, СК устанавливался бы на адрес перехода вместо этого.

СК — это курсор, проходящий через последовательность инструкций. Именно он — единственная причина, по которой CPU знает, какая инструкция «следующая».

LOAD R0,200
0–1
LOAD R1,201
2–3
ADD R0,R1
4–5
STORE 202,R0
6–7
···
···
25
200
17
201
0
202
Память до выполнения. Каждая инструкция занимает 2 байта. Инструкции занимают адреса 0–7. Входные данные — по адресам 200 и 201; слот результата по адресу 202 = 0. СК = 0 (выделенная инструкция).
Разбор примера

Полная трассировка программы из четырёх инструкций.

Отслеживаем: СК, R0, R1 и содержимое адреса памяти 202 после каждого цикла.


Цикл 1

Состояние до: СК = 0, R0 = 0, R1 = 0, память[202] = 0.

  • Выборка: читаем 2 байта начиная с СК = 0. Байты = 0x00 0xC8. Инструкция = LOAD R0, 200.
  • Декодирование: биты 7–6 байта 0 = 00 → LOAD. Бит 5 = 0 → регистр-назначение R0. Байт 1 = 0xC8 = 200 → поле адреса = 200.
  • Исполнение: читаем память[200]. Результат = 25. Записываем 25 в R0.
  • Продвижение СК: СК = 0 + 2 = 2.

Состояние после: СК = 2, R0 = 25, R1 = 0, память[202] = 0.


Цикл 2

Состояние до: СК = 2, R0 = 25, R1 = 0, память[202] = 0.

  • Выборка: читаем 2 байта начиная с СК = 2. Байты = 0x20 0xC9. Инструкция = LOAD R1, 201.
  • Декодирование: биты 7–6 байта 0 = 00 → LOAD. Бит 5 = 1 → регистр-назначение R1. Байт 1 = 0xC9 = 201 → поле адреса = 201.
  • Исполнение: читаем память[201]. Результат = 17. Записываем 17 в R1.
  • Продвижение СК: СК = 2 + 2 = 4.

Состояние после: СК = 4, R0 = 25, R1 = 17, память[202] = 0.


Цикл 3

Состояние до: СК = 4, R0 = 25, R1 = 17, память[202] = 0.

  • Выборка: читаем 2 байта начиная с СК = 4. Байты = 0x80 0x00. Инструкция = ADD R0, R1.
  • Декодирование: биты 7–6 байта 0 = 10 → ADD. Операнда-адреса нет. Источники = R0, R1. Назначение = R0.
  • Исполнение: АЛУ вычисляет R0 + R1 = 25 + 17 = 42. Записываем 42 в R0.
  • Продвижение СК: СК = 4 + 2 = 6.

Состояние после: СК = 6, R0 = 42, R1 = 17, память[202] = 0.


Цикл 4

Состояние до: СК = 6, R0 = 42, R1 = 17, память[202] = 0.

  • Выборка: читаем 2 байта начиная с СК = 6. Байты = 0x40 0xCA. Инструкция = STORE 202, R0.
  • Декодирование: биты 7–6 байта 0 = 01 → STORE. Бит 5 = 0 → регистр-источник R0. Байт 1 = 0xCA = 202 → адрес-назначение = 202.
  • Исполнение: записываем значение R0 (42) в память[202].
  • Продвижение СК: СК = 6 + 2 = 8.

Состояние после: СК = 8, R0 = 42, R1 = 17, память[202] = 42.


Программа завершена. Адрес памяти 202 теперь хранит 42 — сумму двух входных чисел (25 + 17). CPU попытается выбрать инструкцию по адресу 8, но нашей программе не нужна пятая инструкция, поэтому в реальной системе ОС завершила бы процесс до достижения адреса 8.

Всё вычисление потребовало ровно четыре цикла выборки–декодирования–исполнения. Каждый цикл имел одну и ту же трёхшаговую структуру. CPU никогда не нужно было «понимать», что именно он вычисляет — он просто выполнял свой цикл.

Граничные случаи

Что происходит, когда СК выходит за конец программы? В нашем примере СК достигнет адреса 8 и попытается выбрать байты, которые там находятся, — мусор или, возможно, код другой программы. Реальные ОС предотвращают это: двоичный файл программы содержит метаданные, сообщающие ОС, где заканчивается программа (или есть явная инструкция HALT, или системный вызов для завершения). ОС настраивает защиту памяти так, чтобы программа не могла выполняться за пределами своей области. На практике последняя инструкция почти каждой программы — вызов системного вызова «exit», который корректно завершает процесс.

Практика 0 / 5

В нашем игрушечном CPU каждая инструкция занимает 2 байта. После выполнения инструкции, начинающейся по адресу СК = 4 (не JUMP), каким становится СК?

До цикла 3 в нашей трассировке R0 = 25, R1 = 17. После выполнения ADD R0, R1 что хранит R0?

После цикла 4 (STORE 202, R0) в нашей трассировке что хранит адрес памяти 202?

Сколько циклов выборки–декодирования–исполнения потребовалось нашей программе из 4 инструкций для вычисления 25 + 17 и сохранения результата?

В начале цикла 1 СК = 0. После завершения всех 4 циклов СК = ?

Проверь себя
Викторина

В трассировке игрушечного CPU какова роль инструкции ADD по сравнению с инструкциями LOAD?

Итог

Минимальный CPU требует: регистров (быстрое рабочее хранилище — R0, R1), счётчика команд (регистра, хранящего адрес следующей инструкции), памяти (байтовых ячеек для инструкций и данных) и АЛУ (схемы, вычисляющей арифметику). Поведение CPU полностью описывается циклом выборки–декодирования–исполнения: выбрать байты инструкции по СК, декодировать опкод и операнды, исполнить соответствующее действие, продвинуть СК. В нашей игрушечной системе команд каждая инструкция занимает 2 байта — байт инструкции (опкод + выбор регистра) и байт операнда (8-битный адрес или значение) — поэтому СК продвигается на 2 за цикл и все 256 адресов памяти достижимы. Программа из четырёх инструкций (LOAD, LOAD, ADD, STORE) выполняется за четыре цикла и вычисляет сумму без того, чтобы CPU когда-либо «знал», что он складывает — он просто выполнял свой цикл. Каждая более крупная программа — какой бы сложной она ни была — это тот же цикл, работающий дольше, на большем количестве инструкций, с большим регистровым файлом и большей памятью. Механизм никогда не меняется.

Продолжить восхождение ↑Процессор: тест с множественным выбором
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.