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

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

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

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

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

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

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

  • Выполнение текущей программы немедленно приостанавливается
  • Управление передаётся специальному обработчику
  • После завершения работы обработчика выполнение продолжается с прежнего места

Характеристики ловушек

  1. Собственные / несобственные. Обработчик выполняется тем же / иным окружением, что и прерванная программа.

    • Пример собственных ловушек: обработка ecall в RARS в плоской модели памяти, где ловушки обрабатываются «тем же самым процессором» в «той же самой памяти».

    • Несобственная ловушка: ecall под управлением операционной системы приводит к переключению в режим ядра, при этом код обработчика выполняется ядром ОС в другом контексте выполнения.

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

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

    • Пример синхронной ловушки — ecall (любой) и исключение.

    • Пример асинхронной ловушки — обработчик прерывания ввода/вывода.
  4. Невидимые / видимые. Переход по ловушке и выполнение обработчика никак не затрагивают контекст выполняемой программы / явно изменяет в нём что-то.

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

  5. Фатальные / штатные. Ловушка сработала потому, что выполнять программу больше нет возможности / чтобы выполнить некоторое действие.

    • Обработчик фатальной ловушки выполняет какие-то действия «напоследок» и скорее всего не передаст управления обратно в прерванный контекст. Например, попытка процесса обратиться к недоступной памяти фатальна для процесса, ловушка обрабатывается окружением, процесс останавливается, а ОС продолжает работу. Аналогичная ошибка в самом ядре ОС будет обработана ядром: выведется диагностика, после чего вообще вся система будет остановлена.

    • Таким образом, ошибка в запускаемом процессе фатальна для него, но штатна для запустившего его окружения. Пример штатной ловушки — внешний вызов

RARS:

  • все ловушки видимые (кроме фатальных)
  • несобственные: ecall
  • собственные: прерывания и некоторые исключения
  • много исключений считаются фатальными

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

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

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

  • Определение типа и причины ловушки (как минимум должны где-то храниться)

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

  • Поддержка невидимости в рамках одной среды исполнения:
    • Тесно связано с многозадачностью: если есть аппаратная поддержка «задач», объявляем одну такую задачу «ядром», которое будет обрабатывать ловушки
    • ⇒ Т. н. «режим ядра» (kernel mode) VS «режим пользователя» (user mode)
      • теперь обработчики выполняются только ядром, в котором доступны т. н. «привилегированные инструкции»
      • а также обеспечение изоляции / ограничения доступа / разделения времени / …
  • Конвейер, суперскалярность, многоядерность и т. п. усложнения архитектуры могут добавить сложности в этот процесс

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

  • Unprivileged Spec — user level

  • Privileged Spec — kernel level

    • На сайте five-embeddev

    • отдельно «machine level» — полный доступ, плоская модель памяти,

    • отдельно расширение H (hypervisor)(между Machine и Supervisor) — для управления окружениями, т. е. виртуализации

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

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

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

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

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

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

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

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

  • Всего регистров 4096 (номер CSR-регистра — 12 битов).
  • Формат номера:
    • Старшие 2 бита (11:10) — доступ: RW (00, 01, 10) или RO (11)
    • Ещё 2 бита (9:8) — уровень: 00 - user, 01 — supervisor, 10 — hypervisor, 11 — machine
      • Например, обработка ловушек есть на всех уровнях
    • Standard/custom хитрая таблица

  • Номера регистров — не «адреса»:
    • регистры нумеруются подряд: 0, 1, 2, 3 и т. п.
    • при этом занимают 32 или 64 разряда в зависимости от разрядности архитектуры
  • Атомарные (это важно) R/W инструкции типа I

    • csrrw[i] — обмен значениями между регистром CSR и регистром общего назначения

      • csrrw регистр-приёмник, csr-регистр, регистр-источник

    • а также csrrs[i] / csrrc[i] включение/выключение битов

      • содержимое CSR-регистра заносится в приёмник
      • все равные 1 биты из источника выставляются в 1 в CSR-регистре (csrrs) или в 0 (csrrc)
  • Если не хотим читать или писать, используем регистр zero

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

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

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

  • 12-битная immediate-часть инструкции типа I занята номером регистра CSR

  • ⇒ Для числа N в инструкции вида csrrwi регистр-приёмник, csr-регистр, N используется поле rs1 (регистр-источник), так что оно может быть только 5-битовое

