Регистры и адресация со смещением

Вступление: понятие о конвенциях

  • Risc-V наследует «Правило MIPS»:
    • машинный код предназначен для эффективного декодирования/выполнения
    • инструкции языка ассемблера — для удобного написания
  • ⇒ Не только 1:1 отображение содержательных обозначений в код, но и
    • некоторое заранее оговорённое преобразование программы
    • дисциплина написания программ
  • Дисциплина:
    • Использование регистров общего назначения
    • Моделирование полезных структур данных (например, стека)
  • Обратное следствие: соблюдение дисциплины поможет ещё эффективнее проектировать процессор
    • если в коде используются оговорённые дисциплиной регистры (например, для перехода на подпрограмму), можно организовать скрытый аппаратный стек для их хранения (стек RISC-V находится в медленно оперативной мапяти)
    • можно попытаться изваять в камне длинную псевдоинструкцию, если окажется, что это выгодно

Конвенции

Конвенциядоговорённость о стиле написания программ, в которой ограничивается использование возможностей ЭВМ

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

    • Например, использование некоторых регистров ограничено для передачи параметров и получения значений функции, для указателей на различные области памяти (стек, фрейм, глобальную…); требуется или не требуется сохранение исходных значений при завершении подпрограммы и т. п. В действительности все регистры MIPS, кроме двух, работают одинаково
  • Некоторые конвенции предполагают наличие типичных программных механизмов на работающей ЭВМ.
    • Например, часть регистров используется для взаимодействия с ядром ОС, хотя его может и не быть.
    • Тому же служит карта памяти, в которой задано расположение .text, .data, .ktext и .kdata и т. п. На самом деле значения «по умолчанию» можно переключить— но об этом следует договориться в рамках всех программ, запускаемых на данной ЭВМ

  • Множество конвенций порождены спецификой архитектуры ЭВМ
    • Например, $1 ($at) используется для хранения адреса при трансляции псевдоинструкций. Псевдоинструкции образовались в борьбе с избыточностью системы команд
  • Некоторые «возможности» таковыми не являются.
    • Например, нельзя надеяться, что в памяти или регистре при старте находится определённое число (а хоть бы и 0), поэтому считается, что там лежит что угодно

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

Конвенции, как явно задокументированные требования к оформлению программ, плавно перетекают в менее явные и совсем неявные договорённости, на уровне «так не делают». Общий смысл таких договорённостей — удобство (совместной) разработки.

Регистры

Как мы уже знаем, регистр — это особый «быстрый» вид оперативной памяти:

  • Располагается непосредственно на процессоре (время доступа несравненно меньше времени доступа к ОЗУ)
  • Имеет небольшой адрес — номер регистра (в одной инструкции может уместиться несколько таких адресов)
    • ⇒ Нужно меньше регистров
  • При грамотном планировании большинство вычислений можно производить на регистрах, изредка обращаясь к памяти
  • RISC: все вычисления производятся только на регистрах, операции с памятью всего две: прочитать и записать

    • ⇒ Нужно больше регистров

Регистр

Программное имя

Регистр

Программное имя

x0

zero

x16

a6

x1

ra

x17

a7

x2

sp

x18

s2

x3

gp

x19

s3

x4

tp

x20

s4

x5

t0

x21

s5

x6

t1

x22

s6

x7

t2

x23

s7

x8

s0, fp

x24

s8

x9

s1

x25

s9

x10

a0

x26

s10

x11

a1

x27

s11

x12

a2

x28

t3

x13

a3

x29

t4

x14

a4

x30

t5

x15

a5

x31

t6

