Регистры статуса и управления. Исключительные ситуации

TODO написать кое-где развёрнутый текст

RISC-V F: исключения FPU накапливаются в виде флагов в CSR-регистре fflags, обрабатывать их надо явно.

В большинстве архитектур для обработки исключений и прерываний используется механизм ловушек (trap):

Свойства ловушек ( <!> эти свойства не взаимоисключающие!):

  1. Собственные. Обработчик выполняется тем же окружением, что и прерванная программа. Например, ecall под управлением операционной системы в плоской модели памяти приводит к переключению в режим ядра, при этом код обработчика выполняется «тем же самым процессором» в «той же самой памяти». Пример несобственных ловушек — обработка ecall в RARS.

  2. Произвольные. Обработчик вызывается синхронно по запросу со стороны выполняемой программы и с целью получить какой-то результат. Пример произвольной ловушки — ecall (любой), пример непроизвольной — обработчик прерывания ввода/вывода. Произвольная ловушка может не вернуться в программу (например, ecall 10).

  3. Невидимые. Переход по ловушке и выполнение обработчика никак не затрагивают контекст выполняемой программы — настолько, что в идеале программа не может узнать о том, что произошло какое-то событие. Невидимы: программная эмуляция неподдерживаемых инструкций, подгрузка существующих страниц виртуальной памяти, обработка прерываний в других процессах многозадачных систем и т. п. Разумеется, факт сработавшей ловушки можно попробовать угадать по косвенным признакам, например, по «мгновенному» скачку системного времени.

  4. Фатальные. Часть ловушек происходят потому, что выполнять программу больше нет возможности. Обработчик выполняет какие-то действия «напоследок» — например, по корректному/штатному завершению задачи. Примеры фатальных ловушек: ошибки в ядре ОС, срабатывание сторожевого таймера (watchdog) и т. п.

RARS:

Собственные

Произвольные

Невидимые

Фатальные

Приводят к останову

Нет

Нет, но могут

Нет

Да

«Прозрачны» для программы

Нет

Нет

Да

Да, но иногда уже не до этого

Обрабатываются внешним окружением

Нет

Да

Да

Да

Режимы работы CPU или Башня косвенности

Ловушки — довольно общий механизм для «обработки событий в вычислительной системе». Можно, например, в спецификации потребовать, чтобы все ловушки были несобственные и невидимые — тогда описание ловушек не будет входит в архитектуру исполнителя программы, а только в архитектуру окружения (которое может быть каким угодно, например, программой на Java ☺).

Возможные аппаратные требования для реализации собственных ловушек:

RISC-V: Несколько спецификаций для разных режимов работы (ссылки ниже могут измениться после ратификации новых расширений/исправлений):

В RARS мы работаем с плоской моделью памяти, наиболее близкий вариант — устаревшая версия User-level ISA 2.2

Блок счётчиков и регистров управления CSR

Подробнее про блок Control and Status Registers

Типичный процессор, если сильно упрощать, состоит из арифметико-логическтого устройства и устройства управления. АЛУ занимается вычислениями, УУ занимается интерпретацией команд, реагирует на изменение состояния процессора, а также само изменяет это состояние. Часть работы УУ не требует контроля со стороны, так как алгоритм задан заранее и не меняется. Но некоторые функции управления хочется сделать модифицируемыми (например, программно обрабатывать различные системные события).

Есть примерно три способа реализовать интерфейс управления процессором:

  1. Придумать специальный управляющий сопроцессор (примерно как как FPU, но цель другая), разделить инструкции на обычные и инструкции управления. При этом появляются регистры управляющего сопроцессора, возможно, особенная память, действия внутри этого сопроцессора и т. п.
  2. Отказаться от идеи отдельного сопроцессора, и для каждой функции управления ввести отдельную инструкцию в ISA.
  3. Спланировать управляющий сопроцессор (или УУ) как устройство с заданной логикой работы, оставив в интерфейсе управления только специальные регистры. Тогда работа с этими регистрами со стороны ЦПУ общего назначения (чтение и запись) и будет приводить к изменению состояния и логики работы.

В RISC-V реализован этот третий подход — в спецификации определён т. н. «блок регистров управления и статуса»

Инструкции для работы с регистрами управляющего сопроцессора:

csrrc t0, csrReg, t1

Атомарное чтение/очистка CSR регистра: чтение из CSR в t0 и очистка битов CSR в соответствии с t1

csrrci t0, csrReg, 10

Атомарное чтение/очистка CSR регистра непосредственным значением: читает из CSR в t0 и сбрасывает биты в CSR в соответствии с константой

csrrs t0, csrReg, t1

