Архитектура и система команд RISC-V
Базовая статья RISC-V и RISC-V
- Фиксированная длина команды — 32 бита
 - Адресуется побайтно
 - Трёхадресная строго регистровая система команд 
- операции над оперативной памятью — только обмен с регистрами
 - нет общедоступного регистра флагов
 условные переходы — атомарные операции
 32 регистра, из которых r0 — специальный, он содержит нестираемый 0, что само по себе удобно + используется для аппаратного ускорения (например, r0 в качестве приёмника означает «ничего никуда не записывать»)
- Регистры 32 битные (64-битные для rv64)
 Используется конвейер:
- выполнение одной инструкции разделается на 5 стадий, каждая из которых выполняется ровно за один такт работы процессора;
 - несколько инструкция на разных стадиях выполняются параллельно, если между ними нет зависимостей.
 
Рассмотрим наиболее примитивную модель процессора из эмулятора Ripes
Цикл работы процессора:
(F, Istruction Fetch)
Выборка очередной инструкции из памяти: в регистр PC заносится либо результат прибавления 4 к предыдущему значению PC, либо адрес перехода из Branch, а затем содержимое по этому адресу считывается в Istruction Memory
(D, Instruction decode)
- Декодирование опкода / функций
 Заполнение блока операндов Registers значениями из соответствующих регистров и непосредственного операнда в Immediate (если присутствует в инструкции)
(E, Execute operation)
Работа ALU: операции над содержимым блока операндов (регистрами и непосредственным значением) — сложение, сдвиг и прочие преобразования данных, а также вычисление адресов (перехода и адресации со смещением)
(M, Memory access)
- Работа с памятью (чтение / запись)
 
(W, Write back)
- Обновление блока операндов и самих регистров (самый последний элемент на схеме)
 
Система команд RISC-V
- 4 базовых типа команд 
 R — типа «регистр-регистр-регистр» (Register)
I — типа «непосредственное значение-регистр-регистр» (Immediate)
S — типа «регистр-регистр-непосредственное значение» (Store)
U — типа «непосредственное значение-регистр» (Upper)
 - Пояснения к схеме: 
opcode — код операции (6 битов)
rs1 — № регистра-источника (5 битов)
rs2 — № регистра-операнда (5 битов)
rd — № регистра-приёмника (5 битов)
imm[11:…] — непосредственный операнд размером в 12 битов
В случае, когда непосредственное значение определяет «приёмник» (смещение адреса для «близкого» перехода или записи результата в память), 12 битов целиком в поле rd не помещаются, и его приходится «распиливать» (инструкция типа S).
- Непосредственный операнд всегда знаковый, и его знак всегда приходится на 31-й бит. Это значит, что процессору легко отличить отрицательное число от положительного, даже если оно хранится в двух частях машинного слова: у отрицательного числа единичный 31-й бит
 
imm[31:…] — непосредственный операнд размером в 20 битов. Используется в инструкциях типа U для заполнения старших двадцати битов регистра (в операциях «далёкого» перехода и как дополнительная инструкция при записи в регистр полного 32-разрядного непосредственного операнда)
- 31-й бит снова знаковый!
 
funct — поле функции (6 битов), используется для разных инструкций, у которых код операции одинаковый. Например, все арифметические инструкции типа I имеют одинаковый opcode OP-IMM (чему он равен?), а различаются полем funct. По-видимому, для эффективной реализации R-команд в конвейере удобнее не декодировать опкод, а по-быстрому сравнить его с нулём, и получать значения регистров, параллельно декодируя функцию, чтобы потом её применить.
 Интерпретация значения imm может отличаться (например, в командах перехода в imm хранится смещение без последнего бита, так как адрес инструкции всегда чётен)
Система расширяема
«M» — целочисленное умножение и деление
«F», «D» и «Q» — вещественная арифметика 932, 64 и 128-битная)
«C» — «упакованные» инструкции (переменный размер команды)
«V — векторные операции
- …
 … и сужаема (соответствующие расширения можно не реализовывать, если они не нужны)
Стоит ещё раз заметить, что в ISA RISC-V невозможно хранить полный адрес непосредственно в инструкции (как это было в УМ). Полный адрес хранится в регистре (в зависимости от типа инструкции либо в pc, либо в x*), а в непосредственном операнде указывается смещение.
Язык ассемблера
Ассемблер: транслятор, преобразующий исходный текст некоторой программы из представления, удобного для человека, в машинные коды. Язык программирования, с которого происходит трансляция, называется языком ассемблера.
Задачи, решаемые ассемблером:
- Автоматическое вычисление адресов 
- Метки и адресная арифметика
 - Заполнение памяти и выравнивание
 
 - Читаемость 