Соглашения по использованию:

  • Только один регистр особенный — zero (x0, всегда равен 0)

  • Теоретически в любых целях можно использовать любой регистр, но это сильно затрудняет написание работающих программ

    • ⇒ возникает понятие конвенции (договорённости)

  • Регистры a0 - a7 (x10 - x17) используются для передачи параметров подпрограммам и для возврата значений из них

    • Очевидно, у подпрограмм может быть более 8 параметров, так что здесь тоже вступают в силу конвенции, и очень разнообразные
  • Регистры t0 - t6 (x5-x7, x28-x31) можно использовать без ограничений (как и a0 - a7)

  • Регистры s0 - s11 (x8,x9,x18 - x27) по договорённости необходимо восстанавливать в исходные значения перед выходом из подпрограммы. При этом даже если они не используются вне подпрограммы, код сохранения и восстановления обязан присутствовать.

  • Регистр ra (x1) используется для хранения адреса возврата из подпрограммы

  • Регистр sp (x2) содержит ссылку на вершину стека (stack pointer)

  • Регистры gp (x3) и tp (x4) — (global pointer и thread pointer). В RISC-V много внимания уделено аппаратной поддержке многопоточности: программа может состоять из «нитей» — нескольких участков, выполняемых параллельно<<Footnote(В действительности параллельность необязательна, достаточно время от времени без предупреждения переключаться между нитями.)>>. Регистр gp указывает на область данных, доступных всем нитям одновременно (например, на массив), а регистр tp — на данные, уникальные для каждой нити

  • Регистр s0 (x8) в некоторых конвенциях организации подпрограмм используется для хранения ссылки на область данных текущей подпрограммы, поэтому он носит ещё одно название — fp (frame pointer)

Плоская модель памяти RARS

Это тоже такая конвенция. В RARS таких моделей памяти три, на самом деле их ещё больше

  • Формально процессу доступна «вся» оперативная память
  • Адресное пространство разделено на части, использование которых задаётся конвенциями
    • …и может вдобавок проверяться аппаратно (например, запрет на запись, чтение или исполнение)
  • На внешнем — аппаратном — уровне адресное пространство отвечает определённой конвенции; у процессов внутренних уровней (гипервизор, ядро, пользователь) оно виртуализовано
    • ⇒ виртуальные адреса легко подогнать под ту же конвенцию

0xffffffff

highest address in kernel (and memory)

Память устройств

Последний адрес, доступный ядру

memory map limit address

Конец памяти устройств

0xffff0000

MMIO base address

Начало памяти устройств

0x80000000

kernel space base address

Область памяти ядра

Начало памяти ядра

0x7fffffff

highest address in user space

Область данных

Последняя ячейка, доступная пользователю

data segment limit address

Последняя ячейка области данных

0x7ffffffc

↓ stack base address

Адрес исчерпания стека

0x7fffeffc

↓ stack pointer sp

Сюда указывает регистр стека (растёт вниз)

0x10040000

stack limit address

Стек может расти досюда

↑ heap base address

Начало кучи (растёт вверх)

0x10010000

.data base Address

Начало статических данных

0x10008000

global Pointer gp

Сюда указывает регистр глобальных данных

0x10000000

.extern base address

Область глобальных данных

DATA Segment base address

Начало области данных

0x0ffffffc

text limit address

Область программного кода

Последняя ячейка области программного кода

0x00400000

.text base address

Начало программы

TEXT segment base address

Начало области программного кода

0x00000000