Атомарное чтение/установка CSR: читает из CSR в t0 и записывает в CSR побитовое ИЛИ CSR и t1

csrrsi t0, csrReg, 10

Атомарное чтение/установка CSR непосредственным значением: читает из CSR в t0 и записывает в CSR побитовое ИЛИ CSR и непосредственного значения

csrrw t0, csrReg, t1

Атомарное чтение/запись: читает из CSR в t0 и записывает в t1 в CSR

csrrwi t0, csrReg, 10

Атомарное чтение/запись CSR непосредственного значения:читает из CSR в t0 и записывает непосредственное значение в CSR

Псевдоинструкции (с использованием zero):

csrc t1, csrReg

Clear bits in control and status register

csrci csrReg, 100

Clear bits in control and status register

csrr t1, csrReg

Read control and status register

csrs t1, csrReg

Set bits in control and status register

csrsi csrReg, 100

Set bits in control and status register

csrw t1, csrReg

Write control and status register

csrwi csrReg, 100

Write control and status register

Обратите внимание на размер непосредственных значений. Их небольшая величина объясняется форматом команд работы с регистрами контроля и управления/статуса(CSR).

CSR 31-20

rs1 19-15

funct3 14-12

rd 11-7

opcode 6-0

Если быть точным:

Пример: во что раскладываются псевдоинструкции управления FPU:

0x00400000  0x00200293  addi x5,x0,2         1   li       t0 2
0x00400004  0x00229373  csrrw x6,2,x5        2   fsrm     t1 t0
0x00400008  0x00300e13  addi x28,x0,3        3   li       t3 3
0x0040000c  0xd00e71d3  fcvt.s.w f3,x28,dyn  4   fcvt.s.w ft3 t3
0x00400010  0x00700393  addi x7,x0,7         5   li       t2 7
0x00400014  0xd003f153  fcvt.s.w f2,x7,dyn   6   fcvt.s.w ft2 t2
0x00400018  0x183170d3  fdiv.s f1,f2,f3,dyn  7   fdiv.s   ft1 ft2 ft3
0x0040001c  0x003022f3  csrrs x5,3,x0        8   frcsr    t0
0x00400020  0x00202373  csrrs x6,2,x0        9   frrm     t1

CSR и управление

В RISC-V предусмотрена группа регистров только для чтения — регистров статуса (счётчиков). Поскольку в 11-10 битах номера у них 1, начинаются они с 0xc00, т. е. 3072. Все эти счётчики растут настолько быстро, что не помещаются в 32 разряда, поэтому на 32-разрядной архитектуре в разделе «опциональные (custom) регисты» к ним прибавляются парные для хранения старшего слова.

В RARS реализовано почти что шесть:

cycle

3072

количество выполненных тактов (циклов) CPU

time

3073

бортовое время (в «тиках», соизмерять с астрономическим можно только если есть специальные аппаратные часы)

instret

3074

количество «окончательно выполненных» инструкций

cycleh

3200

старшее слово cycle

timeh

3201

старшее слово time

instreth

3202

старшее слово instret

И запись, и чтение CSR-регистра могут привести к изменению работы CPU.

Пример из документации:

  1. Чтение из регистра зажигает лампочку, запись нечётного числа — гасит. Обе операции имеют побочный эффект (чтение не меняет CSR, но лампочка загорается; если в CSR уже было нечётное число и лампочка горела, запись в CSR того же самого числа её гасит)
  2. Запись в регистр чётного числа зажигает лампочку, нечётного — гасит. Обе операции имеют только непрямой эффект, но не побочный

Побочного эффекта по возможности следует избегать:

Проблемы синхронизации (особенно при наличии hardware thread).

Обработка исключений в RARS

Исключение — это синхронная ловушка на конкретной инструкции

Поддержка ловушек в RARS достаточно далека от стандарта:

Вы будете смеяться, но это абсолютно обычная ситуация для практически любого «железа» тоже.

Управляющие регистры RARS:

Название

Номер

Назначение

ustatus

0

Статус, бит 0 глобально разрешает исключения, бит только для чтения 1 сигнализирует об исключении

uie

4

Разрешение прерываний и исключений

utvec

5

Адрес обработчика ловушки

uscratch

64

Регистр «на всякий случай»

ucause

66

Тип («причина») срабатывания ловушки

utval

67

Дополнительная информация (например, адрес при ошибке обращения к памяти)

uip

68

Ожидающие прерывания

uepc

65

Адрес инструкции, которая вызвала исключение (или во время выполнения которой произошло прерывание)

