Регистры статуса и управления. Исключительные ситуации
TODO написать кое-где развёрнутый текст
Исключение (exeption) — возникает при выполнении некоторой инструкции в программе и требует дополнительных действий перед тем, как выполнить следующую инструкцию
- Например, некорректное обращение к памяти, попытка выполнить несуществующую инструкцию, вызвать несуществующий ecall и т. п.
Прерывание (interrupt) — возникает асинхронно в произвольный момент работы программы по инициативе внешнего устройства и требует дополнительных действий перед тем, как продолжить выполнять текущую инструкцию
- Например, срабатывание таймера, появление данных на устройстве ввода, окончание операции вывода большого блока данных и т. п.
RISC-V F: исключения FPU накапливаются в виде флагов в CSR-регистре fflags, обрабатывать их надо явно.
В большинстве архитектур для обработки исключений и прерываний используется механизм ловушек (trap):
- Выполнение текущей программы немедленно приостанавливается
- Управление передаётся специальному обработчику
- После завершения работы обработчика выполнение продолжается с прежнего места
Свойства ловушек ( эти свойства не взаимоисключающие!):
Собственные. Обработчик выполняется тем же окружением, что и прерванная программа. Например, ecall под управлением операционной системы в плоской модели памяти приводит к переключению в режим ядра, при этом код обработчика выполняется «тем же самым процессором» в «той же самой памяти». Пример несобственных ловушек — обработка ecall в RARS.
Произвольные. Обработчик вызывается синхронно по запросу со стороны выполняемой программы и с целью получить какой-то результат. Пример произвольной ловушки — ecall (любой), пример непроизвольной — обработчик прерывания ввода/вывода. Произвольная ловушка может не вернуться в программу (например, ecall 10).
Невидимые. Переход по ловушке и выполнение обработчика никак не затрагивают контекст выполняемой программы — настолько, что в идеале программа не может узнать о том, что произошло какое-то событие. Невидимы: программная эмуляция неподдерживаемых инструкций, подгрузка существующих страниц виртуальной памяти, обработка прерываний в других процессах многозадачных систем и т. п. Разумеется, факт сработавшей ловушки можно попробовать угадать по косвенным признакам, например, по «мгновенному» скачку системного времени.
Фатальные. Часть ловушек происходят потому, что выполнять программу больше нет возможности. Обработчик выполняет какие-то действия «напоследок» — например, по корректному/штатному завершению задачи. Примеры фатальных ловушек: ошибки в ядре ОС, срабатывание сторожевого таймера (watchdog) и т. п.
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
Вариант Расширение N
Блок счётчиков и регистров управления 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 — нет (возможно, снова появятся в спецификации для малых устройств).
Обработчик исключений
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 |
Номера исключений RARS:
- INSTRUCTION_ADDR_MISALIGNED
- INSTRUCTION_ACCESS_FAULT
- ILLEGAL_INSTRUCTION
- ??? (BREAKPOINT)
- LOAD_ADDRESS_MISALIGNED
- LOAD_ACCESS_FAULT
- STORE_ADDRESS_MISALIGNED
- STORE_ACCESS_FAULT
- ENVIRONMENT_CALL
Чтобы создать работающий обработчик исключений, следует:
Установить utvec на адрес кода обработчика
- Адрес кода всегда кратен 4, два младших бита имеют особое значение, но в RARS не используются
Установить биты, соответствующие обрабатываемым прерываниям в uie
Установить в 1 бит разрешения прерывания (младший) в ustatus, чтобы включить обработчик
Дисциплина оформления обработчика:
Можно рассчитывать на постоянное значение регистра uscratch и использовать его на своё усмотрение
Перед выходом из обработчика необходимо восстановить значения всех регистров (включая t* и f*) для соблюдения «локальной невидимости». Исключение, а тем более — прерывание — может возникнуть когда угодно, а после возвращения ничего не должно меняться.
Для этого сразу после входа в ловушку необходимо где-то сохранить часть контекста, которую она испортит, а перед выходом — восстановить. Вот тут-то и понадобится uscratch.
Вернуть управление программе с помощью инструкции uret.
- В случае ловушки прерывания это должен быть адрес прерванной инструкции
В случае ловушки исключения это должен быть адрес инструкции, непосредственно следующей за прерванной, т. е. uepc+4
Дополнительная дисциплина для 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»
Ещё одно отличие 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 (обычно) отсутствет, потому что для эффективной реализации требуется одно из двух:
- Двойная косвенная адресация
Непонятно где находящийся (аппаратный?) фрагмент кода, который будет считывать значений Из ячейки, допустим, 0x80000108 и передавать управление на 0x800015b0
Однако в небольших микроконтроллерах с плоской моделью памяти иногда делают так (хотя это и сложнее, чем схема, описанная ниже)
Поэтому в 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 # и т. д.
- В примере показано начало таблицы прерывания для режима Machine Level
- Традиционно все исключения — это прерывание №0
- Места в памяти при этом занято столько же, сколько и в таблице адресов
- Код позиционно-независим (при условии, что обработчики загружены радом с вектором прерываний)
В RISC-V обработка ловушек вектором включяется с помощью младшего бита CSR-регистра uvec (как часть адреса младший бит не имеет смысла, т. к. адрес инструкции обработчика кратен 4 даже в упакованном ISA).
Д/З
Домашнее задание будет на обработку исключений. К сожалению, в RARS самое интересное исключение — ввод ерунды вместо числа — пока (?) несобственное. Так что будем упражняться с несуществующими системными вызовами и обращением к несуществующей памяти. Имеет смысла заранее написать комплект макросов и библиотеки-обработчика исключений для домашних заданий
- Соблюдать конвенцию неприкосновенности регистров
Аккуратно отлавливать случаи других исключений, в т. ч. попытки вызова других несуществующих ecall — в этом случае RARS должен сам это исключение обрабатывать. Я сделал так:
Сбросил в ustauts сохранённый бит UPIE (не UIE) в 0
Вышел из ловушки, не увеличивая uepc
⇒ выполнилась та же инструкция, но на этот раз в ustauts UIE оказался 0 (он туда приехал из только что занулённого UPIE), вместо ловушки сработал RARS
Домашние задания состоят в написании только обработчика исключений. К обработчику будет приписана тестирующая программа, указанная в задании, и полученный файл передан на тестирование.
TODO EJudge.
EJudge: NewEcall 'Новые вызовы'
Создать обработчик исключений с меткой handler:, реализующий три «новых системных вызова» (100, 101 и 102) для работы со «скрытыми регистрами»:
(a0 = размер). Однократно заказывает у системы память размером в a0 машинных слов, не возвращает ничего (размер и заказанный адрес запоминает в недрах обработчика). Это «скрытые регистры»
(a0 = номер). Возвращает в a0 содержимое «скрытого регистра» № номер. Если номер ⩾ размер, берётся остаток от деления номер % размер (actually, всегда))
(a0 = номер, a1 = значение). Заносит в регистр № номер % размер значение значение
Это делается путём обработки исключения, проверки ucause на равенство ENVIRONMENT_CALL и содержимого a7. Соблюдать конвенцию неприкосновенности регистров. К обработчику будет приписана такая проверяющая программа: NewEcall.asm. Ввод и вывод полученной программы с обработчиком:
8 1 1234 -9 -2 1 4213 2 -1 -7 -2 -1 0
1234 0 0 -1 4213
EJudge: PseudoVM 'Псевдопамять'
Создать обработчик исключений, имитирующий «виртуальную память» для любого «запрещённого» адреса — такого, чтение или запись машинного слова по которому приводило бы к LOAD_ACCESS_FAULT или STORE_ACCESS_FAULT. Исключение — адрес 0x00000000, он не поддерживается. Предлагается использовать таблицу вида «виртуальный» адрес:значение. Размер таблицы — 16 таких пар (т. е. 128 байтов). Можно использовать адрес 0 для обозначения пустой ячейки.
«Виртуальная память» работает только на операциях lw и sw с регистром t0 в качестве приёмника или источника соответственно (другие варианты не проверяются)
- Чтение по любому адресу работает так:
- Если адрес уже есть в таблице, возвращается хранящееся там значение
Если адреса нет в таблице, возвращается 0
- Запись по любому адресу работает так:
- Если адрес уже есть в таблице, меняется его значение
Если адреса нет в таблице, но в ней есть свободная ячейка, сохраняется новая пара «виртуальный» адрес:значение
- Если адреса нет в таблице, и таблица переполнена, не происходит ничего
Это делается обработкой соответствующих двух исключений. Соблюдать конвенцию неприкосновенности регистров. К обработчику будет приписана следующая программа: PseudoVM.asm. Ввод и вывод полученной программы:
21 123 22 1234 20 1001 100500 1000 100 -70001 -70001 -70000 -70004 0
1234 100500 0 0 -70001
(необязательно) Исследовательский бонус. А можно ли сделать так, чтобы с «виртуальной памятью» работал любой регистр?
Номер регистра-источника или приёмника можно выковырять из прерванной команды, адрес которой хранится в uepc, и сделать большую цепочку условных операторов по номеру регистра
- А можно включить в RARS «Settings → Self-modifying code» (чтение и запись в секции кода), скопировать прерванную команду, модифицировать её, а затем выполнить модифицированную команду!
Например, выковырять регистр-приёмник из lw регистр-приёмник что-то-там и вковырять его в инструкцию lw регистр-приёмник найденное_значение, которая будет стоять аккурат перед uret:
Поскольку lw — это псевдоинструкция auipc + собственно lw, менять придётся в двух местах
1 .data 2 vitrvalue: .word 0 # Значение ячейки 3 .text 4 handler: # сохраняем конекст 5 # разбираемся с исключением, выясняем причину 6 # … 7 # причина — LOAD_ACCESS_FAULT 8 csrr t0 uepc # адрес инструкции lw (исключение произошло на ней) 9 addi t2 t0 -4 # адрес инструкции auipc 10 lw t0 (t0) # сама инструкция lw 11 lw t2 (t2) # сама инструкция auipc 12 # зануляем в инструкциях все поля, кроме регистра-приёмника 13 # … 14 la t4 modify # адрес для модификации auipc 15 lw t3 (t4) # инструкция для модификации auipc 16 addi t4 t4 4 # адрес для модификации lw 17 lw t1 (t4) # инструкция для модификации lw 18 # зануляем номера регистров-приёмника в этих инструкциях 19 # … 20 or t3 t3 t2 # формируем новую инструкцию auipc 21 or t1 t1 t0 # формируем новую инструкцию lw 22 sw t1 (t4) # записываем прямо в код! 23 addi t4 t4 -4 24 sw t3 (t4) 25 # находим значение «виртуальной ячейки» 26 # и сохраняем его в vitrvalue 27 # делаем всякие другие дела 28 # восстанавливаем контекст 29 modify: lw t0 vitrvalue # t0 тут будет подменён 30 uret