Зарезервированная область

  • Резервированная память (до 0x400000) может быть использована операционной системой для различных нужд.
    • Доступ к ней со стороны программы приведёт к исключению
  • TEXT — область для инструкций программы (представьте себе, это когда-то называлось текстом ☺!). Теоретически никто не мешает иметь несколько директив .text адрес, размещающих код по различным адресам в пределах 0x400000 - 0x1000000.

    • Обычно после загрузки программы, когда она начала работать, запись по адресам 0x400000 - 0xffffff по умолчанию запрещена. (привет «гарвардской архитектуре»)

  • DATA — область для всевозможных данных программы (глобальных переменных, статических локальных переменных, кучи и стека)

  • .extern base address — область для внешних данных (нужна для взаимодействия с ОС). В этой обрасти размещает данные директива .extern

  • .data base address — начало области, в которую обычно раскладываются данные директивами .data. Именно там лежат переменные, объявленные массивы и прочее. Традиционно имеется зазор между началом области данных (0x10000000) и непосредственно статическими данными (0x10010000 - 0x10040000). Обычно в процессе работы программы нельзя переходить по адресам из области данных и декодировать их как инструкции.

  • Heap (Куча) — область данных, в которую принято помещать динамические данные. Идея в повторном использовании одних и тех же областей памяти для различных нужд. Для этого служат процедуры выделения памяти, в которых запоминается размер и адрес запрошенного фрагмента, и освобождения, в которых эти данные объявляются устаревшими (можно совсем забыть, а можно область пометить как свободную), после чего очередная процедура выделения вполне может выдать ту же самую область. Механизмы выделения/освобождения памяти (т. н. memory managment) обычно довольно непросты, и соответствующие функции предоставляет ОС. Добавление и освобождение данных в куче обычно происходит в сторону увеличения адреса.

  • Stack — область динамических данных особого вида, реализующая абстракцию «стек» и используемая при вызове подпрограмм и передачи им параметров. Добавление и освобождение данных в стеке обычно происходит в сторону уменьшения адреса. Бесконтрольное снятие данных со стека может привести к тому, что регистр стека начнёт указывать за пределы пользовательской памяти, поэтому (и по каким-то ещё соображениям) изначально sp указывает не на самое «дно» стека, а существенно ниже (под 0x7ffff000). Стек и куча растут навстречу друг другу. Строго говоря, нельзя понять, где кончается стек и начинается куча, но это и не важно, лишь бы не пересекались.

  • Память ядра ОС. Начиная с адреса 0x80000000 идёт область, недоступная программе пользователя. Это область кода и данных ядра. Безотносительно к тому, запущена программа под управлением ОС или «на голом железе», для исполнения кода и доступа к памяти требуется особый режим работы процессора. Чтение, запись и переход с использованием адресов ядра пользовательской программе запрещены.

  • Область MMIO служит для адресации ячеек, вообще не принадлежащих оперативной памяти. Обращение по этим адресам приведёт к взаимодействию с данными на внешних устройствах (обычно с регистрами ввода-вывода или собственной памятью устройств)

FrBrGeorge/MyDict/speech_balloon_question.png Можно ли в RARS прочитать байт из раздела .text по нечётному адресу? (Почему :) ?)

  • Ограничение на доступ к «адресам ядра» 0x80000000 - 0xefffffff — специфика плоской модели памяти RARS. В действительности ничто не мешает предоставить доступ программе ко всей адресуемой в RV32I памяти. Непонятно только, в какой памяти при этом будет находиться ядро и другие программы, если система многозадачная ☺

  • Ограничение на доступ к «зарезервированному пространству» 0x0 - 0x003fffff — также специфика плоской модели памяти RARS, но она имеет практическое применение.

    • Например, для описания загружаемого процесса. В RARS директивы .text и .data приводят к заполнению памяти непосредственно по указанным адресам. При наличии операционной системы деле чаще всего результат трансляции записывается в исполняемый файл, который имеет довольно сложный формат, а при необходимости загружается в память в соответствии со специальными таблицами размещения, динамической компоновкой и т. п. Некоторые из этих данных нужны для работы программы под управлением ОС, они-то и размещаются в младших адресах памяти. Чтение и запись в эту область со стороны пользователя запрещены.

  • Удобно запретить обращаться к адресам, которые могут возникнуть в инструкциях косвенной адресации по ошибке (забыли положить адрес в регистр, там осталось предыдущее значение, пошли в память…)— это в первую очередь все маленькие числа, типа 0, 10, 100, 1000, 2, 4, 8, 1024, 42, 1337, 100500 и прочие. Немедленно получить ошибку (вместо того, чтобы, например, самоуверенно расписывать это случайно подвернувшееся вместо в памяти) — очень полезно.

Директивы размещения данных в памяти

В программе на языке ассемблера возникает необходимость описать содержимое сегмента памяти. Для этого код программы помечается .text, а данные — .data . При трансляции в RARS код размещается с адреса 0x400000 (если не сказано иное), а данные — с адреса 0x10010000 (опять-таки, если не сказано иное).

