Основы программирования на языке Assembler под архитектуру RISC-V

Набор непривилегированных инструкций RISC-V

Разбить на страницы
Показывать лекцию целиком

Эта Лекция раскрывает основы непривилегированных целочисленных инструкций архитектуры RISC-V и содержит базовую информацию для написания программ на языке Assembler.

После освоения этой лекции вы должны быть способны:

  • понимать, что архитектура RISC-V определяется через набор инструкций и его расширения;
  • понимать принцип перевода команд на языке Assembler в машинный код;
  • знать основные категории целочисленных инструкций.
  • Набор непривилегированных инструкций RISC-V

    Основной набор целочисленных инструкций

    Набор непривилегированных инструкций архитектуры RISC-V описывает четыре базовые набора инструкций:

  • RV32I (32-х битные целочисленные)
  • RV32E (32-х битные встроенные)
  • RV64I (64-х битные целочисленные)
  • RV128I (128-х битные целочисленные)
  • Числа 32, 64, и 128 характеризуют размер регистра и адресного пространства. Например, 32-х битный регистр может хранить данные размером до 2^32 бит. Набор инструкций RV32I содержит всего 40 инструкций.

    Расширения и именования

    Любой процессор RISC-V поддерживает как минимум один базовый набор инструкций. Например, обозначение RV32I означает, что процессор поддерживает 32-х битный целочисленный набор команд. Распространены дальнейшие расширения базового набора инструкций. Тип расширения, который поддерживает процессор, указывается в качестве суффикса к базовой аббревиатуре набора инструкций. Распространены следующие расширения (таблица 3.1):

    Сокращение Стандартное расширение
    M целочисленные умножение и деление integer multiplication and division
    A атомарные инструкции atomic instructions
    F числа с плавающей запятой одинарной точности single-precision floating point
    D числа с плавающей запятой двойной точности double-precision floating point
    Q числа с плавающей запятой четверной точности quad-precision floating point
    C сжатые инструкции compressed instructions
    V векторные операции vector operations

    Примеры:

  • микропроцессор, построенный на архитектуре RV32IMAC, поддерживает базовый набор 32-х битных инструкций (RV32I), целочисленные умножение и деление (M), атомарные инструкции (A) и сжатые инструкции (C).
  • микропроцессор, построенные на архитектуре RV64IMAFDC поддерживает базовый набор 64-х инструкций (RV64I), целочисленные умножение и деление(M), атомарные инструкции (A), числа с плавающей запятой одинарной (F) и двойной точности (D), а также сжатые инструкции (C).
  • иногда можно встретить микропроцессоры, для которых полная информация зашита внутри процессора и может быть прочитана только после чтения специального регистра. В этом случае суффиксы будут оканчиваться на SU, например, RV64IMAFDCSU. Такое обозначение означает, что процессор поддерживает режим супервизора (S) и пользовательский режим (U).
  • Регистры RISC-V

    Базовый набор инструкций определяет наличие 32 регистров и программного счётчика. Регистры именуются от x0 до x31. Размер регистра соответствует размеру базовых инструкций, например, для процессора RV32I размер каждого из регистров будет составлять 32 бита. Регистр x0 хранит значение 0, которое нельзя поменять.

    Расширения предоставляют большее число регистров. Например, расширения, связанные с вычислениями с плавающей запятой, предоставляют дополнительные регистры работы с дробной арифметикой.

    Использование регистров языками более высокого уровня, чем Assembler, определяется бинарным интерфейсом для приложений (application binary interface, ABI). Этот интерфейс определяет то, как регистры должны использоваться в процессе трансляции программы компилятором из описания на высокоуровневом языке в машинный код. ABI предписывает использование дополнительных имён регистров и их назначение ( Табл.3.2):

    Регистр Имя ABI Описание
    x0 zero Константный ноль Zero constant
    x1 ra Адрес возврата Return address
    x2 sp Указатель на область стека Stack pointer
    x3 gp Глобальный указатель Global pointer
    x4 tp Указатель потока Thread pointer
    x5-x7 t0-t2 Временные значения Temporaries
    x8 s0 / fp Сохраняемый регистр / Указатель на фрейм Saved / Frame pointer
    x9 s1 Сохраняемый регистр Saved register
    x10-x11 a0-a1 Аргументы функций и возвращаемые значения Function args. / return values
    x12-x17 a2-a7 Аргументы функций Function arguments
    x18-x27 s2-s11 Сохраняемые регистры Saved registers
    x28-x31 t3-t6 Временные значения Temporaries
    Pc - Программный счётчик Program counter

    Архитектура RISC-V предполагает выполнение арифметических операций через регистры: для модификации данных, находящихся в памяти, необходимо загрузить данные из памяти в один из регистров, выполнить операцию над данными, и переместить результат из регистра обратно в память.

    Обзор инструкций

    Простой пример

    Для понимания принципа использования инструкций, рассмотрим простой пример, в котором используется только одна инструкция: addi rd, rs1, imm, которая добавляет константное целое imm (в диапазоне 12-ти битного знакового: -2048…2047) к содержимому регистра rs1 и записывает результат в регистр rd. Другими словами выполняется операция rd = rs1 + imm. После получения инструкции программный счётчик инкрементируется и будет содержать адрес следующей инструкции, что можно условно записать как pc = cp + 4, поскольку размер инструкции в машинном коде равен 4 байтам.

    Инструкция addi x1, x0, 1 добавляет число 1 к значению 0, которое хранится в регистре x0 (в регистре x0 всегда хранится значение 0) и пишет результат, равный 1, в регистр x1. Машинный код инструкции addi x1, x0, 1 - это (в шестнадцатеричном представлении) 0x00100093. Префикс 0x перед числом показывает, что число задано в шестнадцатеричной системе счисления. Далее мы увидим, как именно работает кодирование чисел. По аналогии, инструкция addi x1, x1, 1 добавляет значение 1 к содержимому регистра x1 и записывает результат обратно в регистр x1. Машинный код этой инструкции - 0x00108093.

    Теперь представим, что эти машинные коды располагаются в памяти начиная с адреса 0x0. Поскольку каждая инструкция занимает 4 байта, первая инструкция располагается по адресу 0x0, а вторая - по адресу 0x4. Программный счётчик указывает на начало исполняемого кода, следовательно, он равен значению 0x0:

    Адрес     Машинный код     Инструкции
    0x0       0x00100093       addi x1, x0, 1
    0x4       0x00108093       addi x1, x1, 1
    

    После запуска этих двух строк кода микропроцессор начинает выполнение с адреса 0x0. Как уже было объяснено выше, в результате выполнения первой строки в регистре x1 будет получено число 1, при этом программный счётчик будет инкрементирован до значения 0x4. Выполнение второй строки приведёт к увеличению значения регистра x1 на 1. Так как его значение было равно 1 в результате предыдущей операции, регистр после выполнения второй строки примет значение, равное 2.

    На рис.3.1 показан пример того, как этот исходный код запускается в симуляторе Ripes.

    (рис 3.1) Запуск рассмотренного примера в симуляторе Ripes

    Кодирование инструкций

    Базовые целочисленные инструкции в микропроцессорах архитектуры RISC-V разделены на пять типов, каждый из которых имеет свой тип кодирования. Типы соответствуют задачам, выполняемым машинной инструкцией: R-type (register, регистр), I-type (immediate, непосредственное значение), S-type (store, хранение), B-type (branch, ветвление), U-type (upper immediate, верхнее непосредственное значение), и J-type (jump, переход). Таблица 3.3 ниже показывает принцип кодирования:

    Рассмотрим предыдущий пример. Машинный код инструкции addi x1, x0, 1 - это значение 0x00100093. Побайтное представление кода может быть получено из таблицы выше, зная, что код операции равен 0x13, значение funct3 равно 0x0, и что код типа I (целочисленная операция).

    Таким образом, машинный код инструкции addi x1, x0, 1 в побитовом представлении формируется следующим образом:

    Поскольку каждые 4 бита могут быть объединены в шестнадцатеричное число, машинный код получается равным 0x00100093.

    Арифметические и логические операции (с непосредственными значениями)

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

    Инструкция Тип Формат Код funct3 Описание
    addi ADD Immediate I 0010011 0x0 rd = rs1 + imm
    xori XOR Immediate I 0010011 0x4 rd = rs1 ^ imm
    ori OR Immediate I 0010011 0x4 rd = rs1 | imm
    andi AND Immediate I 0010011 0x7 rd = rs1 imm
    slli Shift Left Logical Imm. I 0010011 0x1 imm[11:5]=0x00, rd = rs1 << imm[4:0]
    srli Shift Right Logical Imm. I 0010011 0x5 imm[11:5]=0x00, rd = rs1 << imm[4:0]
    srai Shift Right Arith. Imm. I 0010011 0x5 imm[11:5]=0x20, rd = rs1 >> imm[4:0]
    slti Set Less Than Imm. I 0010011 0x2 rd = (rs1 < imm)? 0:1
    sltiu Set Less Than Imm. Un. I 0010011 0x3 rd = (rs1 < imm)? 0:1

    Как уже было показано выше, инструкции этого типа берут непосредственное значение imm, в качестве источника используется регистр rs1, в качестве регистра назначения - rd.

    Поскольку инструкция addi rd, rs1, imm использует знаковое непосредственное значение imm, она может сработать как для сложения (если imm меняется от 0 до 2047), так и для вычитания (imm меняется от -2048 до -1).

    Инструкции xori, ori и andi rd, rs1, imm выполняют побитовые операции xor (исключающее или), or (логическое сложение) и and (логическое умножение). Например, пусть в регистре x1 лежит значение 0x9, которому в побитовом представлении соответствует число 0b1001, тогда инструкция xori x1, x1, 0x3 выполнит побитовое исключающее или над содержимым регистра x1 и числом 0x3, что можно представить следующим образом:

    x1 = x1 xor 0x3 = 0b1001 xor 0b0011 = 0b1010 = 0xa
    

    Инструкции slli, srli, и srai выполняют операции побитового сдвига с использованием непосредственного значения, задающего шаг сдвига. В качестве такого непосредственного значения может выступать положительное пятибитное число, представляющее значения от 0 до 31.

    Код операции логического (побитового) сдвига влево синтаксически описывается следующим образом: slli rd, rs1, imm, и в процессе выполнения сдвигает каждый бит регистра, задаваемого как rs1, на imm позиций влево. Результат при этом записывается в регистр rd. При каждом сдвиге на 1 бит младший значащий бит устанавливается в 0, а значение старшего значащего бита теряется.

    По аналогии с этим выполняется команда побитового сдвига вправо: srli rd, rs1, imm. Значение регистра rs1 побитового сдвигается вправо, результат записывается в регистр, задаваемый как rd. При этом в старший значащий бит при каждом сдвиге записывается значение 0, а значение младшего значащего бита теряется.

    Результат выполнения команды srai в плане заполнение старшего бита отличается для арифметического сдвига вправо для команды srai rd, rs1, imm. Информация о старшем значащем бите для этой операции сохраняется. Если до сдвига он был равен 0, старший бит заполняется 0 после сдвига. Если он был равен 1, он заполняется 1. Такой сдвиг является знакорасширяющим для значений в rs1, которые записаны в дополнительном коде.

    Рассмотрим пример. Пусть значение регистра x1 равно 0x9, двоичное представление которого записывается как 0b1001. Тогда в результате выполнения инструкции slli x1, x1, 0x2 выполнится логический сдвиг содержимого регистра x1, который можно описать следующим кодом:

    x1 = x1 << 0x2 = 0b1001 << 2 = 0b100100 = 0x24
    

    Оставшиеся две инструкции slti rd, rs1, imm и sltiu rd, rs1, imm сравнивают содержимое регистра-источника с непосредственным значением и устанавливают значение регистра-приёмника в 0 в случае, если значение в регистре-источнике меньше непосредственного значения imm. Отличие между этими двумя командами в том, что команда slti использует значения в дополнительном коде и сравнение происходит со знаком.

    Арифметические и логические операции (с регистрами)

    Следующая категория R-формата включает в себя арифметические и логические операции, которые используют три регистра: два регистра-источника и регистр-приёмник. Большинство этих инструкций по своему действию аналогичны рассмотренным выше. К этим инструкциям относятся (табл.3.5):

    Инструкция Тип Формат Код funct3 funct7 Описание
    Add ADD R 0110011 0x0 0x00 rd = rs1 + rs2
    Sub SUB R 0110011 0x0 0x20 rd = rs1 - rs2
    Xor XOR R 0110011 0x4 0x00 rd = rs1 ^ rs2
    or OR R 0110011 0x6 0x00 rd = rs1 | rs2
    and AND R 0110011 0x7 0x00 rd = rs1 rs2
    sll Shift Left Logical R 0110011 0x1 0x00 rd = rs1 << rs2
    srl Shift Right Logical R 0110011 0x5 0x00 rd = rs1 >> rs2
    sra Set Right Arith. R 0110011 0x5 0x20 rd = rs1 >> rs2
    slt Set Less Than R 0110011 0x2 0x00 Rd = (rs1 < rs2)? 0:1
    sltu Set Less Than Un. R 0110011 0x3 0x00 rd = (rs1 < rs2)? 0:1

    Рассмотрим дополнение предыдущего примера с учётом новых инструкций:

    Адрес		Машинный код       Инструкции
    0x0		0x00100093		addi x1, x0, 1
    0x4		0x00108093		addi x1, x1, 1
    0x8		0x00108133		add  x2, x1, x1
    

    В последней строке примера содержимое регистра x1 добавляется к самому себе, результат записывается в регистр x2. В результате выполнения всех операций в регистре x2 содержится число 4.

    Все эти инструкции в отдельном рассмотрении не нуждаются, поскольку являются функциональными аналогами операций из предыдущего раздела. Исключением является инструкция sub, но, как нам кажется, её функциональность является очевидной и в рассмотрении не нуждается.

    Загрузка и сохранение

    Для того, чтобы получить доступ к памяти, реализован ряд инструкций, которые относятся к I-формату и предназначены для загрузки данных из памяти в регистры, а также ряд инструкций, относящихся к S-формату (S-Format, Save) для сохранения данных из регистров в память. Инструкции указаны в таблице 3.6:

    Инструкция Имя Формат Код funct3 Описание
    lb Load Byte I 0000011 0x0 rd = M[rs1+imm][7:0]
    lh Load Half I 0000011 0x1 rd = M[rs1+imm][15:0]
    lw Load Word I 0000011 0x2 rd = M[rs1+imm][31:0]
    lbu Load Byte Un. I 0000011 0x0 Rd = M[rs1+imm][7:0]
    lhu Load Half Un. I 0010011 0x0 rd = M[rs1+imm][15:0]
    sb Store Byte I 0010011 0x0 M[rs1+imm][7:0] = rs2[7:0]
    sh Store Half I 0100011 0x1 M[rs1+imm][15:0] = rs2[15:0]
    sw Store Word I 0100011 0x2 M[rs1+imm][31:0] = rs2[31:0]

    Рассмотрим следующий пример. Предполагается, что в памяти по адресу 0x10000000находится значение 0x02.

    Адрес		Машинный код       Значение		
    0x0		0x00100093		addi x1, x0, 0x1
    0x4		0x01c09093		slli x1, x1, 28
    0x8		0x00008103		lb x2, 0(x1)
    

    Первые две инструкции записывают в регистр x1 значение 0x10000000. Для этого в регистр x1 записывается число 1, после чего сдвигается влево на 28 позиций, в результате чего в регистр x1 формируется значение 0x10000000. Третья инструкция загружает байт из памяти по адресу, задаваемому регистром x1.

    Синтаксис вызова этой команды следующий: lb rd, imm(rs1). Это можно развернуть в следующее представление: rd = M[rs1+imm][7:0]. Значение одного байта, задаваемого конструкций bits[7:0], загружается в регистра, определяемый как rd (register destination) из памяти по адресу, взятому из регистра, определяемого как rs1 (Register Source), к которому добавляется непосредственное значение смещения imm.

    Из приведённого примера видно, что задавать адрес в регистре с использованием описанных инструкций довольно неудобно. Для исправления этого был разработан ряд инструкций U-формата (U-Format, Upper) (Табл.3.7):

    Инструкция Имя Формат Код funct3 Описание
    lui Load Upper Imm. U 0110111 - rd = imm << 12
    auipc Add Upper Imm. to PC U 0010111 - rd = PC + (imm << 12)

    Инструкция lui rd, imm задаёт значение регистра, определяемого как rd, равным непосредственному значению imm, сдвинутому на 12 бит влево. А инструкция auipc rd, imm устанавливает значение регистра, определяемого как rd, в результат сложения значения регистра программного счётчика PC и непосредственного значения imm, сдвинутого влево на 12 бит.

    Разберём следующий пример:

    Адрес    Машинный код   Значение	           Комментарий
    0x0      0x100000b7	     lui   x1, 0x10000  Загружает 0x10000000 в регистр x1
    0x4      0x10000117	     auipc x2, 0x10000  Загружает 0x10000004 в регистр x2
    

    Инструкция lui x1, 0x10000 задаёт регистру x1 значение 0x10000 << 12, таким образом, x1 = 0x10000000, поскольку сдвиг влево на 12 бит "добавляет" три нуля в шестнадцатеричном представлении. Инструкция auipc x2, 0x10000 устанавливает содержимое регистра x2 равным сумме текущего значения PC (счётчика команд), которое в данный момент равно 0x04 с прибавленным значением 0x10000 << 12. Таким образом, значение регистра x2 после выполнения этого кода будет равно 0x10000004.

    Управление

    Каждая из предыдущих рассмотренных инструкций устанавливает программный счётчик PC на адрес следующей инструкции, расположенной в памяти: pc = pc + 4. Для реализации возможности контроля за процессом исполнения используются инструкции B-формата (B-Format, Branching), которые реализуют механизм ветвления. Они используются для условного перехода к инструкции, адрес которой указывается непосредственно в составе соответствующих команд в случае, если условие выполняется (Табл.3.8):

    Инструкция Тип Формат Код funct3 Описание
    beq Branch == B 1100011 0x0 if (rs1 == rs2) pc+=imm
    bne Branch != B 1100011 0x1 If (rs1 != rs2) pc+=imm
    blt Branch < B 1100011 0x4 if (rs1 < rs2) pc+=imm
    bge Branch >= B 1100011 0x5 if (rs1 >= rs2) pc+=imm
    bltu Branch < Un. B 1100011 0x6 if (rs1 < rs2) pc+=imm
    bleu Branch >= Un. B 1100011 0x7 if (rs1 >= rs2) pc+=imm

    Инструкция ветвления сравнивает значения регистров rs1 и rs2 способом, определяемым самой инструкцией. Если условие истинно, программный счётчик увеличивается на непосредственное значение imm. В выражении blt rs1, rs2, imm проверяется, что rs1 меньше, чем rs2, и если это так, программный счётчик PC изменяется следующим образом: pc = pc + imm. В противном случае он принимает значение адреса следующей инструкции в памяти (pc = pc +4).

    Инструкции blt и bge учитывают знак числа, инструкции bltu и bleu - нет, воспринимают числа как беззнаковые.

    Рассмотрим пример:

    Адрес    Машинный код   Значение           Комментарий
    0x00     0xffb00093     addi x1, x0, -5    x1 = -5
    0x04     0x00500113     addi x2, x0, 5     x2 = 5
    0x08     0x0020c463     blt  x1, x2, 0x8   if (x1 < x2) pc = pc + 8 
    0x0c     0x00100193     addi x3, x0, 1     skipped if (x1 < x2): x3 = 1
    0x10     0x00200193     addi x3, x0, 2     x3 = 2
    

    Инструкция по адресу 0x08 проверяет условие, что x1 < x2, то есть -5 < 5. Поскольку выражение истинно, программный счётчик устанавливается в значение 0x10 и инструкция по адресу 0x0c пропускается (не выполняется). Если бы инструкция по адресу 0x08 была bltu, отрицательное значение, хранящиеся в регистре x1 (-5) интерпретировалось бы как положительное значение, равное 0xFFFFFFFB (представление положительного числа, в двоичном коде идентичного отрицательному числу -5), а значит условие бы не выполнилось.

    Помимо инструкций для условного перехода, есть инструкции для безусловного перехода: jalr и jal. Эти инструкции позволяют переходить на требуемый адрес путём модификации программного счётчика PC, однако при этом сохраняют адрес возврата, который равен PC+4 на момент вызова инструкции. Такой подход позволяет вернуться в то место, откуда произошёл переход. Именно так реализуется модульность разрабатываемых программ.

    Инструкции записываются в разных форматах (Табл.3.9):

    Инструкция Тип Формат Код funct3 Описание
    jal Jump and Link J 1101111 - rd = PC+4; PC += imm
    jalr Jump and Link Register I 1100111 0x0 rd = PC+4; PC = rs + imm

    Инструкция jal rd, imm добавляет непосредственное значение к программному счётчику PC, PC += imm, и пишет адрес инструкции, следующей за jal в регистр, задаваемый как rd. Инструкция jalr rd, rs, imm устанавливает программный счётчик в значение, равное результату сложения регистра, задаваемого как rs, и непосредственного значения imm, pc = rs + imm, и пишет адрес возврата в регистр, задаваемый как rd.

    Рассмотрим пример:

    Адрес     Машинный код      Значение         Комментарий
    0x00      0x00c000ef        jal x1,0xc       set pc to 0x0 + 0xc = 0xc, 
                                                 x1 = pc + 4 = 0x4
    0x04      0x00000013        addi x0, x0, 0   no operation
    0x08      0x00010093        addi x1, x2, 0   set x1 = x2
    0x0c      0x00008167        jalr x2, x1, 0   set pc to x1, 
                                                 x2 = pc + 4 = 0x10
    0x10      0x00100093        addi x1, x0, 1   x1 = 1
    

    Программа работает следующим образом. Она начинается с инструкции jal x1, 0xc, которая расположена по адресу 0x00. В результате своего выполнения адрес возврата пишется в регистр x1 (следующая инструкция за инструкцией по адресу 0x00 размещается по адресу 0x04, именно это число помещается в регистр x1) и осуществляется переход по адресу 0x0c. По адресу 0x0c размещается инструкция jalr x2, x1, 0, которая пишет уже свой адрес возврата (значение 0x10) в регистр x2 и осуществляет переход по адресу, который лежит в регистре x1 (там размещается адрес возврата для первого выполненного перехода, это адрес 0x04). Программа продолжает выполняться и исполняется инструкция, расположенная по адресу 0x04, которая ничего не делает. Следующая за ней инструкция, расположенная по адресу 0x08) записывает значение 0x10 (адрес возврата для команды jalr) в регистр x1. После этого, вторая итерация выполнения команды jalr по адресу 0xc передаёт управление по адресу 0x10 (значение регистра x1). Итого, программный счётчик PC последовательного принимает следующие значения: 0x00, 0x0c, 0x04, 0x08, 0x0c, и 0x10.

    Системный интерфейс

    Две инструкции, ecall и ebreak задаются в I-формате (I-format, immediate), и обе используются для доступа к системным функциям. Системные функции обычно требуют привилегированного доступа и наличия инструкций, которые лежат вне пользовательского режима доступа.

    Инструкция Тип Формат Код funct3 Описание
    ecall Environment Call I 1110011 0x0 imm = 0, rd = rs1 = 0, передать управление системе
    ebreak Environment Break I 1110011 0x0 imm = 1, rd = rs1 = 0, передать управление отладчику

    Инструкция ecall запрашивает системную сервисную операцию, так называемый системный вызов. Согласно бинарному интерфейсу для приложений ABI, идентификатор и дополнительные параметры предоставляются через предварительно настроенные регистры, при этом значение регистра x17 задаёт тип системного вызова. В следующем примере показан вызов функции выхода в ОС Linux. Системный вызов exit говорит ОС Linux о том, что ОС должна закончить выполнение программы и выгрузить программу из памяти.

    Адрес       Машинный код     Значение           Комментарий
    0x00        0x05d00893       addi x17, x0, 93   set x17 = 93,
                                                    Linux system call exit
    0x04        0x00000073       ecall              call system
    

    Инструкция ebreak используется отладчиком для отладки программ. Отладчик временно подменяет инструкцией ebreak код в том месте, где программа должна остановиться. Ситуация, когда вызов инструкции ebreak реализуется программистом вручную, нетипична.

    Упорядочение памяти

    Наконец, инструкция fence используется для синхронизации доступа к памяти между различными процессорами, использующими общую область памяти. Инструкция fence разделяет программный код на код, идущий до неё (предшествующий набор, predecessor set) и инструкции после неё (последующий набор, successor set). Команда полезна для систем, использующих более одного процессорного ядра RISC-V.

    Инструкция fence заботится о том, чтобы каждая инструкция из предшествующего ей набора была выполнена исполняющим ядром (и так для каждого ядра) перед тем, как будет выполнена первая инструкция из последующего за ней набора. Инструкция fence очень важна для реализации синхронизации процессов в операционных системах, в том числе в таких механизмах, как механизмы блокировки. Однако эти механизмы выходят за пределы рассматриваемого курса и упоминаются лишь для полноты картины. Обычная инструкция fence имеет следующий I-формат (Табл.3.11):

    Инструкция Тип Формат Код funct3 Описание
    fence Fence I 0001111 0x0 rd, rs1 зарезервированы. Для всех типов доступа к памяти imm = 0b000011111111.

    'M'-расширение команд

    В качестве примера расширений, рассмотрим M-расширение, реализующее операции умножения и деления. Инструкции приведены в таблице 3.12:

    Инструкция Тип Формат Код funct3 funct7 instruction
    mul Multiply R 0110011 0x0 0x01 rd = (rs1 * rs2)[31:0]
    mulh Multiply High R 0110011 0x1 0x01 rd = (rs1 * rs2)[63:32]
    mulsu Multiply High Sign/Uns. R 0110011 0x3 0x01 rd = (rs1 * rs2)[63:32]
    mulu Multiply Unsigned R 0110011 0x3 0x01 rd = (rs1 * rs2)[63:32]
    div Divide R 0110011 0x4 0x01 rd = rs1 / rs2
    divu Divide Unsigned R 0110011 0x5 0x01 rd = rs1 / rs2
    rem Remainder R 0110011 0x6 0x01 rd = rs1 % rs2
    remu Remainder Unsigned R 0110011 0x7 0x01 rd = rs1 % rs2

    Поскольку перемножение двух (потенциально больших) чисел может превысить размер регистра (для RV32 этот размер составляет 32 бита), младшая часть результата умножения возвращается с использованием инструкции mul, а старшая часть - с использованием инструкций mulh, mulsu, mulu в зависимости от того, какая комбинация чисел в плане знака используется. Например:

    Адрес       Машинный код    Значение            Комментарий
    0x00        0x000100b7      lui x1, 0x10        x1 = 0x00010000
    0x04        0x00108093      addi x1, x1, 1      x1 = 0x00010001
    0x08        0x00080137      lui x2, 0x80        x2 = 0x00080000
    0x0c        0x022081b3      mul x3, x1, x2      x3 = 0x00080000, 
                                                    lower 32 bits of mult.
    0x10        0x02209233      mulh x4, x1, x2     x4 = 0x00000008, 
                                                    higher 32 bits of mult.
    

    Деление на нуль, результат которого не определён, записывает в регистр-приёмник значение -1 (или, другими словами, выставляет все биты в 1). Это значит, что перед выполнением операции деления программисту необходимо самостоятельно проверять делитель и использовать операторы ветвления для обработки исключительных ситуаций.

    Замечания касательно архитектуры RV64

    В целом, для архитектуры RV64 набор инструкций такой же, как и для архитектуры RV32. Однако для RV64 регистры имеют размер в 64 бита, а не в 32, и операции выполняются над 64-битными числами. Более того, для RV64 реализуется ряд инструкций, специфичных именно для 64-битной архитектуры, такие инструкции имеют в конце букву 'w'. Такие инструкции работают только с 32-битными числами, причём используются младшие 32 бита то 64-битных чисел. Примерами таких инструкций являются addiw, addw, sllw, mulw.

    Контрольные вопросы

  • Какие наборы инструкций RISC V являются базовыми?
  • Какой набор инструкций поддерживает процессор с архитектурой RV128IMFDV?
  • Для чего используются регистры x10-x17?
  • Чем отличаются различные форматы/типы инструкций?
  • Можно ли выполнить арифметическую операцию над значениями, не загруженными в регистр?
  • Закодируйте инструкцию andi x1, x0, 7
  • Среди арифметических инструкций есть инструкции add, sub, addi. Почему нет инструкции subi?
  • Инструкцией какого типа является lb x2, 0(x1)?
  • Можно ли явно менять значение регистра PC?
  • Какая инструкция используется для совершения системного вызова?
  • Какой из барьеров памяти реализует инструкция fence?
  • Можно ли в архитектуре RV32IM с помощью одной инструкции получить результат умножения 46340 * 46341?
  • Страницы:

    Эта Лекция раскрывает основы непривилегированных целочисленных инструкций архитектуры RISC-V и содержит базовую информацию для написания программ на языке Assembler.

    После освоения этой лекции вы должны быть способны:

  • понимать, что архитектура RISC-V определяется через набор инструкций и его расширения;
  • понимать принцип перевода команд на языке Assembler в машинный код;
  • знать основные категории целочисленных инструкций.
  • Набор непривилегированных инструкций RISC-V

    Основной набор целочисленных инструкций

    Набор непривилегированных инструкций архитектуры RISC-V описывает четыре базовые набора инструкций:

  • RV32I (32-х битные целочисленные)
  • RV32E (32-х битные встроенные)
  • RV64I (64-х битные целочисленные)
  • RV128I (128-х битные целочисленные)
  • Числа 32, 64, и 128 характеризуют размер регистра и адресного пространства. Например, 32-х битный регистр может хранить данные размером до 2^32 бит. Набор инструкций RV32I содержит всего 40 инструкций.

    Расширения и именования

    Любой процессор RISC-V поддерживает как минимум один базовый набор инструкций. Например, обозначение RV32I означает, что процессор поддерживает 32-х битный целочисленный набор команд. Распространены дальнейшие расширения базового набора инструкций. Тип расширения, который поддерживает процессор, указывается в качестве суффикса к базовой аббревиатуре набора инструкций. Распространены следующие расширения (таблица 3.1):

    Сокращение Стандартное расширение
    M целочисленные умножение и деление integer multiplication and division
    A атомарные инструкции atomic instructions
    F числа с плавающей запятой одинарной точности single-precision floating point
    D числа с плавающей запятой двойной точности double-precision floating point
    Q числа с плавающей запятой четверной точности quad-precision floating point
    C сжатые инструкции compressed instructions
    V векторные операции vector operations

    Примеры:

  • микропроцессор, построенный на архитектуре RV32IMAC, поддерживает базовый набор 32-х битных инструкций (RV32I), целочисленные умножение и деление (M), атомарные инструкции (A) и сжатые инструкции (C).
  • микропроцессор, построенные на архитектуре RV64IMAFDC поддерживает базовый набор 64-х инструкций (RV64I), целочисленные умножение и деление(M), атомарные инструкции (A), числа с плавающей запятой одинарной (F) и двойной точности (D), а также сжатые инструкции (C).
  • иногда можно встретить микропроцессоры, для которых полная информация зашита внутри процессора и может быть прочитана только после чтения специального регистра. В этом случае суффиксы будут оканчиваться на SU, например, RV64IMAFDCSU. Такое обозначение означает, что процессор поддерживает режим супервизора (S) и пользовательский режим (U).
  • Регистры RISC-V

    Базовый набор инструкций определяет наличие 32 регистров и программного счётчика. Регистры именуются от x0 до x31. Размер регистра соответствует размеру базовых инструкций, например, для процессора RV32I размер каждого из регистров будет составлять 32 бита. Регистр x0 хранит значение 0, которое нельзя поменять.

    Расширения предоставляют большее число регистров. Например, расширения, связанные с вычислениями с плавающей запятой, предоставляют дополнительные регистры работы с дробной арифметикой.

    Использование регистров языками более высокого уровня, чем Assembler, определяется бинарным интерфейсом для приложений (application binary interface, ABI). Этот интерфейс определяет то, как регистры должны использоваться в процессе трансляции программы компилятором из описания на высокоуровневом языке в машинный код. ABI предписывает использование дополнительных имён регистров и их назначение ( Табл.3.2):

    Регистр Имя ABI Описание
    x0 zero Константный ноль Zero constant
    x1 ra Адрес возврата Return address
    x2 sp Указатель на область стека Stack pointer
    x3 gp Глобальный указатель Global pointer
    x4 tp Указатель потока Thread pointer
    x5-x7 t0-t2 Временные значения Temporaries
    x8 s0 / fp Сохраняемый регистр / Указатель на фрейм Saved / Frame pointer
    x9 s1 Сохраняемый регистр Saved register
    x10-x11 a0-a1 Аргументы функций и возвращаемые значения Function args. / return values
    x12-x17 a2-a7 Аргументы функций Function arguments
    x18-x27 s2-s11 Сохраняемые регистры Saved registers
    x28-x31 t3-t6 Временные значения Temporaries
    Pc - Программный счётчик Program counter

    Архитектура RISC-V предполагает выполнение арифметических операций через регистры: для модификации данных, находящихся в памяти, необходимо загрузить данные из памяти в один из регистров, выполнить операцию над данными, и переместить результат из регистра обратно в память.

    Обзор инструкций

    Простой пример

    Для понимания принципа использования инструкций, рассмотрим простой пример, в котором используется только одна инструкция: addi rd, rs1, imm, которая добавляет константное целое imm (в диапазоне 12-ти битного знакового: -2048…2047) к содержимому регистра rs1 и записывает результат в регистр rd. Другими словами выполняется операция rd = rs1 + imm. После получения инструкции программный счётчик инкрементируется и будет содержать адрес следующей инструкции, что можно условно записать как pc = cp + 4, поскольку размер инструкции в машинном коде равен 4 байтам.

    Инструкция addi x1, x0, 1 добавляет число 1 к значению 0, которое хранится в регистре x0 (в регистре x0 всегда хранится значение 0) и пишет результат, равный 1, в регистр x1. Машинный код инструкции addi x1, x0, 1 - это (в шестнадцатеричном представлении) 0x00100093. Префикс 0x перед числом показывает, что число задано в шестнадцатеричной системе счисления. Далее мы увидим, как именно работает кодирование чисел. По аналогии, инструкция addi x1, x1, 1 добавляет значение 1 к содержимому регистра x1 и записывает результат обратно в регистр x1. Машинный код этой инструкции - 0x00108093.

    Теперь представим, что эти машинные коды располагаются в памяти начиная с адреса 0x0. Поскольку каждая инструкция занимает 4 байта, первая инструкция располагается по адресу 0x0, а вторая - по адресу 0x4. Программный счётчик указывает на начало исполняемого кода, следовательно, он равен значению 0x0:

    Адрес     Машинный код     Инструкции
    0x0       0x00100093       addi x1, x0, 1
    0x4       0x00108093       addi x1, x1, 1
    

    После запуска этих двух строк кода микропроцессор начинает выполнение с адреса 0x0. Как уже было объяснено выше, в результате выполнения первой строки в регистре x1 будет получено число 1, при этом программный счётчик будет инкрементирован до значения 0x4. Выполнение второй строки приведёт к увеличению значения регистра x1 на 1. Так как его значение было равно 1 в результате предыдущей операции, регистр после выполнения второй строки примет значение, равное 2.

    На рис.3.1 показан пример того, как этот исходный код запускается в симуляторе Ripes.

    (рис 3.1) Запуск рассмотренного примера в симуляторе Ripes

    Кодирование инструкций

    Базовые целочисленные инструкции в микропроцессорах архитектуры RISC-V разделены на пять типов, каждый из которых имеет свой тип кодирования. Типы соответствуют задачам, выполняемым машинной инструкцией: R-type (register, регистр), I-type (immediate, непосредственное значение), S-type (store, хранение), B-type (branch, ветвление), U-type (upper immediate, верхнее непосредственное значение), и J-type (jump, переход). Таблица 3.3 ниже показывает принцип кодирования:

    Рассмотрим предыдущий пример. Машинный код инструкции addi x1, x0, 1 - это значение 0x00100093. Побайтное представление кода может быть получено из таблицы выше, зная, что код операции равен 0x13, значение funct3 равно 0x0, и что код типа I (целочисленная операция).

    Таким образом, машинный код инструкции addi x1, x0, 1 в побитовом представлении формируется следующим образом:

    Поскольку каждые 4 бита могут быть объединены в шестнадцатеричное число, машинный код получается равным 0x00100093.

    Арифметические и логические операции (с непосредственными значениями)

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

    Инструкция Тип Формат Код funct3 Описание
    addi ADD Immediate I 0010011 0x0 rd = rs1 + imm
    xori XOR Immediate I 0010011 0x4 rd = rs1 ^ imm
    ori OR Immediate I 0010011 0x4 rd = rs1 | imm
    andi AND Immediate I 0010011 0x7 rd = rs1 imm
    slli Shift Left Logical Imm. I 0010011 0x1 imm[11:5]=0x00, rd = rs1 << imm[4:0]
    srli Shift Right Logical Imm. I 0010011 0x5 imm[11:5]=0x00, rd = rs1 << imm[4:0]
    srai Shift Right Arith. Imm. I 0010011 0x5 imm[11:5]=0x20, rd = rs1 >> imm[4:0]
    slti Set Less Than Imm. I 0010011 0x2 rd = (rs1 < imm)? 0:1
    sltiu Set Less Than Imm. Un. I 0010011 0x3 rd = (rs1 < imm)? 0:1

    Как уже было показано выше, инструкции этого типа берут непосредственное значение imm, в качестве источника используется регистр rs1, в качестве регистра назначения - rd.

    Поскольку инструкция addi rd, rs1, imm использует знаковое непосредственное значение imm, она может сработать как для сложения (если imm меняется от 0 до 2047), так и для вычитания (imm меняется от -2048 до -1).

    Инструкции xori, ori и andi rd, rs1, imm выполняют побитовые операции xor (исключающее или), or (логическое сложение) и and (логическое умножение). Например, пусть в регистре x1 лежит значение 0x9, которому в побитовом представлении соответствует число 0b1001, тогда инструкция xori x1, x1, 0x3 выполнит побитовое исключающее или над содержимым регистра x1 и числом 0x3, что можно представить следующим образом:

    x1 = x1 xor 0x3 = 0b1001 xor 0b0011 = 0b1010 = 0xa
    

    Инструкции slli, srli, и srai выполняют операции побитового сдвига с использованием непосредственного значения, задающего шаг сдвига. В качестве такого непосредственного значения может выступать положительное пятибитное число, представляющее значения от 0 до 31.

    Код операции логического (побитового) сдвига влево синтаксически описывается следующим образом: slli rd, rs1, imm, и в процессе выполнения сдвигает каждый бит регистра, задаваемого как rs1, на imm позиций влево. Результат при этом записывается в регистр rd. При каждом сдвиге на 1 бит младший значащий бит устанавливается в 0, а значение старшего значащего бита теряется.

    По аналогии с этим выполняется команда побитового сдвига вправо: srli rd, rs1, imm. Значение регистра rs1 побитового сдвигается вправо, результат записывается в регистр, задаваемый как rd. При этом в старший значащий бит при каждом сдвиге записывается значение 0, а значение младшего значащего бита теряется.

    Результат выполнения команды srai в плане заполнение старшего бита отличается для арифметического сдвига вправо для команды srai rd, rs1, imm. Информация о старшем значащем бите для этой операции сохраняется. Если до сдвига он был равен 0, старший бит заполняется 0 после сдвига. Если он был равен 1, он заполняется 1. Такой сдвиг является знакорасширяющим для значений в rs1, которые записаны в дополнительном коде.

    Рассмотрим пример. Пусть значение регистра x1 равно 0x9, двоичное представление которого записывается как 0b1001. Тогда в результате выполнения инструкции slli x1, x1, 0x2 выполнится логический сдвиг содержимого регистра x1, который можно описать следующим кодом:

    x1 = x1 << 0x2 = 0b1001 << 2 = 0b100100 = 0x24
    

    Оставшиеся две инструкции slti rd, rs1, imm и sltiu rd, rs1, imm сравнивают содержимое регистра-источника с непосредственным значением и устанавливают значение регистра-приёмника в 0 в случае, если значение в регистре-источнике меньше непосредственного значения imm. Отличие между этими двумя командами в том, что команда slti использует значения в дополнительном коде и сравнение происходит со знаком.

    Арифметические и логические операции (с регистрами)

    Следующая категория R-формата включает в себя арифметические и логические операции, которые используют три регистра: два регистра-источника и регистр-приёмник. Большинство этих инструкций по своему действию аналогичны рассмотренным выше. К этим инструкциям относятся (табл.3.5):

    Инструкция Тип Формат Код funct3 funct7 Описание
    Add ADD R 0110011 0x0 0x00 rd = rs1 + rs2
    Sub SUB R 0110011 0x0 0x20 rd = rs1 - rs2
    Xor XOR R 0110011 0x4 0x00 rd = rs1 ^ rs2
    or OR R 0110011 0x6 0x00 rd = rs1 | rs2
    and AND R 0110011 0x7 0x00 rd = rs1 rs2
    sll Shift Left Logical R 0110011 0x1 0x00 rd = rs1 << rs2
    srl Shift Right Logical R 0110011 0x5 0x00 rd = rs1 >> rs2
    sra Set Right Arith. R 0110011 0x5 0x20 rd = rs1 >> rs2
    slt Set Less Than R 0110011 0x2 0x00 Rd = (rs1 < rs2)? 0:1
    sltu Set Less Than Un. R 0110011 0x3 0x00 rd = (rs1 < rs2)? 0:1

    Рассмотрим дополнение предыдущего примера с учётом новых инструкций:

    Адрес		Машинный код       Инструкции
    0x0		0x00100093		addi x1, x0, 1
    0x4		0x00108093		addi x1, x1, 1
    0x8		0x00108133		add  x2, x1, x1
    

    В последней строке примера содержимое регистра x1 добавляется к самому себе, результат записывается в регистр x2. В результате выполнения всех операций в регистре x2 содержится число 4.

    Все эти инструкции в отдельном рассмотрении не нуждаются, поскольку являются функциональными аналогами операций из предыдущего раздела. Исключением является инструкция sub, но, как нам кажется, её функциональность является очевидной и в рассмотрении не нуждается.

    Загрузка и сохранение

    Для того, чтобы получить доступ к памяти, реализован ряд инструкций, которые относятся к I-формату и предназначены для загрузки данных из памяти в регистры, а также ряд инструкций, относящихся к S-формату (S-Format, Save) для сохранения данных из регистров в память. Инструкции указаны в таблице 3.6:

    Инструкция Имя Формат Код funct3 Описание
    lb Load Byte I 0000011 0x0 rd = M[rs1+imm][7:0]
    lh Load Half I 0000011 0x1 rd = M[rs1+imm][15:0]
    lw Load Word I 0000011 0x2 rd = M[rs1+imm][31:0]
    lbu Load Byte Un. I 0000011 0x0 Rd = M[rs1+imm][7:0]
    lhu Load Half Un. I 0010011 0x0 rd = M[rs1+imm][15:0]
    sb Store Byte I 0010011 0x0 M[rs1+imm][7:0] = rs2[7:0]
    sh Store Half I 0100011 0x1 M[rs1+imm][15:0] = rs2[15:0]
    sw Store Word I 0100011 0x2 M[rs1+imm][31:0] = rs2[31:0]

    Рассмотрим следующий пример. Предполагается, что в памяти по адресу 0x10000000находится значение 0x02.

    Адрес		Машинный код       Значение		
    0x0		0x00100093		addi x1, x0, 0x1
    0x4		0x01c09093		slli x1, x1, 28
    0x8		0x00008103		lb x2, 0(x1)
    

    Первые две инструкции записывают в регистр x1 значение 0x10000000. Для этого в регистр x1 записывается число 1, после чего сдвигается влево на 28 позиций, в результате чего в регистр x1 формируется значение 0x10000000. Третья инструкция загружает байт из памяти по адресу, задаваемому регистром x1.

    Синтаксис вызова этой команды следующий: lb rd, imm(rs1). Это можно развернуть в следующее представление: rd = M[rs1+imm][7:0]. Значение одного байта, задаваемого конструкций bits[7:0], загружается в регистра, определяемый как rd (register destination) из памяти по адресу, взятому из регистра, определяемого как rs1 (Register Source), к которому добавляется непосредственное значение смещения imm.

    Из приведённого примера видно, что задавать адрес в регистре с использованием описанных инструкций довольно неудобно. Для исправления этого был разработан ряд инструкций U-формата (U-Format, Upper) (Табл.3.7):

    Инструкция Имя Формат Код funct3 Описание
    lui Load Upper Imm. U 0110111 - rd = imm << 12
    auipc Add Upper Imm. to PC U 0010111 - rd = PC + (imm << 12)

    Инструкция lui rd, imm задаёт значение регистра, определяемого как rd, равным непосредственному значению imm, сдвинутому на 12 бит влево. А инструкция auipc rd, imm устанавливает значение регистра, определяемого как rd, в результат сложения значения регистра программного счётчика PC и непосредственного значения imm, сдвинутого влево на 12 бит.

    Разберём следующий пример:

    Адрес    Машинный код   Значение	           Комментарий
    0x0      0x100000b7	     lui   x1, 0x10000  Загружает 0x10000000 в регистр x1
    0x4      0x10000117	     auipc x2, 0x10000  Загружает 0x10000004 в регистр x2
    

    Инструкция lui x1, 0x10000 задаёт регистру x1 значение 0x10000 << 12, таким образом, x1 = 0x10000000, поскольку сдвиг влево на 12 бит "добавляет" три нуля в шестнадцатеричном представлении. Инструкция auipc x2, 0x10000 устанавливает содержимое регистра x2 равным сумме текущего значения PC (счётчика команд), которое в данный момент равно 0x04 с прибавленным значением 0x10000 << 12. Таким образом, значение регистра x2 после выполнения этого кода будет равно 0x10000004.

    Управление

    Каждая из предыдущих рассмотренных инструкций устанавливает программный счётчик PC на адрес следующей инструкции, расположенной в памяти: pc = pc + 4. Для реализации возможности контроля за процессом исполнения используются инструкции B-формата (B-Format, Branching), которые реализуют механизм ветвления. Они используются для условного перехода к инструкции, адрес которой указывается непосредственно в составе соответствующих команд в случае, если условие выполняется (Табл.3.8):

    Инструкция Тип Формат Код funct3 Описание
    beq Branch == B 1100011 0x0 if (rs1 == rs2) pc+=imm
    bne Branch != B 1100011 0x1 If (rs1 != rs2) pc+=imm
    blt Branch < B 1100011 0x4 if (rs1 < rs2) pc+=imm
    bge Branch >= B 1100011 0x5 if (rs1 >= rs2) pc+=imm
    bltu Branch < Un. B 1100011 0x6 if (rs1 < rs2) pc+=imm
    bleu Branch >= Un. B 1100011 0x7 if (rs1 >= rs2) pc+=imm

    Инструкция ветвления сравнивает значения регистров rs1 и rs2 способом, определяемым самой инструкцией. Если условие истинно, программный счётчик увеличивается на непосредственное значение imm. В выражении blt rs1, rs2, imm проверяется, что rs1 меньше, чем rs2, и если это так, программный счётчик PC изменяется следующим образом: pc = pc + imm. В противном случае он принимает значение адреса следующей инструкции в памяти (pc = pc +4).

    Инструкции blt и bge учитывают знак числа, инструкции bltu и bleu - нет, воспринимают числа как беззнаковые.

    Рассмотрим пример:

    Адрес    Машинный код   Значение           Комментарий
    0x00     0xffb00093     addi x1, x0, -5    x1 = -5
    0x04     0x00500113     addi x2, x0, 5     x2 = 5
    0x08     0x0020c463     blt  x1, x2, 0x8   if (x1 < x2) pc = pc + 8 
    0x0c     0x00100193     addi x3, x0, 1     skipped if (x1 < x2): x3 = 1
    0x10     0x00200193     addi x3, x0, 2     x3 = 2
    

    Инструкция по адресу 0x08 проверяет условие, что x1 < x2, то есть -5 < 5. Поскольку выражение истинно, программный счётчик устанавливается в значение 0x10 и инструкция по адресу 0x0c пропускается (не выполняется). Если бы инструкция по адресу 0x08 была bltu, отрицательное значение, хранящиеся в регистре x1 (-5) интерпретировалось бы как положительное значение, равное 0xFFFFFFFB (представление положительного числа, в двоичном коде идентичного отрицательному числу -5), а значит условие бы не выполнилось.

    Помимо инструкций для условного перехода, есть инструкции для безусловного перехода: jalr и jal. Эти инструкции позволяют переходить на требуемый адрес путём модификации программного счётчика PC, однако при этом сохраняют адрес возврата, который равен PC+4 на момент вызова инструкции. Такой подход позволяет вернуться в то место, откуда произошёл переход. Именно так реализуется модульность разрабатываемых программ.

    Инструкции записываются в разных форматах (Табл.3.9):

    Инструкция Тип Формат Код funct3 Описание
    jal Jump and Link J 1101111 - rd = PC+4; PC += imm
    jalr Jump and Link Register I 1100111 0x0 rd = PC+4; PC = rs + imm

    Инструкция jal rd, imm добавляет непосредственное значение к программному счётчику PC, PC += imm, и пишет адрес инструкции, следующей за jal в регистр, задаваемый как rd. Инструкция jalr rd, rs, imm устанавливает программный счётчик в значение, равное результату сложения регистра, задаваемого как rs, и непосредственного значения imm, pc = rs + imm, и пишет адрес возврата в регистр, задаваемый как rd.

    Рассмотрим пример:

    Адрес     Машинный код      Значение         Комментарий
    0x00      0x00c000ef        jal x1,0xc       set pc to 0x0 + 0xc = 0xc, 
                                                 x1 = pc + 4 = 0x4
    0x04      0x00000013        addi x0, x0, 0   no operation
    0x08      0x00010093        addi x1, x2, 0   set x1 = x2
    0x0c      0x00008167        jalr x2, x1, 0   set pc to x1, 
                                                 x2 = pc + 4 = 0x10
    0x10      0x00100093        addi x1, x0, 1   x1 = 1
    

    Программа работает следующим образом. Она начинается с инструкции jal x1, 0xc, которая расположена по адресу 0x00. В результате своего выполнения адрес возврата пишется в регистр x1 (следующая инструкция за инструкцией по адресу 0x00 размещается по адресу 0x04, именно это число помещается в регистр x1) и осуществляется переход по адресу 0x0c. По адресу 0x0c размещается инструкция jalr x2, x1, 0, которая пишет уже свой адрес возврата (значение 0x10) в регистр x2 и осуществляет переход по адресу, который лежит в регистре x1 (там размещается адрес возврата для первого выполненного перехода, это адрес 0x04). Программа продолжает выполняться и исполняется инструкция, расположенная по адресу 0x04, которая ничего не делает. Следующая за ней инструкция, расположенная по адресу 0x08) записывает значение 0x10 (адрес возврата для команды jalr) в регистр x1. После этого, вторая итерация выполнения команды jalr по адресу 0xc передаёт управление по адресу 0x10 (значение регистра x1). Итого, программный счётчик PC последовательного принимает следующие значения: 0x00, 0x0c, 0x04, 0x08, 0x0c, и 0x10.

    Системный интерфейс

    Две инструкции, ecall и ebreak задаются в I-формате (I-format, immediate), и обе используются для доступа к системным функциям. Системные функции обычно требуют привилегированного доступа и наличия инструкций, которые лежат вне пользовательского режима доступа.

    Инструкция Тип Формат Код funct3 Описание
    ecall Environment Call I 1110011 0x0 imm = 0, rd = rs1 = 0, передать управление системе
    ebreak Environment Break I 1110011 0x0 imm = 1, rd = rs1 = 0, передать управление отладчику

    Инструкция ecall запрашивает системную сервисную операцию, так называемый системный вызов. Согласно бинарному интерфейсу для приложений ABI, идентификатор и дополнительные параметры предоставляются через предварительно настроенные регистры, при этом значение регистра x17 задаёт тип системного вызова. В следующем примере показан вызов функции выхода в ОС Linux. Системный вызов exit говорит ОС Linux о том, что ОС должна закончить выполнение программы и выгрузить программу из памяти.

    Адрес       Машинный код     Значение           Комментарий
    0x00        0x05d00893       addi x17, x0, 93   set x17 = 93,
                                                    Linux system call exit
    0x04        0x00000073       ecall              call system
    

    Инструкция ebreak используется отладчиком для отладки программ. Отладчик временно подменяет инструкцией ebreak код в том месте, где программа должна остановиться. Ситуация, когда вызов инструкции ebreak реализуется программистом вручную, нетипична.

    Упорядочение памяти

    Наконец, инструкция fence используется для синхронизации доступа к памяти между различными процессорами, использующими общую область памяти. Инструкция fence разделяет программный код на код, идущий до неё (предшествующий набор, predecessor set) и инструкции после неё (последующий набор, successor set). Команда полезна для систем, использующих более одного процессорного ядра RISC-V.

    Инструкция fence заботится о том, чтобы каждая инструкция из предшествующего ей набора была выполнена исполняющим ядром (и так для каждого ядра) перед тем, как будет выполнена первая инструкция из последующего за ней набора. Инструкция fence очень важна для реализации синхронизации процессов в операционных системах, в том числе в таких механизмах, как механизмы блокировки. Однако эти механизмы выходят за пределы рассматриваемого курса и упоминаются лишь для полноты картины. Обычная инструкция fence имеет следующий I-формат (Табл.3.11):

    Инструкция Тип Формат Код funct3 Описание
    fence Fence I 0001111 0x0 rd, rs1 зарезервированы. Для всех типов доступа к памяти imm = 0b000011111111.

    'M'-расширение команд

    В качестве примера расширений, рассмотрим M-расширение, реализующее операции умножения и деления. Инструкции приведены в таблице 3.12:

    Инструкция Тип Формат Код funct3 funct7 instruction
    mul Multiply R 0110011 0x0 0x01 rd = (rs1 * rs2)[31:0]
    mulh Multiply High R 0110011 0x1 0x01 rd = (rs1 * rs2)[63:32]
    mulsu Multiply High Sign/Uns. R 0110011 0x3 0x01 rd = (rs1 * rs2)[63:32]
    mulu Multiply Unsigned R 0110011 0x3 0x01 rd = (rs1 * rs2)[63:32]
    div Divide R 0110011 0x4 0x01 rd = rs1 / rs2
    divu Divide Unsigned R 0110011 0x5 0x01 rd = rs1 / rs2
    rem Remainder R 0110011 0x6 0x01 rd = rs1 % rs2
    remu Remainder Unsigned R 0110011 0x7 0x01 rd = rs1 % rs2

    Поскольку перемножение двух (потенциально больших) чисел может превысить размер регистра (для RV32 этот размер составляет 32 бита), младшая часть результата умножения возвращается с использованием инструкции mul, а старшая часть - с использованием инструкций mulh, mulsu, mulu в зависимости от того, какая комбинация чисел в плане знака используется. Например:

    Адрес       Машинный код    Значение            Комментарий
    0x00        0x000100b7      lui x1, 0x10        x1 = 0x00010000
    0x04        0x00108093      addi x1, x1, 1      x1 = 0x00010001
    0x08        0x00080137      lui x2, 0x80        x2 = 0x00080000
    0x0c        0x022081b3      mul x3, x1, x2      x3 = 0x00080000, 
                                                    lower 32 bits of mult.
    0x10        0x02209233      mulh x4, x1, x2     x4 = 0x00000008, 
                                                    higher 32 bits of mult.
    

    Деление на нуль, результат которого не определён, записывает в регистр-приёмник значение -1 (или, другими словами, выставляет все биты в 1). Это значит, что перед выполнением операции деления программисту необходимо самостоятельно проверять делитель и использовать операторы ветвления для обработки исключительных ситуаций.

    Замечания касательно архитектуры RV64

    В целом, для архитектуры RV64 набор инструкций такой же, как и для архитектуры RV32. Однако для RV64 регистры имеют размер в 64 бита, а не в 32, и операции выполняются над 64-битными числами. Более того, для RV64 реализуется ряд инструкций, специфичных именно для 64-битной архитектуры, такие инструкции имеют в конце букву 'w'. Такие инструкции работают только с 32-битными числами, причём используются младшие 32 бита то 64-битных чисел. Примерами таких инструкций являются addiw, addw, sllw, mulw.

    Контрольные вопросы

  • Какие наборы инструкций RISC V являются базовыми?
  • Какой набор инструкций поддерживает процессор с архитектурой RV128IMFDV?
  • Для чего используются регистры x10-x17?
  • Чем отличаются различные форматы/типы инструкций?
  • Можно ли выполнить арифметическую операцию над значениями, не загруженными в регистр?
  • Закодируйте инструкцию andi x1, x0, 7
  • Среди арифметических инструкций есть инструкции add, sub, addi. Почему нет инструкции subi?
  • Инструкцией какого типа является lb x2, 0(x1)?
  • Можно ли явно менять значение регистра PC?
  • Какая инструкция используется для совершения системного вызова?
  • Какой из барьеров памяти реализует инструкция fence?
  • Можно ли в архитектуре RV32IM с помощью одной инструкции получить результат умножения 46340 * 46341?
  • Вернуться к учебному плану