- Ключевые слова и идентификаторы вместо чисел
 - Умолчания и сокращения (например, опускается регистр по умолчанию или нулевое смещение в косвенной адресации)
 Удобные формы команд (в т. ч. псевдоинструкции, в которых очевидный синтаксис транслируется в неочеваидную реализацию, например, копирование регистров — это сложение с нулём)
 - Однозначность получаемого машинного кода 
- Инструкция всегда транслируется в один и тот же машинный код
 Частичная взаимная однозначность: машинный код можно дизассемблерировать, восстановится всё, кроме имён и (если постараться, псевдоинструкции тоже можно восстановить)
 - Повторное использование 
- Макроопределения и макроподстановка
 - Поддержка нескольких исходных файлов
 
 - Поддержка ОС (библиотеки, исполняемый формат и т. п.) — часто выносится в отдельную программу-компоновщик (linker)
 
Название «сборщик», вероятно, происходит от способности сначала собирать метки в программе, а затем преобразовывать их в адреса. Кроме того, исходный код большой программы обычно разбит на несколько файлов, возможно, имелдась в виду сборка единого машинного кода из них.
Цикл программирования на языке ассемблера — классический для компилируемого языка:
- Написание текста программы
 - Трансляция в машинные коды (запускаемый файл)
 - Загрузка запускаемого файла в память и запуск
 
Специфика RARS: программа транслируется сразу в соответствующие места машинной памяти, нет необходимости хранить оттранслированный запускаемый файл.
Обзор ISA
Шпаргалка (NB! в RARS есть встроенная подсказака)
Общий вид инструкции ассемблера RISC-V:
[метка:] операция [операнд1[, операнд2[, …]]]
- Квадратные скобки означают необязательность (необязательны метки и количество операндов в зависимости от типа инструкции варьируется от 0 до 3)
 - В некоторых диалектах можно опускать запятые между операндами
 - Инструкция транслируется в одну машинную команду 
- Псевдоинструкция (см. ниже) может транслироваться в несколько команд
 
 - В качестве операндов, в зависимости от типа инструкции, могут выступать регистры и константы (в специальных случаях может добавляться четвёртый операнд, например, тип округления вещественного числа в виде мнемонического сокращения)
 
В тексте программы на языке ассемблера могут встречаться т. н. директивы — команды самому ассемблеру.
- В языке ассемблера RISC-V директивы начинаются с точки
 - Большая часть директив не транслируется в какое-либо содержимое машинной памяти. 
Например, в программе наверняка встретятся директива .text, обозначающая область программного кода, и .data, обозначающая область данных. Встретив такие директивы, ассемблер RARS поменяет адрес, начиная с которого он заполняет оперативную память оттранслированным кодом. Сами по себе эти директивы ни во что не превращаются
 Директивы размещения данных в памяти (например, .word или .asciz) интерпретируются как данные (числа или строки соответственно), и эти данные размещаются в очередные адреса памяти
 TODO Примеры инструкций (базовый набор?) 
Псевдоинструкции
Псевдоинструкции — это заложенные в спецификацию конструкции языка ассемблера, семантика которых не совпадает с машинными командами, в которые они раскрываются
- Синонимы для не вполне очевидных реализаций: 
Assembler Code Disassembler not t3 t2 0xfff3ce13 x28 x7 0xffffffffНепосредственная константа 0xfff занимает 12 битов
31-й бит единичный, так что это -1
При преобразовании в 32-разрядное число происходит распространение знака (поэтому дизассемблер показывает уже 0xffffffff, то есть 32-разрядное -1)
Побитовое исключающее или с ячейкой, все биты которой единичны, — это и есть искомый побитовый not
 - Наиболее эффективные из нескольких вариантов: 
Assembler Code Disassembler mv t1 t0 0x00500333 add x6 x0 x5Процессор может оптимизировать это «сложение», вообще не выполняя его, т. к. первый операнд — всегда нулевой регистр zero (x0)
 - В командах, относительно которых есть договорённость использовать некоторые конкретные регистры, эти регистры не указываются; можно не указывать и нулевые смещения 
Assembler Code Disassembler ret 0x00008067 jalr x0 x1 0Псевдоинструкция «возврат из подпрограммы» в действительности реализована как «текущее значение pc записать в регистр x0 (zero), перейти по адресу, хранящемуся в регистре x1 (ra) со смещением 0.
Тут тоже довольно много места для оптимизации со стороны процессора — например, ничего записывать в zero не надо
 Могут раскладываться в несколько последовательных операций