В секции .data помещают директивы (указания ассемблеру) по размещению данных в памяти.

  • .word число — одно или несколько 4-байтовых чисел

  • .dword число — одно или несколько 8-байтовых чисел

  • .half число — одно или несколько 2-байтовых чисел

  • .byte число — одно или несколько однобайтовых чисел

  • .ascii "строка" — последовательность символов в кодировке ASCII

  • .asciz "строка" — то же, только после последнего символа обязательно записывается нулевой байт (конец строки, договорённость, например, для языка Си) Пример размещения данных различного размера:

       1 .data
       2         .word   0xdeadbeef
       3         .dword  0xacebad0feeded
       4         .half   0x1234, 0x5678
       5         .byte   12, 13, 14, 15
       6         .half   0x3344
       7         .byte   0x66, 0x77
    

Результат трансляции пословно (секция .data начинается по умолчанию с адреса 0x10010000). Обратим внимание на little endian: младший байт в слове имеет меньший адрес!

10010000: deafbeef d0feeded 000aceba 56781234 0f0e0d0c 77663344

Для того, чтобы обращаться к соответствующим ячейкам памяти, можно использовать и адреса, и метки. Метка — символическое имя, оно заменяет адрес в программе на языке ассемблера и транслируется в соответствующий адрес в машинных кодах. В подавляющем большинстве случаев ассемблер RISC-V заменяет метку на смещение относительно исполняемой инструкции, то есть регистра «program counter», pc.

Разберём, как работают псевдоинструкции li (load immediate) и la (load address):

   1 .data
   2 var:    .word   0xbadface
   3 
   4 .text
   5         li      t5 0x10010000
   6         la      t6 var

Несмотря на то, что метка var соответствует адресу 0x10010000, и значения регистров t5 и t6 будут равны (проверьте!), действия эти псевдоинструкции задают разные. Посмотрим на получившийся код:

0x00400000  0x10010f37  lui x30,0x00010010      5       li      t5 0x10010000
0x00400004  0x000f0f13  addi x30,x30,0x00000000
0x00400008  0x0fc10f97  auipc x31,0x0000fc10    6       la      t6 var
0x0040000c  0xff8f8f93  addi x31,x31,0xfffffff8
  • Псевдоинструкция li  t5 0x10010000 превратилась в загрузку старших 20 битов числа 0x10010000 с помощью инструкции типа U (lui) и добавление младших 12 (которые оказались нулями)

  • Псевдоинструкция la  t6 var сначала воспользовалась другой инструкцией типа Uauipc.

    • Инструкция auipc регистр, смещение работает так:

      1. Как полагается инструкции U, заполняет 20 старших битов регистра смещением. Младшие 12 битов зануляет. В нашем случае получается 0xfc10000.

      2. Добавляет в регистр адрес текущей исполняемой инструкции. В нашем случае — 0x00400008. Получается 0x10010008

    • Это 0x10010008примерный адрес метки, и следующей инструкцией к нему прибавляется оставшееся расстояние (все узнали в 0xfffffff8 число -8?). Получается 0x10001000

Такой подход называется позиционно-независимым кодированием. Смысл его в том, что одну и ту же программу можно загрузить в произвольный адрес памяти (а не только в конвенциональные 0x400000 - 0x10000000 - 0x10010000). При этом в регистр t5 попадёт число 0x10001000 — потому что именно число мы и хотели туда положить, а в t6 — адрес, отстоящий от данной инструкции на 0xfc0fff8 байтов, каким бы ни был её адрес.

Регистр pc не является регистром общего назначения, но для ориентации в программах на RISC-V его всегда надо иметь в виду. Получить значение pc можно, например, с помощью auipc  t0 0; больше ничего напрямую с ним делать нельзя.

Начиная со значения, указанного в директиве .data (или с адреса по умолчанию), ассемблер высчитывает адрес, который будет соответствовать метке, каждый раз прибавляя размер очередной отведённой ячейки. Обращение к ячейке памяти размером N байтов в архитектуре RISC-V возможно при условии, что адрес этой ячейки кратен N (следствие little endian). Исключение — 64-разрядные значения, которым достаточно границы слова и расширение «C», посвящённое плотной упаковке инструкций. Поэтому при размещении ассемблером ячеек разного размера происходит выравнивание: если очередная ячейка имеет размер, которому не кратен текущий предполагаемый адрес её размещения, к этому адресу дополнительно прибавляется от одного до трёх байтов, чтобы обеспечить кратность. Пример разнообразных данных в памяти с автоматическим и ручным выравниванием.

   1 .data
   2         .word   0x76543210
   3         .dword  0x1212343456567878
   4         .half   0x2468
   5         .word   0x76543210
   6         .byte   1
   7         .word   0x76543210
   8         .byte   3
   9         .half   0x2468, 0x0ac4
  10         .byte   5, 7, 9
  11         .align  2
  12         .byte   1, 3, 5
  13         .align  3
  14         .byte   6
  15         .align  1
  16         .byte   0xaa

