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

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

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

    • Например, некорректное обращение к памяти, попытка выполнить несуществующую инструкцию, вызов ecall или ebreak и т. п.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 (<!> в RARS ebreak обрабатывается окружением)

  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                            # Продолжим работу программы

Пример обработчика исключений, чуть более близкого к жизни (как всегда, спасибо @COKPOWEHEU за идею):

   1 .macro  csrprint        %message %csrreg %end
   2 .data
   3 msg:    .asciz  %message
   4 .text
   5         la      a0 msg
   6         li      a7 4
   7         ecall
   8         li      a0 ' '
   9         li      a7 11
  10         ecall
  11         csrr    a0 %csrreg
  12         li      a7 34
  13         ecall
  14         li      a0 %end
  15         li      a7 11
  16         ecall
  17 .end_macro
  18 
  19 .text
  20 .globl main                     # Это код старта системы
  21 main:
  22         li      t0 0
  23         csrw    t0 uscratch     # Вершина ядерного стека 0 - 4 == 0xfffffffc
  24         la      t0 handler
  25         csrw    t0 utvec                # обработчик прерывания
  26   
  27         li      t0 0x10
  28         csrw    t0 ustatus      # UPIE=1, а PIE=0, т. е. прерывания «были разрешены», но пока запрещены
  29         la      t0 user_main
  30         csrw    t0 uepc         # начало программы пользователя
  31         uret                    # восстанавливаем разрешение прерываний и переходим к программе
  32   
  33 user_main:                      # Это пользовательский код
  34         csrprint        "CSR cycle" cycle '\n'  
  35         lw      t0 0(zero)      # Исключение по доступу к NULL
  36         csrprint        "CSR cycle" cycle '\n'  
  37         li a7, 10
  38         ecall
  39   
  40 handler:
  41         csrrw   sp uscratch, sp # стек ядра меняем местами со стеком пользователя
  42         addi    sp sp -8
  43         sw      a0 4(sp)
  44         sw      a7 0(sp)        # сохраняем регистры на стеке
  45 
  46         csrprint        "Exception" ucause ' '  
  47         csrprint        "at" uepc ' '
  48         csrprint        "value" utval '\n'
  49  
  50         csrr    a0 uepc
  51         addi    a0 a0 4
  52         csrw    a0 uepc         # исключение не фатальное
  53         lw      a7 0(sp)
  54         lw      a0 4(sp)        # Восстанавливаем регистры
  55         addi    sp sp 8         # Восстанавливаем стек ядра
  56         csrrw   sp uscratch sp  # Восстанавливаем пользовательский стек и сохраняем стек ядра
  57         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 самое интересное исключение — ввод ерунды вместо числа — пока (?) несобственное. Так что будем упражняться с несуществующими системными вызовами и обращением к несуществующей памяти. Имеет смысл заранее написать комплект макросов и библиотеки-обработчика исключений для домашних заданий

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

TODO

  1. EJudge: UnCSR 'Несуществующий регистр'

    Написать обработчик исключения INSTRUCTION_ACCESS_FAULT, когда оно возникает в RARS при попытке обратиться к несуществующему CSR-регистру. Соответствующая инструкция при этом игнорируется и выполнение кода продолжается. Все остальные исключения, в том числе другие случаи INSTRUCTION_ACCESS_FAULT, должны считаться фатальными и приводить к останову программы с диагностикой «Exception №», где № — это десятичный номер исключения. К обработчику будет приписана такая проверяющая программа (TODO) и соответствующий проверочный код. Метка обработчика должна называться handler:.

    • Проверочный код для примера ниже:
      • TODO

    Input:

    10
    Output:

    10
    Exception 5
  2. EJudge: SafeInt 'Ввод целого'

    Написать новый ecall 71 — «безопасный ввод целого». Этот ecall должен вводить строку, пропускать в ней лидирующие пробелы, а затем преобразовывать находящееся в ней число в десятичной записи в целое. Вызов должен возвращать количество обработанных цифр в a1, а само число — в a0. Если в начале строки строке не содержится целого числа, оба регистра нулевые. Задачу можно решить так: обработать исключение, проверить ucause на равенство ENVIRONMENT_CALL и содержимого a7 на равенство 71. Если возникло какое-то другое исключение или a7 не равен 71, немедленно завершать работу с диагностикой «Exception №», где № — номер исключения. К решению будет приписана такая проверяющая программа: (TODO)

    Input:

    123e4
     -3456
    no
    Output:

    123 3
    -3456 4
    0 0
  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/ArchitectureAssembler2025/07_Exceptions (последним исправлял пользователь FrBrGeorge 2025-04-04 13:48:46)