Пример: во что раскладываются псевдоинструкции управления 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
  • Регистры fflags (1) и frm (2) — это всего лишь биты регистра fcsr (3), изменения, сделанные в них, отражаются в fcsr, и наоборот.

  • ⇒ запись в эти регистры имеет «непрямой эффект» (впрочем, для непрямого эффекта достаточно и того, что их значение изменяет поведение FPU)

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.

  • Если состояние CPU однозначно соответствует содержимому CSR-регистра, и меняется, если записать туда определённые данные, это называется «непрямой эффект» (indirect effect), а побочным эффектом не считается

    • (я бы, конечно, назвал такой эффект прямым, но легаси есть легаси)

    • Например, появление некоторой комбинации значений на CSR-регистрах может вызывать ловушку: это однозначная зависимость от содержимого регистров, и, следовательно, непрямой эффект
  • Если состояние CPU не может быть распознано на основании только содержимого CSR-регистра, но зависит и от самого факта чтения или записи, это «побочный эффект» (side effect)

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

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

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

  • В стандартном ISA чтение не должно иметь побочного эффекта
  • В стандартном ISA запись может иметь документированный побочный эффект

  • В расширении ISA обе операции могут иметь документированный побочный эффект при доступе к описанным в этом расширении нестандартным CSR-регистрам

  • При использовании zero в качестве регистра источника или приёмника распознаётся ситуация «не было записи» и «не было чтения» соответственно, и гарантируется отсутствие побочных эффектов, если они у соответствующей операции были

Проблемы синхронизации (особенно при наличии 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

  • User Previous IE - устанавливается автоматически при входе в ловушку; предотвращает повторный вход.
  • User Interrupt enable - глобальное разрешение ловушек (0 - отключить, 1 — включить).
  • когда происходит вход в ловушку:
    • бит 0 сбрасывается в 0
    • бит 4 устанавливается в 1
  • когда происходит выход из ловушки, восстанавливаются предыдущее значение

В регистре 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)

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

  • Установить utvec на адрес кода обработчика

    • Адрес кода всегда кратен 4, два младших бита имеют особое значение, но в RARS не используются
  • Установить биты, соответствующие обрабатываемым прерываниям в uie

  • Установить в 1 бит разрешения прерывания (младший) в ustatus, чтобы включить обработчик

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

  • Можно рассчитывать на постоянное значение регистра uscratch и использовать его на своё усмотрение

  • Перед выходом из обработчика необходимо восстановить значения всех регистров (включая t* и f*) для соблюдения «локальной невидимости». Исключение, а тем более — прерывание — может возникнуть когда угодно, а после возвращения ничего не должно меняться.

    • Для этого сразу после входа в ловушку необходимо где-то сохранить часть контекста, которую она испортит, а перед выходом — восстановить. Вот тут-то и понадобится uscratch.

  • Вернуть управление программе с помощью инструкции uret.

    • В случае ловушки прерывания это должен быть адрес прерванной инструкции
    • В случае ловушки исключения это должен быть адрес инструкции, непосредственно следующей за прерванной, т. е. uepc+4

    • (на больших архитектурах есть также mret и sret для возврата на соответствующий уровень выполнения, а вот hret — нет; интересно, почему)

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

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

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

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

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

Адрес

Содержимое

Пояснение

0x80000100

0x80000180

адрес обработчика прерывания № 0

0x80000104

0x800007ac

адрес обработчика прерывания № 1

0x80000108

0x800015b0

адрес обработчика прерывания № 2

0x80000120

0x80000e54

адрес обработчика прерывания № 8

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

  • Двойная косвенная адресация
  • Непонятно где находящийся (аппаратный?) фрагмент кода, который будет считывать значения из ячейки, допустим, 0x80000108 и передавать управление на 0x800015b0

  • Однако в небольших микроконтроллерах с плоской моделью памяти иногда делают так (хотя это и сложнее, чем схема, описанная ниже)

Поэтому в 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 
  • В примере показано начало таблицы прерывания для режима Machine Level
  • Традиционно все исключения — это прерывание № 0 (векторизовать исключения обычно смысла нет)

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

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

LecturesCMC/ArchitectureAssemblerProject/16_CSRsTraps (последним исправлял пользователь FrBrGeorge 2024-07-24 16:05:59)