В «большом RISC-V» есть симметричные регистры для других режимов работы процессора (supervisor, hypervisor, machine), а для user — нет (возможно, снова появятся в спецификации для малых устройств).

Обработчик исключений

CSR регистр ustatus(0):

bits

31-5

4

3-1

0

UPIE

UIE

В регистре CSR ucause (0x42, 42, Карл) отображается номер ловушки и её тип (прерывание или исключение):

bits

31

30-5

4-0

1 — interrupt, 0 — exception

cause

Номера исключений RARS:

  1. INSTRUCTION_ADDR_MISALIGNED
  2. INSTRUCTION_ACCESS_FAULT
  3. ILLEGAL_INSTRUCTION
  4. ??? (BREAKPOINT)
  5. LOAD_ADDRESS_MISALIGNED
  6. LOAD_ACCESS_FAULT
  7. STORE_ADDRESS_MISALIGNED
  8. STORE_ACCESS_FAULT
  9. ENVIRONMENT_CALL

Чтобы создать работающий обработчик исключений, следует:

Дисциплина оформления обработчика:

Дополнительная дисциплина для RARS:

В «больших» системах предусматривается отдельный стек ядра (пользовательский стек может быть испорчен)

Пример тривиального обработчика, не соблюдающего конвенцию по сохранению контекста (пройти под отладчиком RARS):

   1 .text
   2         la      t0      handler
   3         csrrw   zero    5       t0      # Сохранение адреса обработчика исключения в utvec (5)
   4         csrrsi  zero    0       1       # Разрешить обработку исключений бит 0 в регистре uststus (0)
   5         lw      t0      (zero)          # Попытка чтения по адресу 0
   6         li      a7      10
   7         ecall
   8 
   9 handler:
  10         csrrw   t0      65      zero    # В регистре uepc (65) — адрес инструкции, где произошло прерывание
  11         addi    t0      t0      4       # Добавим к этому адресу 4
  12         csrrw   zero    65      t0      # Запишем обратно в uepc
  13         uret                            # Продолжим работу программы

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

   1 .text
   2         la      t0 handle
   3         csrw    t0 utvec        # Сохранение адреса обработчика исключения в utvec
   4         csrsi   ustatus 1       # Разрешить обработку исключений (бит 0 в регистре ustatus)
   5         lw      t0 (zero)       # Попытка чтения по адресу 0
   6         li      a7 1000         # Несуществующий системный вызов
   7         ecall
   8         li      a7 10
   9         ecall
  10 .data
  11 h_a0:   .space  4
  12 h_a7:   .space  4
  13 .text
  14 handle: csrw    t0 uscratch     # Сохраним t0
  15         sw      a0 h_a0 t0      # Сохраним a0
  16         sw      a7 h_a7 t0      # Сохраним a7
  17         csrr    a0 ucause       # Прочтём причину исключения
  18         li      a7 34           # Выведем её
  19         ecall
  20         li      a0 '\n'
  21         li      a7 11
  22         ecall
  23         lw      a0 h_a0         # Восстановим a0
  24         lw      a7 h_a7         # Восстановим a7
  25         csrr    t0 uepc         # Адрес прерванной инструкции
  26         addi    t0 t0 4         # Адрес следующей инструкции
  27         csrw    t0 uepc         # Запишем его
  28         csrr    t0 uscratch     # Восстановим t0
  29         uret

Связь с внешним окружением

Функции окружения (ecall) входят в ту или иную конвенцию. Нередко возникает необходимость обменяться с окружением произвольными данными, специфичными для данного экземпляра окружения, локальных договорённостей и т. п. Для этого можно воспользоваться инструкцией ebreak, которая в обычном случае приводит к полной передаче управления окружению (это инструкция, которую отладчик вписывает в код, чтобы исполнение остановилось и можно было продолжать отладку).

К сожалению, эта инструкция не параметризуема. Специальная конвенция описывает, как оформить её с помощью псевдоинструкций NOP особого вида, чтобы намекнуть окружению: мы хотим не выпасть в отладчик, а запрашиваем специальное обслуживание. Такая технология называется «semihosting»

   1 slli x0, x0, 0x1f       # 0x01f01013    Entry NOP
   2 ebreak                  # 0x00100073    Break to debugger
   3 srai x0, x0, 7          # 0x40705013    NOP encoding the semihosting call number 7

Ещё одно отличие semihosting от ecall — окружению, к которому мы обращаемся, нет нужды уметь «перехватывать» различные инструкции (например, ecall). Использование ebreak гарантированно передаст управление отладчику, а тот в состоянии разобраться, что перед ним не просто ebreak, а «semihosting call number 7».

В RARS ebreak в любом контексте приводит к выходу в отладчик.

