Цели этого руководства:
- Узнайте, как семантические функции используются для реализации семантики инструкций.
- Узнайте, как семантические функции связаны с описанием декодера ISA.
- Напишите семантические функции инструкций для инструкций RiscV RV32I.
- Протестируйте финальную версию симулятора, запустив небольшой исполняемый файл «Hello World».
Обзор семантических функций
Семантическая функция в MPACT-Sim — это функция, которая реализует работу инструкции так, что ее побочные эффекты видны в моделируемом состоянии точно так же, как побочные эффекты инструкции видны при аппаратном выполнении. Внутреннее представление каждой декодированной инструкции в симуляторе содержит вызываемый объект, который используется для вызова семантической функции для этой инструкции.
Семантическая функция имеет сигнатуру void(Instruction *)
, то есть функцию, которая принимает указатель на экземпляр класса Instruction
и возвращает void
.
Класс Instruction
определен в файле Instruction.h.
Для написания семантических функций нас особенно интересуют векторы интерфейса операндов источника и назначения, доступ к которым осуществляется с помощью вызовов методов Source(int i)
и Destination(int i)
.
Интерфейсы операндов источника и назначения показаны ниже:
// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
public:
// Methods for accessing the nth value element.
virtual bool AsBool(int index) = 0;
virtual int8_t AsInt8(int index) = 0;
virtual uint8_t AsUint8(int index) = 0;
virtual int16_t AsInt16(int index) = 0;
virtual uint16_t AsUint16(int) = 0;
virtual int32_t AsInt32(int index) = 0;
virtual uint32_t AsUint32(int index) = 0;
virtual int64_t AsInt64(int index) = 0;
virtual uint64_t AsUint64(int index) = 0;
// Return a pointer to the object instance that implements the state in
// question (or nullptr) if no such object "makes sense". This is used if
// the object requires additional manipulation - such as a fifo that needs
// to be pop'ed. If no such manipulation is required, nullptr should be
// returned.
virtual std::any GetObject() const = 0;
// Return the shape of the operand (the number of elements in each dimension).
// For instance {1} indicates a scalar quantity, whereas {128} indicates an
// 128 element vector quantity.
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
virtual ~SourceOperandInterface() = default;
};
// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
public:
virtual ~DestinationOperandInterface() = default;
// Allocates a data buffer with ownership, latency and delay line set up.
virtual DataBuffer *AllocateDataBuffer() = 0;
// Takes an existing data buffer, and initializes it for the destination
// as if AllocateDataBuffer had been called.
virtual void InitializeDataBuffer(DataBuffer *db) = 0;
// Allocates and initializes data buffer as if AllocateDataBuffer had been
// called, but also copies in the value from the current value of the
// destination.
virtual DataBuffer *CopyDataBuffer() = 0;
// Returns the latency associated with the destination operand.
virtual int latency() const = 0;
// Return a pointer to the object instance that implmements the state in
// question (or nullptr if no such object "makes sense").
virtual std::any GetObject() const = 0;
// Returns the order of the destination operand (size in each dimension).
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
};
Основной способ написания семантической функции для обычной инструкции с тремя операндами, такой как 32-битная инструкция add
, заключается в следующем:
void MyAddFunction(Instruction *inst) {
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
uint32_t c = a + b;
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, c);
db->Submit();
}
Давайте разберем части этой функции. Две первые строки тела функции считываются из исходных операндов 0 и 1. Вызов AsUint32(0)
интерпретирует базовые данные как массив uint32_t
и извлекает 0-й элемент. Это верно независимо от того, является ли базовый регистр или значение массивом или нет. Размер (в элементах) исходного операнда можно получить из метода исходного операнда shape()
, который возвращает вектор, содержащий количество элементов в каждом измерении. Этот метод возвращает {1}
для скаляра, {16}
для вектора из 16 элементов и {4, 4}
для массива 4x4.
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
Затем временному элементу uint32_t
с именем c
присваивается значение a + b
.
Следующая строка может потребовать немного большего пояснения:
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
DataBuffer — это объект с подсчетом ссылок, который используется для хранения значений в моделируемом состоянии, например в регистрах. Он относительно нетипизирован, хотя его размер зависит от объекта, из которого он выделен. В данном случае это размер sizeof(uint32_t)
. Этот оператор выделяет новый буфер данных, размер которого соответствует месту назначения, которое является целью этого операнда назначения - в данном случае 32-битный целочисленный регистр. DataBuffer также инициализируется с архитектурной задержкой для инструкции. Это указывается во время декодирования инструкции.
Следующая строка рассматривает экземпляр буфера данных как массив uint32_t
и записывает значение, хранящееся в c
, в 0-й элемент.
db->Set<uint32_t>(0, c);
Наконец, последний оператор передает буфер данных в симулятор, который будет использоваться в качестве нового значения состояния целевой машины (в данном случае регистра) после задержки инструкции, которая была установлена при декодировании инструкции, и вектора целевого операнда. населен.
Хотя это достаточно короткая функция, в ней есть немного шаблонного кода, который повторяется при реализации инструкции за инструкцией. Кроме того, это может скрыть реальную семантику инструкции. Чтобы еще больше упростить написание семантических функций для большинства инструкций, существует ряд шаблонных вспомогательных функций, определенных в Instruction_helpers.h . Эти помощники скрывают шаблонный код для инструкций с одним, двумя или тремя исходными операндами и одним целевым операндом. Давайте посмотрим на две вспомогательные функции операнда:
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument1, Argument2)> operation) {
Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument, Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Result, Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
Вы заметите, что вместо использования такого утверждения:
uint32_t a = inst->Source(0)->AsUint32(0);
Вспомогательная функция использует:
generic::GetInstructionSource<Argument>(instruction, 0);
GetInstructionSource
— это семейство шаблонных вспомогательных функций, которые используются для предоставления шаблонных методов доступа к операндам источника инструкций. Без них каждая вспомогательная функция инструкций должна была бы специализироваться для каждого типа, чтобы получить доступ к исходному операнду с помощью правильной функции As<int type>()
. Вы можете увидеть определения этих функций шаблона в файле Instruction.h .
Как видите, существует три реализации, в зависимости от того, совпадают ли типы исходных операндов с целевыми, отличаются ли адресаты от источников или все они разные. Каждая версия функции принимает указатель на экземпляр инструкции, а также вызываемый объект (включая лямбда-функции). Это означает, что теперь мы можем переписать вышеприведенную семантическую функцию add
следующим образом:
void MyAddFunction(Instruction *inst) {
generic::BinaryOp<uint32_t>(inst,
[](uint32_t a, uint32_t b) { return a + b; });
}
При компиляции с использованием bazel build -c opt
и copts = ["-O3"]
в файле сборки это должно быть полностью встроено без каких-либо накладных расходов, что дает нам краткость обозначений без каких-либо потерь производительности.
Как уже упоминалось, существуют вспомогательные функции для унарных, двоичных и троичных скалярных инструкций, а также векторные эквиваленты. Они также служат полезными шаблонами для создания собственных помощников для инструкций, которые не вписываются в общий шаблон.
Начальная сборка
Если вы еще не изменили каталог на riscv_semantic_functions
, сделайте это сейчас. Затем соберите проект следующим образом: эта сборка должна пройти успешно.
$ bazel build :riscv32i
...<snip>...
Файлы не создаются, поэтому на самом деле это всего лишь пробный прогон, чтобы убедиться, что все в порядке.
Добавить три операнда инструкции ALU
Теперь давайте добавим семантические функции для некоторых общих инструкций ALU с тремя операндами. Откройте файл rv32i_instructions.cc
и убедитесь, что все недостающие определения добавляются в файл rv32i_instructions.h
по мере продвижения.
Инструкции, которые мы добавим:
-
add
— 32-битное целое число add. -
and
- 32-битное поразрядное и. -
or
- 32-битное побитовое или. -
sll
— 32-битный логический сдвиг влево. -
sltu
— 32-битное беззнаковое множество меньше чем. -
sra
— 32-битный арифметический сдвиг вправо. -
srl
— 32-битный логический сдвиг вправо. -
sub
— вычитание 32-битного целого числа. -
xor
— 32-битное побитовое исключающее ИЛИ.
Если вы читали предыдущие руководства, вы, возможно, помните, что в декодере мы различали инструкции регистр-регистр и инструкции немедленного регистра. Когда дело доходит до семантических функций, нам больше не нужно этого делать. Интерфейсы операндов будут считывать значение операнда из любого операнда, регистрового или непосредственного, при этом семантическая функция полностью не зависит от того, чем на самом деле является основной исходный операнд.
За исключением sra
, все приведенные выше инструкции можно рассматривать как работающие с 32-битными беззнаковыми значениями, поэтому для них мы можем использовать шаблонную функцию BinaryOp
, которую мы рассматривали ранее, только с одним аргументом типа шаблона. Заполните тела функций в rv32i_instructions.cc
соответствующим образом. Обратите внимание, что для величины сдвига используются только младшие 5 бит второго операнда команды сдвига. В противном случае все операции имеют вид src0 op src1
:
-
add
:a + b
-
and
:a & b
-
or
:a | b
-
sll
:a << (b & 0x1f)
-
sltu
:(a < b) ? 1 : 0
-
srl
:a >> (b & 0x1f)
-
sub
:a - b
-
xor
:a ^ b
Для sra
мы будем использовать шаблон BinaryOp
с тремя аргументами. Глядя на шаблон, первый аргумент типа — это тип результата uint32_t
. Второй — это тип исходного операнда 0, в данном случае int32_t
, а последний — тип исходного операнда 1, в данном случае uint32_t
. Это составляет тело семантической функции sra
:
generic::BinaryOp<uint32_t, int32_t, uint32_t>(
instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });
Вносите изменения и стройте. Вы можете проверить свою работу по rv32i_instructions.cc .
Добавьте два операнда в инструкции ALU
Есть только две инструкции ALU с двумя операндами: lui
и auipc
. Первый копирует предварительно сдвинутый исходный операнд непосредственно в место назначения. Последний добавляет адрес инструкции непосредственно перед записью ее в пункт назначения. Адрес инструкции доступен из address()
объекта Instruction
.
Поскольку существует только один исходный операнд, мы не можем использовать BinaryOp
, вместо этого нам нужно использовать UnaryOp
. Поскольку мы можем рассматривать как исходный, так и целевой операнды как uint32_t
мы можем использовать версию шаблона с одним аргументом.
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
Тело семантической функции lui
настолько тривиально, насколько это возможно, просто верните источник. Семантическая функция для auipc
создает небольшую проблему, поскольку вам необходимо получить доступ к методу address()
в экземпляре Instruction
. Ответ заключается в том, чтобы добавить instruction
к лямбда-захвату, сделав ее доступной для использования в теле лямбда-функции. Вместо [](uint32_t a) { ... }
как раньше, лямбду следует писать [instruction](uint32_t a) { ... }
. Теперь instruction
можно использовать в теле лямбды.
Вносите изменения и стройте. Вы можете проверить свою работу по rv32i_instructions.cc .
Добавьте инструкции по изменению потока управления
Инструкции изменения потока управления, которые вам необходимо реализовать, делятся на инструкции условного перехода (более короткие переходы, которые выполняются, если сравнение верно) и инструкции перехода и ссылки, которые используются для реализации вызовов функций (-and-link удаляется путем установки регистра связи в ноль, в результате чего запись не выполняется).
Добавить инструкции условного перехода
Вспомогательной функции для инструкции ветвления нет, поэтому есть два варианта. Напишите семантические функции с нуля или напишите локальную вспомогательную функцию. Поскольку нам нужно реализовать 6 инструкций ветвления, последнее, похоже, стоит затраченных усилий. Прежде чем мы это сделаем, давайте посмотрим на реализацию семантической функции инструкции ветвления с нуля.
void MyConditionalBranchGreaterEqual(Instruction *instruction) {
int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
if (a >= b) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0,m target);
db->Submit();
}
}
Единственное, что различается в инструкциях ветвления, — это условие ветвления и типы данных (подписное и беззнаковое 32-битное целое число) двух исходных операндов. Это означает, что нам нужен параметр шаблона для исходных операндов. Сама вспомогательная функция должна принимать экземпляр Instruction
и вызываемый объект, такой как std::function
, который возвращает bool
в качестве параметров. Вспомогательная функция будет выглядеть так:
template <typename OperandType>
static inline void BranchConditional(
Instruction *instruction,
std::function<bool(OperandType, OperandType)> cond) {
OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
if (cond(a, b)) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, target);
db->Submit();
}
}
Теперь мы можем записать семантическую функцию bge
(ветвь со знаком больше или равно) как:
void RV32IBge(Instruction *instruction) {
BranchConditional<int32_t>(instruction,
[](int32_t a, int32_t b) { return a >= b; });
}
Остальные инструкции ветвления следующие:
- Бек - ветвь равная.
- Бгеу — ветвь больше или равна (без знака).
- Блт – переход меньше (со знаком).
- Блту — ветка меньше (беззнаковая).
- Бне - ветви не равные.
Внесите изменения для реализации этих семантических функций и перестройте. Вы можете проверить свою работу по rv32i_instructions.cc .
Добавьте инструкции по переходу и соединению
Нет смысла писать вспомогательную функцию для инструкций перехода и ссылки, поэтому нам придется написать ее с нуля. Начнем с рассмотрения семантики их инструкций.
Инструкция jal
берет смещение от исходного операнда 0 и добавляет его к текущему компьютеру (адресу инструкции) для вычисления цели перехода. Цель перехода записывается в операнд назначения 0. Адрес возврата — это адрес следующей последовательной инструкции. Его можно вычислить, добавив размер текущей инструкции к ее адресу. Адрес возврата записывается в операнд назначения 1. Не забудьте включить указатель объекта инструкции в захват лямбда-выражения.
Инструкция jalr
принимает базовый регистр в качестве исходного операнда 0 и смещение в качестве исходного операнда 1 и складывает их для вычисления цели перехода. В остальном она идентична инструкции jal
.
На основе этих описаний семантики инструкций напишите две семантические функции и постройте их. Вы можете проверить свою работу по rv32i_instructions.cc .
Добавить инструкции по сохранению памяти
Нам нужно реализовать три инструкции сохранения: сохранение байта ( sb
), сохранение полуслова ( sh
) и сохранение слова ( sw
). Инструкции сохранения отличаются от инструкций, которые мы реализовали до сих пор, тем, что они не записываются в состояние локального процессора. Вместо этого они записывают в системный ресурс — основную память. MPACT-Sim не рассматривает память как операнд инструкции, поэтому доступ к памяти должен выполняться с использованием другой методологии.
Ответ заключается в том, чтобы добавить методы доступа к памяти к объекту MPACT-Sim ArchState
или, точнее, создать новый объект состояния RiscV, производный от ArchState
, куда его можно добавить. Объект ArchState
управляет основными ресурсами, такими как регистры и другие объекты состояния. Он также управляет линиями задержки, используемыми для буферизации буферов данных операндов назначения, пока они не будут записаны обратно в объекты регистра. Большинство инструкций можно реализовать без знания этого класса, но некоторые, например операции с памятью и другие специфические системные инструкции, требуют, чтобы функциональность находилась в этом объекте состояния.
Давайте в качестве примера рассмотрим семантическую функцию для инструкции fence
, которая уже реализована в rv32i_instructions.cc
. Инструкция fence
удерживает ошибку инструкции до тех пор, пока не будут завершены определенные операции с памятью. Он используется для обеспечения порядка в памяти между инструкциями, выполняющимися до инструкции, и теми, которые выполняются после нее.
// Fence.
void RV32IFence(Instruction *instruction) {
uint32_t bits = instruction->Source(0)->AsUint32(0);
int fm = (bits >> 8) & 0xf;
int predecessor = (bits >> 4) & 0xf;
int successor = bits & 0xf;
auto *state = static_cast<RiscVState *>(instruction->state());
state->Fence(instruction, fm, predecessor, successor);
}
Ключевой частью семантической функции инструкции fence
являются последние две строки. Сначала объект состояния извлекается с помощью метода класса Instruction
и downcast<>
в производный класс, специфичный для RiscV. Затем вызывается метод Fence
класса RiscVState
для выполнения операции ограничения.
Инструкции магазина будут работать аналогично. Сначала эффективный адрес доступа к памяти вычисляется из исходных операндов базового и смещенного команд, затем значение, подлежащее сохранению, извлекается из следующего исходного операнда. Затем объект состояния RiscV получается с помощью вызова метода state()
и static_cast<>
, и вызывается соответствующий метод.
Метод StoreMemory
объекта RiscVState
относительно прост, но имеет несколько последствий, о которых нам следует знать:
void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);
Как мы видим, метод принимает три параметра: указатель на саму инструкцию сохранения, адрес хранилища и указатель на экземпляр DataBuffer
, содержащий данные сохранения. Обратите внимание: размер не требуется, сам экземпляр DataBuffer
содержит метод size()
. Однако для инструкции не существует целевого операнда, который можно было бы использовать для выделения экземпляра DataBuffer
соответствующего размера. Вместо этого нам нужно использовать фабрику DataBuffer
, полученную из метода db_factory()
в экземпляре Instruction
. У фабрики есть метод Allocate(int size)
, который возвращает экземпляр DataBuffer
необходимого размера. Вот пример того, как использовать это для выделения экземпляра DataBuffer
для хранилища полуслов (обратите внимание, что auto
— это функция C++, которая определяет тип из правой части присваивания):
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(uint16_t));
Как только у нас появится экземпляр DataBuffer
, мы сможем писать в него как обычно:
db->Set<uint16_t>(0, value);
Затем передайте его в интерфейс хранилища памяти:
state->StoreMemory(instruction, address, db);
Мы еще не совсем закончили. Экземпляр DataBuffer
подсчитывает количество ссылок. Обычно это понимается и обрабатывается методом Submit
, чтобы максимально упростить наиболее частый вариант использования. Однако StoreMemory
записан не так. Он будет IncRef
экземпляра DataBuffer
во время работы с ним, а затем DecRef
когда закончит. Однако если семантическая функция не удаляет собственную ссылку DecRef
, она никогда не будет возвращена. Таким образом, последняя строка должна быть:
db->DecRef();
Функций хранения три, единственное, что отличается, это размер доступа к памяти. Это звучит как прекрасная возможность для еще одной локальной шаблонной вспомогательной функции. Единственное, что отличается в функции хранилища, — это тип значения хранилища, поэтому шаблон должен иметь его в качестве аргумента. Кроме этого, необходимо передать только экземпляр Instruction
:
template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->Set<ValueType>(0, value);
state->StoreMemory(instruction, address, db);
db->DecRef();
}
Идите вперед и закончите семантические функции хранилища и постройте. Вы можете проверить свою работу по rv32i_instructions.cc .
Добавьте инструкции по загрузке памяти
Инструкции по загрузке, которые необходимо реализовать, следующие:
-
lb
— загрузить байт, знак-расширить в слово. -
lbu
- загрузить байт без знака, расширить до слова с нуля. -
lh
- загрузить половину слова, знак-расширить в слово. -
lhu
- загрузить половину слова без знака, расширить до слова с нуля. -
lw
- загрузочное слово.
Инструкции загрузки — самые сложные инструкции, которые нам предстоит смоделировать в этом уроке. Они подобны инструкциям сохранения в том, что им требуется доступ к объекту RiscVState
, но добавляет сложности, поскольку каждая инструкция загрузки разделена на две отдельные семантические функции. Первая аналогична команде сохранения, поскольку она вычисляет эффективный адрес и инициирует доступ к памяти. Второй выполняется после завершения доступа к памяти и записывает данные памяти в операнд назначения регистра.
Начнем с объявления метода LoadMemory
в RiscVState
:
void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
Instruction *child_inst, ReferenceCount *context);
По сравнению с методом StoreMemory
метод LoadMemory
принимает два дополнительных параметра: указатель на экземпляр Instruction
и указатель на context
объект с подсчетом ссылок. Первая — это дочерняя инструкция, реализующая обратную запись регистров (описанную в руководстве по декодеру ISA). Доступ к нему осуществляется с помощью метода child()
в текущем экземпляре Instruction
. Последний является указателем на экземпляр класса, производного от ReferenceCount
, который в данном случае хранит экземпляр DataBuffer
, который будет содержать загруженные данные. Объект контекста доступен через метод context()
в объекте Instruction
(хотя для большинства инструкций для него установлено значение nullptr
).
Объект контекста для загрузки памяти RiscV определяется как следующая структура:
// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
~LoadContext() override {
if (value_db != nullptr) value_db->DecRef();
}
// Override the base class method so that the data buffer can be DecRef'ed
// when the context object is recycled.
void OnRefCountIsZero() override {
if (value_db != nullptr) value_db->DecRef();
value_db = nullptr;
// Call the base class method.
generic::ReferenceCount::OnRefCountIsZero();
}
// Data buffers for the value loaded from memory (byte, half, word, etc.).
DataBuffer *value_db = nullptr;
};
Инструкции загрузки одинаковы, за исключением размера данных (байт, полуслово и слово) и того, является ли загружаемое значение расширенным знаком или нет. Последнее влияет только на обучение ребенка . Давайте создадим шаблонную вспомогательную функцию для основных инструкций загрузки. Она будет очень похожа на инструкцию сохранения, за исключением того, что она не будет обращаться к исходному операнду для получения значения и будет создавать объект контекста.
template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->set_latency(0);
auto *context = new riscv::LoadContext(db);
state->LoadMemory(instruction, address, db, instruction->child(), context);
context->DecRef();
}
Как видите, основное отличие состоит в том, что выделенный экземпляр DataBuffer
передается вызову LoadMemory
в качестве параметра, а также сохраняется в объекте LoadContext
.
Семантические функции дочерних инструкций очень похожи. Сначала LoadContext
получается путем вызова метода Instruction
context()
и статического приведения к LoadContext *
. Во-вторых, значение (в соответствии с типом данных) считывается из экземпляра DataBuffer
загрузки данных. В-третьих, из целевого операнда выделяется новый экземпляр DataBuffer
. Наконец, загруженное значение записывается в новый экземпляр DataBuffer
и Submit
. Опять же, хорошей идеей будет использование шаблонной вспомогательной функции:
template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
auto *context = down_cast<riscv::LoadContext *>(instruction->context());
uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, value);
db->Submit();
}
Идем дальше и реализуем эти последние вспомогательные функции и семантические функции. Обратите внимание на тип данных, который вы используете в шаблоне для каждого вызова вспомогательной функции, и на то, что он соответствует размеру и знаковому/беззнаковому характеру инструкции загрузки.
Вы можете проверить свою работу по rv32i_instructions.cc .
Создайте и запустите финальный симулятор
Теперь, когда мы выполнили всю сложную работу, мы можем построить финальный симулятор. Библиотеки C++ верхнего уровня, которые объединяют всю работу в этих руководствах, расположены в other/
. Нет необходимости слишком внимательно изучать этот код. Мы рассмотрим эту тему в будущем расширенном руководстве.
Измените рабочий каталог на other/
и выполните сборку. Он должен построиться без ошибок.
$ cd ../other
$ bazel build :rv32i_sim
В этом каталоге в файле hello_rv32i.elf
находится простая программа «hello world». Чтобы запустить симулятор для этого файла и увидеть результаты:
$ bazel run :rv32i_sim -- other/hello_rv32i.elf
Вы должны увидеть что-то вроде:
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$
Симулятор также можно запустить в интерактивном режиме с помощью команды bazel run :rv32i_sim -- -i other/hello_rv32i.elf
. Это вызывает простую командную оболочку. Введите help
в командной строке, чтобы просмотреть доступные команды.
$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000 addi ra, 0, 0
[0] > help
quit - exit command shell.
core [N] - direct subsequent commands to core N
(default: 0).
run - run program from current pc until a
breakpoint or exit. Wait until halted.
run free - run program in background from current pc
until breakpoint or exit.
wait - wait for any free run to complete.
step [N] - step [N] instructions (default: 1).
halt - halt a running program.
reg get NAME [FORMAT] - get the value or register NAME.
reg NAME [FORMAT] - get the value of register NAME.
reg set NAME VALUE - set register NAME to VALUE.
reg set NAME SYMBOL - set register NAME to value of SYMBOL.
mem get VALUE [FORMAT] - get memory from location VALUE according to
format. The format is a letter (o, d, u, x,
or X) followed by width (8, 16, 32, 64).
The default format is x32.
mem get SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem set VALUE [FORMAT] VALUE - set memory at location VALUE(1) to VALUE(2)
according to FORMAT. Default format is x32.
mem set SYMBOL [FORMAT] VALUE - set memory at location SYMBOL to VALUE
according to FORMAT. Default format is x32.
break set VALUE - set breakpoint at address VALUE.
break set SYMBOL - set breakpoint at value of SYMBOL.
break VALUE - set breakpoint at address VALUE.
break SYMBOL - set breakpoint at value of SYMBOL.
break clear VALUE - clear breakpoint at address VALUE.
break clear SYMBOL - clear breakpoint at value of SYMBOL.
break clear all - remove all breakpoints.
help - display this message.
_start:
80000000 addi ra, 0, 0
[0] >
На этом урок завершается. Мы надеемся, что это было полезно.