Выравнивание можно вызвать директивно, с помощью .align номер (где номер 0,1,2 и 3 соответствует байту, полуслову, слову и двойному слову соответственно). Вот во что превращается пример выше. Обратите внимание на нулевые байты, добавленные для выравнивания; не забываем про little endian: для выравнивания на границу двойного слова к адресу 0x1001002a пришлось добавить пять байтов!

0x10010000    0x76543210 0x56567878 0x12123434 0x00002468 0x76543210 0x00000001 0x76543210 0x24680003
0x10010020    0x07050ac4 0x00000009 0x00050301 0x00000000 0x00090007 0x00000000 0x00000000 0x00000000

Директива .data автоматически заполняет область данных, начиная с 0x10010000, причём последующие директивы .data продолжают заполнение с последнего незанятого адреса. Однако можно указывать адрес размещения данных явно в виде параметра .data. Директивы .data можно перемежать с директивами .text, это никак не повлияет на размещение данных:

   1 .data
   2         .word   0x123456
   3         .word   1
   4 .text
   5         mv      t1 zero
   6 .data
   7         .word   0x7890a
   8         .word   2
   9 
  10 .data   0x10010040
  11         .word   0x334455
  12 .text
  13         nop

Память в области данных в результате:

0x10010000    0x00123456 0x00000001 0x0007890a 0x00000002 0x00000000 0x00000000 0x00000000 0x00000000
0x10010020    0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000
0x10010040    0x00334455 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000

Адресация в секции кода

Адресация в секции кода нужна для переходов — условных и безусловных. Для этого используются команды типа S и U соответственно, но их машинный вид разбирать мы будем позже, а пока просто напишем проверку какого-нибудь условия.

Пример с условным переходом:

   1 .data
   2 odd:    .asciz  "Odd\n"
   3 even:   .asciz  "Even\n"
   4 
   5 .text
   6         la      t1 odd          # Считаем, что число нечётное
   7         li      a7 5            # Чтение целого числа в регистр a0
   8         ecall
   9         andi    t0 a0 1         # Проверяем, чётно ли число
  10         bnez    t0 isodd        # Нечётно
  11         la      t1 even
  12 isodd:  mv      a0 t1           # Вывод строки, адрес которой находится в a0
  13         li      a7 4
  14         ecall
  • Здесь мы использовали внешний вызов №5 (чтение целого числа) и №4 (вывод строки)

Полученный код:

Address     Code        Basic                        Line Source

0x00400000  0x0fc10317  auipc x6,64528               6          la      t1 odd
0x00400004  0x00030313  addi x6,x6,0                      
0x00400008  0x00500893  addi x17,x0,5                7          li      a7 5
0x0040000c  0x00000073  ecall                        8          ecall
0x00400010  0x00157293  andi x5,x10,1                9          andi    t0 a0 1
0x00400014  0x00029663  bne x5,x0,12                 10         bnez    t0 isodd
0x00400018  0x0fc10317  auipc x6,64528               11         la      t1 even
0x0040001c  0xfed30313  addi x6,x6,-19
0x00400020  0x00600533  add x10,x0,x6                12  isodd: mv      a0 t1
0x00400024  0x00400893  addi x17,x0,4                13         li      a7 4
0x00400028  0x00000073  ecall                        14         ecall
  • В инструкции условного перехода (типа B) практически невозможно вручную отыскать смещение:

    • оно хранится в битах 8, 9, 10, 11, 25, 26, 27, 28, 29, 30, 7, 31 (именно в такой последовательности!) + младший бит смещения в коде всегда 0, и не хранится

    • 0002966316 — это 000000000000001010010110011000112 ; биты смещения, начиная с первого, — 0, 1, 1, 0, 0, 0, … (далее все нули), то есть …0001102 + нулевой бит 0 — …00011002 == 12 (это число показывает дизассемблер) == 000c16

    • Действительно, адрес инструкции bne = 0040001416, а адрес метки isodd: = 0040001416 + 000c16 = 0040002016

