Регистры статуса и управления. Исключительные ситуации
Ловушка (trap) — ситуация, при которой надо срочно выполнить код из другого места в памяти, невзирая на состояние процессора, а затем, возможно, продолжить со старого места. Ловушки распознаются аппаратно и в RISC-V бывают двух видов:
Исключение (exception) — возникает при выполнении некоторой инструкции в программе и требует дополнительных действий перед тем, как выполнить следующую инструкцию
- Например, некорректное обращение к памяти, попытка выполнить несуществующую инструкцию, вызвать несуществующий ecall и т. п.
Прерывание (interrupt) — возникает асинхронно в произвольный момент работы программы по инициативе внешнего устройства и требует дополнительных действий перед тем, как продолжить выполнять текущую инструкцию
- Например, срабатывание таймера, появление данных на устройстве ввода, окончание операции вывода большого блока данных и т. п.
RISC-V F: исключения FPU накапливаются в виде флагов в CSR-регистре fflags, обрабатывать их надо явно.
В большинстве архитектур для обработки исключений и прерываний используется механизм ловушек (trap):
- Выполнение текущей программы немедленно приостанавливается
- Управление передаётся специальному обработчику
- После завершения работы обработчика выполнение продолжается с прежнего места
Характеристики ловушек
Собственные / несобственные. Обработчик выполняется тем же / иным окружением, что и прерванная программа.
Пример собственных ловушек: обработка ecall в RARS в плоской модели памяти, где ловушки обрабатываются «тем же самым процессором» в «той же самой памяти».
Несобственная ловушка: ecall под управлением операционной системы приводит к переключению в режим ядра, при этом код обработчика выполняется ядром ОС в другом контексте выполнения.
Внутренние / внешние. Обработчик внутренней ловушки вызывается в зависимости от состояния процессора, регистров и других свойств контекста выполнения / независимо от свойств контекста.
- Внешние вызовы и обработчики ошибок (например при обращении к несуществующей памяти) — это внутренние (да!) ловушки.
- Прерывание при получении данных с внешнего устройства или по таймеру — это внешняя ловушка, потому что состояние таймера или другого внешнего устройства никак не отражается в процессоре.
Синхронные / асинхронные. Обработчик вызывается синхронно по запросу со стороны выполняемой программы и с целью получить какой-то результат / по неведомой этой программе причине в неопределённый момент.
Пример синхронной ловушки — ecall (любой) и исключение.
- Пример асинхронной ловушки — обработчик прерывания ввода/вывода.
Невидимые / видимые. Переход по ловушке и выполнение обработчика никак не затрагивают контекст выполняемой программы / явно изменяет в нём что-то.
- В идеале программа не может узнать о том, что сработала невидимая ловушка. Невидимы: программная эмуляция неподдерживаемых инструкций, подгрузка существующих страниц виртуальной памяти, обработка прерываний в других процессах многозадачных систем и т. п. Разумеется, факт сработавшей ловушки можно попробовать угадать по косвенным признакам, например, по «мгновенному» скачку системного времени.
Видимы многие внешние вызовы, которые возвращают значения в регистрах a* и/или изменяют содержимое оперативной памяти вызвавшего их контекста.
Фатальные / штатные. Ловушка сработала потому, что выполнять программу больше нет возможности / чтобы выполнить некоторое действие.
Обработчик фатальной ловушки выполняет какие-то действия «напоследок» и скорее всего не передаст управления обратно в прерванный контекст. Например, попытка процесса обратиться к недоступной памяти фатальна для процесса, ловушка обрабатывается окружением, процесс останавливается, а ОС продолжает работу. Аналогичная ошибка в самом ядре ОС будет обработана ядром: выведется диагностика, после чего вообще вся система будет остановлена.
- Таким образом, ошибка в запускаемом процессе фатальна для него, но штатна для запустившего его окружения. Пример штатной ловушки — внешний вызов
RARS:
- все ловушки видимые (кроме фатальных)
- несобственные: ecall
- собственные: прерывания и некоторые исключения
- много исключений считаются фатальными
Режимы работы CPU или Башня косвенности
Ловушки — довольно общий механизм для «обработки событий в вычислительной системе». Можно, например, в спецификации потребовать, чтобы все ловушки были несобственные и невидимые — тогда описание ловушек не будет входит в архитектуру исполнителя программы, а только в архитектуру окружения (которое может быть каким угодно, например, программой на Java ☺).
Возможные аппаратные требования для реализации собственных ловушек:
Определение типа и причины ловушки (как минимум должны где-то храниться)
- Аппаратный разбор причины (например, вектор обработчиков вместо общего)
- Мгновенное переключение контекста (регистров, флагов, аппаратного стека, если таковой имеется, и т. п.),
- сохранение и восстановление предыдущего контекста
- (непонятно, сколько должно быть таких «запасных хранилищ» для контекста)
Запрет и/или дисциплина обработки повторных ловушек (исключительных ситуаций внутри ловушки), возможно, специальный режим работы процессора
- Поддержка невидимости в рамках одной среды исполнения:
- Тесно связано с многозадачностью: если есть аппаратная поддержка «задач», объявляем одну такую задачу «ядром», которое будет обрабатывать ловушки
- ⇒ Т. н. «режим ядра» (kernel mode) VS «режим пользователя» (user mode)
- теперь обработчики выполняются только ядром, в котором доступны т. н. «привилегированные инструкции»
- а также обеспечение изоляции / ограничения доступа / разделения времени / …
- Конвейер, суперскалярность, многоядерность и т. п. усложнения архитектуры могут добавить сложности в этот процесс
RISC-V: Несколько спецификаций для разных режимов работы (ссылки ниже могут измениться после ратификации новых расширений/исправлений):
Unprivileged Spec — user level
Privileged Spec — kernel level
отдельно «machine level» — полный доступ, плоская модель памяти,
отдельно расширение H (hypervisor)(между Machine и Supervisor) — для управления окружениями, т. е. виртуализации
отдельно «supervisor level» — доступ, достаточный для организации виртуальной памяти всех процессов (собственно окружение)
В RARS мы работаем с плоской моделью памяти, наиболее близкий вариант — устаревшая версия User-level ISA 2.2
Блок счётчиков и регистров управления CSR
Подробнее про блок Control and Status Registers
Типичный процессор, если сильно упрощать, состоит из арифметико-логическтого устройства и устройства управления. АЛУ занимается вычислениями, УУ занимается интерпретацией команд, реагирует на изменение состояния процессора, а также само изменяет это состояние. Часть работы УУ не требует контроля со стороны, так как алгоритм задан заранее и не меняется. Но некоторые функции управления хочется сделать модифицируемыми (например, программно обрабатывать различные системные события).
Есть примерно три способа реализовать интерфейс управления процессором:
- Придумать специальный управляющий сопроцессор (примерно как как FPU, но цель другая), разделить инструкции на обычные и инструкции управления. При этом появляются регистры управляющего сопроцессора, возможно, особенная память, действия внутри этого сопроцессора и т. п.
- Отказаться от идеи отдельного сопроцессора, и для каждой функции управления ввести отдельную инструкцию в ISA.
- Спланировать управляющий сопроцессор (или УУ) как устройство с заданной логикой работы, оставив в интерфейсе управления только специальные регистры. Тогда работа с этими регистрами со стороны ЦПУ общего назначения (чтение и запись) и будет приводить к изменению состояния и логики работы.
В 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-битовое
См., например, дизайн регистра fcsr
Пример: во что раскладываются псевдоинструкции управления 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)
Пример из документации:
- Чтение из регистра зажигает лампочку, запись нечётного числа — гасит. Обе операции имеют побочный эффект (чтение не меняет CSR, но лампочка загорается; если в CSR уже было нечётное число и лампочка горела, запись в CSR того же самого числа её гасит)
- Запись в регистр чётного числа зажигает лампочку, нечётного — гасит. Обе операции имеют только непрямой эффект, но не побочный
Побочного эффекта по возможности следует избегать:
- В стандартном 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:
- INSTRUCTION_ADDR_MISALIGNED
- INSTRUCTION_ACCESS_FAULT
- ILLEGAL_INSTRUCTION
- ??? (BREAKPOINT)
- LOAD_ADDRESS_MISALIGNED
- LOAD_ACCESS_FAULT
- STORE_ADDRESS_MISALIGNED
- STORE_ACCESS_FAULT
- 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 (кода!):
- В примере показано начало таблицы прерывания для режима Machine Level
Традиционно все исключения — это прерывание № 0 (векторизовать исключения обычно смысла нет)
- Места в памяти при этом занято столько же, сколько и в таблице адресов
- Код позиционно-независим (при условии, что обработчики загружены в одном адресном пространстве с вектором прерываний)
В RISC-V обработка ловушек вектором включается с помощью младшего бита CSR-регистра uvec (как часть адреса младший бит не имеет смысла, т. к. адрес инструкции обработчика кратен 4 даже в упакованном ISA).