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

Ловушка (trap) — ситуация, при которой надо срочно выполнить код из другого места в памяти, невзирая на состояние процессора, а затем, возможно, продолжить со старого места. Ловушки распознаются аппаратно и в RISC-V бывают двух видов:

  1. Исключение (exception) — возникает при выполнении некоторой инструкции в программе и требует дополнительных действий перед тем, как выполнить следующую инструкцию

    • Например, некорректное обращение к памяти, попытка выполнить несуществующую инструкцию, вызвать несуществующий ecall и т. п.
  2. Прерывание (interrupt) — возникает асинхронно в произвольный момент работы программы по инициативе внешнего устройства и требует дополнительных действий перед тем, как продолжить выполнять текущую инструкцию

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

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

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

Параметры ловушек

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

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

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

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

  5. Фатальные / штатные. Часть ловушек происходят потому, что выполнять программу больше нет возможности. Обработчик выполняет какие-то действия «напоследок» — например, по корректному/штатному завершению задачи. Примеры фатальных ловушек: ошибки в ядре ОС, срабатывание сторожевого таймера (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 — нет. Было т. н. «расширение N», но его перестали развивать. Если процессор совсем простой, скорее всего он работает на уровне Machine, а если он поддерживает несколько уровней, исключения удобнее обрабатывать уровнем выше.

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

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

Номера исключений (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 (в «больших» архитектурах это значение соответствует уровню, на котором произошёл вызов: 8-Umode, 9-Smode, 10-Hmode, 11-Mmode)

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

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

Дополнительная дисциплина для 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 (кода!):

   1 .text.mtvec_table
   2         b  riscv_mtvec_exception
   3         b  riscv_mtvec_ssi
   4         b  riscv_mtvec_msi
   5         b  riscv_mtvec_sti
   6         b  riscv_mtvec_mti
   7         # и т. д.
   8 

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

Д/З

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

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

  1. EJudge: UnRead 'Невыровненное слово'

    • TODO В 2024 году в формулировке задачи имеется неточность: обработчик называется handle:, а не handler:, как в остальных задачах. В будущем это надо будет исправить.

    Написать обработчик исключения LOAD_ADDRESS_MISALIGNED с меткой handle:. Обработчик должен помогать прочесть машинное слово с адреса, не кратного 4, в регистр t0.

    • Гарантируется, что приёмник — всегда регистр t0

    • Порядок читаемых байтов — Addr (старший), Addr + 1, Addr + 2, Addr + 3 (младший)

    К обработчику будет приписана такая проверяющая программа: UnRead.asm

    Input:

    0
    1
    2
    3
    4
    5
    6
    7
    8
    -1
    Output:

    0x11223344
    0x88112233
    0x77881122
    0x66778811
    0x55667788
    0xcc556677
    0xbbcc5566
    0xaabbcc55
    0x99aabbcc
  2. 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
  3. 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/ArchitectureAssembler2024/07_Exceptions (последним исправлял пользователь FrBrGeorge 2024-04-13 19:09:58)