Базовые инструкции вида «сравнить и перейти» — это beq/bne и blt/bge с их беззнаковыми аналогами btlu/bgeu. Все остальные инструкции вида b*, включая безусловный переход b, — псевдо, потому что получаются простой перестановкой регистров-операндов или подстановкой регистра zero в нужное место. Непосредственный 12-битный операнд используется для хранения смещения относительно текущего адреса. Вычислением этого смещения из метки занимается ассемблер, оно бывает положительное (вперёд, как в примере выше) и отрицательное (назад, как в следующем). 12 битов хватает на то, чтобы сделать переход на 4 килобайта кода (4, а не 2, потому что адрес перехода всегда кратен 2 и самый младший бит просто не хранится).

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

   1         li      s2 10           # Граница счётчика
   2         li      s1 1            # Счётчик
   3 loop:   li      a7 1            # Вывод счётчика
   4         mv      a0 s1
   5         ecall
   6         addi    s1 s1 1         # Увеличение
   7         blt     s1 s2 loop      # Сравнение счётчика и границы и переход
   8         li      a7 10           # Останов
   9         ecall

Строго говоря, для этой программы не нужно вносить инструкцию li a7 1 внутрь цикла — значение a7 не меняется. Метку loop: можно былдо сдвинуть на следующую инструкцию. Однако этого не сделано намеренно: поскольку a7 — это номер внешнего вызова, всегда может случиться, что внутри цикла нам потребуется ещё один, другой (например, вывод символа «перевод строки»). Что же тогда, снова двигать метку?

FrBrGeorge/MyDict/speech_balloon_question.png что произойдёт, если сдвинуть loop: на инструкцию mv a0 s1, а внутри цикла вызвать другой ecall, например, №11?

Полученный код:

 Address    Code        Basic                     Source

0x00400000  0x00100493  addi x9,x0,0x00000001  1         li      s1 1
0x00400004  0x00a00913  addi x18,x0,0x0000000a 2         li      s2 10
0x00400008  0x00100893  addi x17,x0,0x00000001 3   loop: li      a7 1
0x0040000c  0x00900533  add x10,x0,x9          4         mv      a0 s1
0x00400010  0x00000073  ecall                  5         ecall
0x00400014  0x00148493  addi x9,x9,0x00000001  6         addi    s1 s1 1
0x00400018  0xff24c8e3  blt x9,x18,0xfffffff0  7         blt     s1 s2 loop
0x0040001c  0x00a00893  addi x17,x0,0x0000000a 8         li      a7 10
0x00400020  0x00000073  ecall                  9         ecall
  • Обратите внимание на переход на -16 байтов назад — это как раз адрес метки loop.

    • (более ранние версии RARS показывали в этом месте действительное значение поля «destination», -8. Добавляем всегда нулевой младший бит, получаем -16)

Если надо перейти дальше, чем это допустимо в b*, используйте отдельно сравнение, отдельно безусловный переход псевдоинструкцией j: она имеет тип U (с особенностями, о которых ниже), и поле непосредственной константы занимает 20 битов. Но сначала напишите 4 килобайта кода в одном из условий. А лучше — наоборот, не пишите так☺.

FrBrGeorge/MyDict/speech_balloon_question.png На сколько четырёхбайтных инструкций можно перейти командой j (число может быть отрицательным, младший бит равен нулю и не хранится)?

Цикл с постусловием — штука ненадёжная, так что идеологически верно было бы переписать пример выше в соответствии с «канонической схемой цикла»:

  1. Инициализация
  2. Проверка условия
  3. Тело
  4. Изменение