Assembler Code Disassembler call subr 0x00000317 auipc x6 0 0x010300e7 jalr x1 x6 16- Псевдоинструкция «вызов подпрограммы» реализована той же инструкцией!
 Во вспомогательный регистр x6 (t1 — это значение взято по умолчанию) должен попасть точный адрес перехода. Но самая длинная константа занимает 20 битов в команде типа U. Поэтому в этом регистре с помощью инструкции auipc формируется приблизительный адрес перехода — текущее значение pc, к которому прибавляется смещение, округлённое до 32-20=12 битов (в нашем случае 0)
Далее выполняется команда «записать в x1 (ra) адрес возврата (текущий +4), и перейти по адресу, хранящемуся в x6 со смещением 16»
- В зависимости от конкретных операндов могут раскрываться в разные команды 
Assembler Code Disassembler li t1 0x123 0x12300393 → addi x7 x0 0x00000123 li t0 0x1234567 0x01234337 → lui x6 0x00001234 0x56730313 addi x6 x6 0x00000567Пример псевдоинстркуции li приёмник константа (загрузка в регистр-приёмник непосредственного значения-константы) в двух вариантах: с небольшим и с большим числами
Нет никакой инструкции «положить число в регистр», зато есть инструкция «сложить I-число с регистром zero (нестираемым нулём) и положить результат в регистр»
Если константа в li больше 12 битов (не подходит для инструкции типа I), псевдоинструкция раскладывается в две:
записать старшие 20 битов 32-разрядной константы в старшие 20 битов регистра (младшие 12 битов регистра при этом обнуляются) — инструкция типа U,
добавить оставшиеся 12 битов константы в регистр — инструкция типа I
Соответственно, программисту не надо в уме прикидывать, влезает ли константа в 12 битов, и самому распиливать/объединять части, если не влезает.
- Надо помнить, что 12 битов включает в себя знаковый, так что 11 битов
 это — наиболее эффективные способы реализации команды li регистр число
 
Обратите также внимание на то, что к регистрам регистры можно обращаться как xРЕГИСТР, так и по их мнемонике (tРЕГИСТР, aРЕГИСТР, sРЕГИСТР, zero и т. п.), отражающей ковенции их использования.
 Найдите все поля инструкций типа I и U в колонке Code из примера выше. 
Полный список инструкций, поддерживаемых в RARS.
- В самом RARS есть интерактивная подсказка, её довольно удобно пользоваться, не зазубривая все инструкции заранее
 
Представление о «внешних вызовах» (environment call)
Низкоуровневое программирование (в машинных кодах или на языке ассемблера) требует некоторого количества высокоуровневых операций. Например, простейший ввод десятичных чисел с клавиатуры, если его реализовывать от начала до конца, становится крайне сложным мероприятием: надо управлять внешним устройством (клавиатурой), отслеживать появление на нём введённых символов, выявлять конец ввода этих символов, хранить их где-то, а после окончания ввода — преобразовывать из строкового представления (каждый символ — это один байт, обозначающий цифру) в десятичное число (одна ячейка памяти). И это мы ещё не договорились об обработке ошибок ввода!
Зачастую исполнитель низкоуровневых команд вообще не выполняет такие сложные операции, а передаёт их «на уровень выше»: кто-то или что-то, что запустило программу, пускай само позаботится о вводе и выводе. Например, в учебных машинах вводом и выводом занимается сама программа modelmachine по запросу программиста, а в системе команд вообще ничего про ввод и вывод нет.
В RISC-V это «обращение к запустившему нас окружению» включено в систему команд — это инструкция ecall. С её помощью образом программа может самостоятельно вводить и выводить данные (и выполнять множество других функций), если известно, что эти операции умеет выполнять окружение. Окружением (aka «то, что нас запустило») может при этом являться что угодно — ядро операционной системы, гипервизор, аппаратура, а в случае RARS — программный код на Java.
Список таких функций сильно зависит от природы окружения. Например, в RARS они определены так. От программиста требуется только положить в регистр a7 (x17) номер внешнего вызова согласно таблице, в регистры a0 - a6 (x10 - x16) — ожидаемые этим конкретным внешним вызовом параметры и выполнить инструкцию ecall.
По большей части в домашних заданиях мы будем пользоваться тремя внешними вызовами — ввод, вывод и останов.
   1         li      a7 5        # Внешний вызов №5 — ввести десятичное число
   2         ecall               # Результат — в регистре a0
   3         mv      t0 a0       # Сохраняем результат в t0
   4         ecall               # Регистр a7 не менялся, тот же внешний вызов
   5         add     a0 t0 a0    # Прибавляем ко второму число первое
   6         li      a7 1        # Внешний вызов №1 — вывести десятичное число
   7         ecall
   8         li      a7 10       # Внешний вызов №10 — останов программы
   9         ecall

