Регистры и модель памяти. Виды адресации

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

Регистры

Регистр

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

Регистр

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

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

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

Плоская модель памяти 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

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

Задание: проверить, можно ли в RARS прочитать байт из раздела .text по нечётному адресу? (Почему :) ?)

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

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

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

Результат трансляции пословно (секция .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

Такой подход называется позиционно-независимым кодированием. Смысл его в том, что одну и ту же программу можно загрузить в произвольный адрес памяти (а не только в конвенциональные 0x400000 - 0x10000000 - 0x10010000). При этом в регистр t5 попадёт число 0x100010000 — потому что именно число мы и хотели туда положить, а в 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         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

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

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

 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

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

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

  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

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

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

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

Про беззнаковые операции

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

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

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

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

Биты ячейки:  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

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

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

Косвенная адресация и массивы

Ещё один способ обращаться к памяти — это записать полный абсолютный адрес в регистр, и воспользоваться инструкцией, которая работает с памятью, находящейся по этому адресу. Вот как раскрывается псевдоинструкция lw метка:

   1 .data
   2         .word   0x1223344
   3 var:    .word   0xdeadbeef
   4 addr:   .word   var
   5 .text
   6         lw      t1 var
   7         lw      t2 addr
   8         lw      t3 (t2)
   9         lw      t4 4(t2)
  10         lw      t5 -4(t2)

Что даёт:

 Address    Code        Basic                     Source

0x00400000  0x0fc10317  auipc x6,0x0000fc10   6         lw      t1 var
0x00400004  0x00432303  lw x6,0x00000004(x6)
0x00400008  0x0fc10397  auipc x7,0x0000fc10   7         lw      t2 addr
0x0040000c  0x0003a383  lw x7,0x00000000(x7)
0x00400010  0x0003ae03  lw x28,0x00000000(x7) 8         lw      t3 (t2)
0x00400014  0x0043ae83  lw x29,0x00000004(x7) 9         lw      t4 4(t2)
0x00400018  0xffc3af03  lw x30,0xfffffffc(x7) 10        lw      t5 -4(t2)

Если нужно обработать массив данных, косвенная адресация — единственный способ. Массив — это адрес в памяти и длина (количество элементов * размер одного элемента). В примере массив слов расписывается последовательными значениями:

   1 .data
   2 array:  .space  64
   3 arrend:
   4 .text
   5         la      t0 array
   6         la      t1 arrend
   7         li      t2 1
   8 loop:   sw      t2 (t0)
   9         addi    t2 t2 1
  10         addi    t0 t0 4
  11         bltu    t0 t1 loop

0x10010000    0x00000001 0x00000002 0x00000003 0x00000004 0x00000005 0x00000006 0x00000007 0x00000008
0x10010020    0x00000009 0x0000000a 0x0000000b 0x0000000c 0x0000000d 0x0000000e 0x0000000f 0x00000010

Ещё один пример:

   1 .data
   2 sep:    .asciz  "--------\n"    # Строка-разделитель (с \n и нулём в конце)
   3 .align  2                       # Выравнивание на границу слова
   4 array:  .space  64              # 64 байта
   5 arrend:                         # Граница массива
   6 .text
   7         la      t0 array                # Счётчик
   8         la      s1 arrend
   9         li      t2 1            # Число, которое мы будем записывать в массив
  10 fill:   sw      t2 (t0)         # Запись числа по адресу в t0
  11         addi    t2 t2 1         # Изменим число
  12         addi    t0 t0 4         # Увеличим адрес на размер слова в байтах
  13         bltu    t0 s1 fill      # Если не вышли за границу массива
  14         la      a0 sep          # Выведем строку-разделитель
  15         li      a7 4
  16         ecall
  17         la      t0 array
  18 out:    li      a7 1
  19         lw      a0 (t0)         # Выведем очередной элемент массива
  20         ecall
  21         li      a7 11           # Выведем перевод строки
  22         li      a0 10
  23         ecall
  24         addi    t0 t0 4
  25         blt     t0 s1 out
  26         li      a7 10           # Останов
  27         ecall

В некоторых архитектурах популярна двойная косвенная адресация — это когда в ячейке памяти лежит адрес ячейки памяти, в которой лежит нужное значение (как в переменной addr: в одном из примеров выше). Это удобно для организации таблиц ссылок, и, наверное, можно как-то оптимизировать, чтобы оно работало быстрее двух последовательных инструкций обычной косвенной адресации. Но поскольку с точки зрения скорости двойное обращение к памяти — очень медленная операция, идеологии RISC она не соответствует.

Д/З

Ввод целого числа, напоминаю — системный вызов № 5. Не забывайте выводить переводы строки, как в примере выше.

Решения можно запускать из командной строки, как это делает EJudge (первая строка — команда, следующая — числа, третья — вывод суммы цифр, последняя — диагностика о причине останова RARS):

$ rars nc sm me DigitSum.asm
2341234
19
Program terminated by calling exit

В последних двух задачах предполагается хранить в памяти массив данных неизвествного размера. Поскольку он такой один, вы просто заводите все переменные, какие хотите (я обошёлся без переменных вообще, регистров хатило), ставите в конце секции .data метку и пишете туда сколько влезет. В плоской модели памяти доступно всё адресное пространство.

  1. Всем, кто ещё не успел, зарегистрироваться на EJudge и решить задачку про вывод двух "Hello" (системный вызов № 4 ☺)

  2. EJudge: DigitSum 'Сумма цифр'

    Ввести целое число (возможно, отрицательное) и посчитать сумму его цифр в десятичной записи; вывести как целое.

    Input:

    -12345
    Output:

    15
  3. EJudge: PlusMinus 'Плюс-минус'

    Ввести натуральное N, затем N целых чисел ai; посчитать формулу a0-a1+a2-…±aN-1 . Вывести результат.

    Input:

    4
    22
    13
    14
    15
    Output:

    8
  4. EJudge: EvenBack 'Чётные назад'

    Ввести целое N, затем N целых чисел. Вывести из этих чисел только чётные, причём в обратном порядке (стеком пользоваться запрещается :) )

    Input:

    6
    12
    -11
    3
    88
    0
    1
    Output:

    0
    88
    12
  5. EJudge: NoDups 'Без повторений'

    Ввести целое N, затем N целых чисел. Вывести эти числа, пропуская уже выведенные, если встретятся повторы.

    Input:

    8
    12
    34
    -12
    23
    12
    -12
    56
    9
    Output:

    12
    34
    -12
    23
    56
    9

LecturesCMC/ArchitectureAssembler2022/02_MemoryRegisters (последним исправлял пользователь FrBrGeorge 2022-02-16 19:46:48)