При этом придётся сделать ещё один (безусловный) переход.

   1         li      s2 10
   2         li      s1 1            # Инициализация
   3 loop:   bge     s1 s2 final     # Проверка условия
   4         li      a7 1            # Тело
   5         mv      a0 s1
   6         ecall                   # Вывод целого
   7         li      a7 11
   8         li      a0 10
   9         ecall                   # Вывод перевода строки
  10         addi    s1 s1 1         # Изменение
  11         j       loop            # Дополнительный переход
  12 final:  li      a7 10
  13         ecall
  • Заодно вставим вывод перевода строки (понадобится для Д/З)
  • Безусловный переход может называться b, а может j — это одна и та же псевдоинструкция.

    • Это инструкция «длинного» перехода типа J (20 битов, т. е. по мегабайту в обе стороны с учётом ещё одного младшего бита).

    • Строго говоря — это инструкция перехода на подпрограмму с сохранением адреса возврата в регистре zero (где он и пропадает).

Про отсутствие регистра флагов

Операция «сравнить и перейти» в трёхадресной архитектуре RISC-V атомарна. Это значит, что нет необходимости заводить отдельный регистр флагов, и сверяться с ним относительно результатов сравнения. Атомарность сравнения-перехода также делает более эффективной микропрограммную реализацию.

Регистр флагов бывает нужен также для определения переполнения, но в спецификации справедливо замечают, что переполнение в большинстве случаев проверяется одной операцией «сравнить-перейти» (зачем флаги тогда?), и только в случае, когда мы не знаем знаков обоих слагаемых, проверка потребует дополнительных вычислений.

Про беззнаковые операции и особенности арифметики

Операции сложения и вычитания целых чисел в дополнительном коде работают независимо от того, считаем мы эти числа знаковыми или беззнаковыми. Побитовые операции вроде OR вообще ничего не знают о знаках. Операции умножения и деления (из расширения «M») и операции сравнения бывают как знаковыми, так и беззнаковыми, причём для умножения в «M» предусмотрен и знаково-беззнаковый вариант. Беззнаковые инструкции обозначаются дополнительным суффиксом U.

Классический алгоритм целочисленного деления вычисляет одновременно частное и остаток, но в отличие от большинства архитектур, в RISC-V нет возможности задать два регистра-приёмника (это привело бы к услжнению и замдлению логики). Поэтому делением занимается инструкуция div (беззнаковым — divu), а остатком — rem (remu).

Алгоритмы умножения и деления требуют на порядок больше тактов, чем сложение, а в программе часто требуется и частное, и остаток. Очевидная аппаратная оптимизация состоит в том, чтобы запоминать этот остаток где-то в укромном уголке (теневом регистре) и немедленно доставать оттуда, не выполняя деления ещё раз. На этот счёт есть в расширении есть конвенция: рассчитывать на такой порядок инструкций:

   1         div     приёмник1, делимое, делитель
   2         rem     приёмник2, делимое, делитель
  • В конвенции также сказано, что регистр приёмник1 не должен совпадать с регистрами делимое1 или делитель1

Результат умножения двух целых может оказаться размером в два машинных слова, поэтому в RISC-V предусмотрено два типа инструкций — mul, которая возвращает младшее слово результата, и mulh, которая возвращает старшее слово — с похожими конвенциями относительно возможной оптимизации.

Особенности кодирования инструкций в секции кода

Относительно инструкций типа B и J:

  • В базовом RV32I размер инструкции — 4 байта, но в «упакованном» расширении C адрес инструкции может быть кратным не 4, а только 2.

    • Архитектуры RV64I и RV128I расширяют размер адресного пространства и соответствующих операций, но размер самой инструкции остаётся 32 бита.

  • При использовании непосредственного значения в командах перехода один младший бит адреса всегда равен 0. Его можно не хранить вообще, а приписывать к адресу в процессе декодирования.
  • Таким образом, например, инструкция типа B совпадает по формату с типом S, а тип J — с типом U, но непосредственное значение будет интерпретироваться по-другому.

На схеме взята за основу инструкция типа R, она содержит только номера регистров и дополнительные поля (funct7 и funct3, одно 7 битов, другое — 3). Что делать с этими полями, если в инструкцию необходимо вписать некоторое непосредственное значение-число?

  • Если непосредственное значение выступает как «источник», его битами можно занять поля funct7 и source2 (инструкция типа I)

  • Если непосредственное значение выступает как «приёмник», его битами можно занять поля funct7 и destination (инструкции типа S и B)

  • В инструкциях типа U и J непосредственное значение занимает 20 старших битов.