(Если успеем: HINT Instructions)

Вектор прерываний

Быстрый аппаратный вызов обработчика ловушки можно сделать с помощью т. н. «вектора прерываний» (в RARS не поддерживается).

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

Вариант с таблицей:

Адрес

Содержимое

Пояснение

0x80000100

0x80000180

адрес обработчика исключения № 0

0x80000104

0x800007ac

адрес обработчика исключения № 1

0x80000108

0x800015b0

адрес обработчика исключения № 2

0x80000120

0x80000e54

адрес обработчика исключения № 8

В RISC-V (обычно) отсутствет, потому что для эффективной реализации требуется одно из двух:

Поэтому в RISC-V вектор прерываний — это особый вид секции .text (кода!):

.text.mtvec_table
        b  riscv_mtvec_exception
        b  riscv_mtvec_ssi
        b  riscv_mtvec_msi
        b  riscv_mtvec_sti
        b  riscv_mtvec_mti
        # и т. д.

В RISC-V обработка ловушек вектором включяется с помощью младшего бита CSR-регистра uvec (как часть адреса младший бит не имеет смысла, т. к. адрес инструкции обработчика кратен 4 даже в упакованном ISA).

Д/З

Домашнее задание будет на обработку исключений. К сожалению, в RARS самое интересное исключение — ввод ерунды вместо числа — пока (?) несобственное. Так что будем упражняться с несуществующими системными вызовами и обращением к несуществующей памяти. Имеет смысла заранее написать комплект макросов и библиотеки-обработчика исключений для домашних заданий

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

TODO EJudge.

  1. EJudge: NewEcall 'Новые вызовы'

    Создать обработчик исключений с меткой handler:, реализующий три «новых системных вызова» (100, 101 и 102) для работы со «скрытыми регистрами»:

    1. (a0 = размер). Однократно заказывает у системы память размером в a0 машинных слов, не возвращает ничего (размер и заказанный адрес запоминает в недрах обработчика). Это «скрытые регистры»

    2. (a0 = номер). Возвращает в a0 содержимое «скрытого регистра» № номер. Если номерразмер, берётся остаток от деления номер % размер (actually, всегда))

    3. (a0 = номер, a1 = значение). Заносит в регистр № номер % размер значение значение

    Это делается путём обработки исключения, проверки ucause на равенство ENVIRONMENT_CALL и содержимого a7. Соблюдать конвенцию неприкосновенности регистров. К обработчику будет приписана такая проверяющая программа: NewEcall.asm. Ввод и вывод полученной программы с обработчиком:

    Input:

    8
    1
    1234
    -9
    -2
    1
    4213
    2
    -1
    -7
    -2
    -1
    0
    Output:

    1234
    0
    0
    -1
    4213
  2. EJudge: PseudoVM 'Псевдопамять'

    Создать обработчик исключений, имитирующий «виртуальную память» для любого «запрещённого» адреса — такого, чтение или запись машинного слова по которому приводило бы к LOAD_ACCESS_FAULT или STORE_ACCESS_FAULT. Исключение — адрес 0x00000000, он не поддерживается. Предлагается использовать таблицу вида «виртуальный» адрес:значение. Размер таблицы — 16 таких пар (т. е. 128 байтов). Можно использовать адрес 0 для обозначения пустой ячейки.

    • «Виртуальная память» работает только на операциях lw и sw с регистром t0 в качестве приёмника или источника соответственно (другие варианты не проверяются)

    • Чтение по любому адресу работает так:
      • Если адрес уже есть в таблице, возвращается хранящееся там значение
      • Если адреса нет в таблице, возвращается 0

    • Запись по любому адресу работает так:
      • Если адрес уже есть в таблице, меняется его значение
      • Если адреса нет в таблице, но в ней есть свободная ячейка, сохраняется новая пара «виртуальный» адрес:значение

      • Если адреса нет в таблице, и таблица переполнена, не происходит ничего

    Это делается обработкой соответствующих двух исключений. Соблюдать конвенцию неприкосновенности регистров. К обработчику будет приписана следующая программа: PseudoVM.asm. Ввод и вывод полученной программы:

    Input:

    21
    123
    22
    1234
    20
    1001
    100500
    1000
    100
    -70001
    -70001
    -70000
    -70004
    0
    Output:

    1234
    100500
    0
    0
    -70001

<!> (необязательно) Исследовательский бонус. А можно ли сделать так, чтобы с «виртуальной памятью» работал любой регистр?

LecturesCMC/ArchitectureAssembler2022/06_Exceptions (последним исправлял пользователь FrBrGeorge 2022-04-03 18:02:35)