Биты ячейки:  31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0
Инструкция R:       funct7        ↓   source2    ↓   source1    ↓ funct3 ↓  destination ↓      opcode
Инструкция I: 11 10  9  8  7  6  5  4  3  2  1  0 ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ··
Инструкция S: 11 10  9  8  7  6  5 ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ··  4  3  2  1  0 ·· ·· ·· ·· ·· ·· ··
     → число: 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10  9  8  7  6  5  4  3  2  1  0
Инструкция B: 12 10  9  8  7  6  5 ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ··  4  3  2  1 11 ·· ·· ·· ·· ·· ·· ··
     → число: 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 10  9  8  7  6  5  4  3  2  1 =0
Инструкция U: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ··
     → число: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 =0 =0 =0 =0 =0 =0 =0 =0 =0 =0 =0 =0
Инструкция J: 20 10  9  8  7  6  5  4  3  2  1 11 19 18 17 16 15 14 13 12 ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ·· ··
     → число: 20 20 20 20 20 20 20 20 20 20 20 20 19 18 17 16 15 14 13 12 11 10  9  8  7  6  5  4  3  2  1 =0
  • В представлении инструкции показано, какой бит числа где хранится
  • В представлении получившегося числа наиболее старший из хранящихся битов считается знаковым и распространяется вплоть до 31-го
  • =0 означает, что соответствующий бит всегда равен 0

Немного о свойствах числа, которое в результате получается:

  • Бит, который в получившемся числе станет знаковым, в инструкции всегда 31-й (т. е. тоже знаковый).
    • Это свойство полезно также и в 64-разрядном варианте RV64I.

  • Если непосредственное значение — смещение адреса в инструкции типа B, младший бит его всегда равен нулю и не хранится, а вместо него хранится самый старший, не считая знакового — 11-й.

  • Инструкция типа U нужна для работы со старшими битами слова (см. разложение псевдоинструкции lw), т. е. с 31-го по 12-й, которые просто переносятся туда один к одному.

  • Инструкция типа J интерпретирует эти 20 битов как биты с 20-го по 1-й адресного смещения (0-й бит равен 0). Получается, что:

    • 31-й бит — знаковый (расширяется вплоть до 20-го)
    • Биты с 19-го по 12-й просто совпадают с теми же самыми битами числа (с 19-го по 12-й), как в типе U

    • В битах с 30-го по 21-й хранятся биты числа с 10-го по 1-й, как в типе I

    • Нулевой бит числа равен нулю, а в 20-м бите хранится оставшийся 11-й бит числа

Немножко очень заморочено, но «если постоять, повтыкать, всё понятно становится»™…

Цикл с пост-условием

Напишем решение простой задачи: ввод двух натуральных чисел N и M и вывод всех чисел в диапазоне от 0 до N включительно, кратных M.

        li      a7 5            # Чтение целого
        ecall
        mv      s2 a0           # N
        li      a7 5            # Чтение целого
        ecall
        mv      s3 a0           # M
        li      t1 0            # Инициализация
loop:   rem     t0 t1 s3        # Остаток от деление на M
        bnez    t0 nodiv        # Не кратное M число
        mv      a0 t1           # Вывод числа
        li      a7 1
        ecall
        li      a0 ' '          # Вывод пробела
        li      a7 11
        ecall
nodiv:  addi    t1 t1 1         # Изменение: следующее число
        ble     t1 s2 loop      # Проверка условия и переход
        li      a7 10           # Останов
        ecall

FrBrGeorge/MyDict/speech_balloon_question.png Добавьте в программу проверку на M==0

FrBrGeorge/MyDict/speech_balloon_question.png модифицируйте программу так, чтобы M и N могли быть отрицательными (подсказка: на M обращать внимание не надо, а вот для отрицательного N надо подумать о диапазоне)

LecturesCMC/ArchitectureAssemblerProject/08_RegistersAddressing (последним исправлял пользователь FrBrGeorge 2024-07-13 13:04:58)