Os objetivos deste tutorial são:
- Saiba como as funções semânticas são usadas para implementar a semântica de instruções.
- Saiba como as funções semânticas estão relacionadas à descrição do decodificador da ISA.
- Escreva funções semânticas de instrução para as instruções do RiscV RV32I.
- Teste o simulador final executando um pequeno "Hello World" executável.
Visão geral das funções semânticas
Uma função semântica em MPACT-Sim é uma função que implementa a operação de uma instrução para que seus efeitos colaterais sejam visíveis no estado simulado, da mesma forma que os efeitos colaterais da instrução ficam visíveis quando executados ao hardware. Representação interna do simulador de cada instrução decodificada. contém uma função chamável que é usada para chamar a função semântica desse instrução.
Uma função semântica tem a assinatura void(Instruction *)
, ou seja, uma
que usa um ponteiro para uma instância da classe Instruction
e
retorna void
.
A classe Instruction
é definida em
instruction.h
Para escrever funções semânticas, estamos particularmente interessados
os vetores de interface do operando de origem e destino acessados usando o
Chamadas de método Source(int i)
e Destination(int i)
As interfaces de operando de origem e destino são mostradas abaixo:
// 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;
};
A maneira básica de escrever uma função semântica para um operando normal 3.
como uma instrução add
de 32 bits, é a seguinte:
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();
}
Vamos detalhar as partes dessa função. As duas primeiras linhas
o corpo da função lê os operandos de origem 0 e 1. A chamada AsUint32(0)
interpreta os dados como uma matriz uint32_t
e busca o 0
. Isso acontece independentemente de o registro ou o valor subjacente
matriz com valor ou não. O tamanho (em elementos) do operando de origem pode ser
extraído do método de operando de origem shape()
, que retorna um vetor
que contém o número de elementos em cada dimensão. Esse método retorna {1}
.
para um escalar, {16}
para um vetor de 16 elementos e {4, 4}
para uma matriz 4x4.
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
Em seguida, um uint32_t
temporário chamado c
recebe o valor a + b
.
A próxima linha pode exigir um pouco mais de explicação:
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
Um DataBuffer é um objeto contado por referência que é usado para armazenar valores em
um estado simulado, como registros. Ele é relativamente não digitado, mas tem um
com base no objeto do qual está alocado. Nesse caso, esse tamanho é
sizeof(uint32_t)
: Essa instrução aloca um novo buffer de dados dimensionado para o
destino que é o alvo deste operando de destino - neste caso, um
Registro de números inteiros de 32 bits. O DataBuffer também é inicializado com a
latência de arquitetura para a instrução. Isso é especificado durante a instrução
decodificação.
A próxima linha trata a instância do buffer de dados como uma matriz de uint32_t
e
grava o valor armazenado em c
no elemento 0.
db->Set<uint32_t>(0, c);
Por fim, a última instrução envia o buffer de dados ao simulador para uso. como o novo valor do estado da máquina de destino (neste caso, um registro) após o latência da instrução definida quando ela foi decodificada e a vetor de operando de destino preenchido.
Embora essa função seja razoavelmente breve, ela tem um pouco de código boilerplate código que se torna repetitivo na hora de implementar a instrução após a instrução. Além disso, pode ocultar a semântica real da instrução. Para para simplificar ainda mais a escrita das funções semânticas para a maioria das instruções, há uma série de funções helper com modelo definidas em instruction_helpers.h. Esses auxiliares escondem o código boilerplate para instruções com um, dois ou três operandos de origem e um único operando de destino. Vamos analisar um dois funções auxiliares do operando:
// 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);
}
Você perceberá que, em vez de usar uma instrução como:
uint32_t a = inst->Source(0)->AsUint32(0);
A função auxiliar usa:
generic::GetInstructionSource<Argument>(instruction, 0);
O GetInstructionSource
é uma família de funções auxiliares com modelo que
são usados para fornecer métodos de acesso de modelo à fonte da instrução
operandos. Sem eles, cada função auxiliar de instrução teria
especializada para cada tipo para acessar o operando de origem com o
função As<int type>()
. Para conferir as definições desses modelos
funções na
instruction.h.
Como você pode ver, há três implementações, dependendo se a origem
operando forem os mesmos que o destino, independente de o destino ser
diferentes das origens ou se são todas diferentes. Cada versão do
a função toma um ponteiro para a instância de instrução, bem como uma string
(o que inclui funções lambda). Isso significa que agora podemos reescrever a add
função semântica acima da seguinte maneira:
void MyAddFunction(Instruction *inst) {
generic::BinaryOp<uint32_t>(inst,
[](uint32_t a, uint32_t b) { return a + b; });
}
Quando compilado com bazel build -c opt
e copts = ["-O3"]
no build
o arquivo deve estar inline, sem overhead, oferecendo uma
ser sucinto, sem nenhuma penalidade de desempenho.
Como mencionado, existem funções auxiliares para funções unárias, binárias e escalares ternárias bem como equivalentes de vetores. Eles também servem como modelos úteis para criar seus próprios ajudantes para instruções que não se encaixam no molde geral.
Build inicial
Se você não mudou o diretório para riscv_semantic_functions
, faça isso
agora. Em seguida, crie o projeto da seguinte maneira: ela deverá ser bem-sucedida.
$ bazel build :riscv32i
...<snip>...
Nenhum arquivo é gerado, então esta é apenas uma simulação para para ter certeza de que tudo está em ordem.
Adicionar instruções de ALU de três operandos
Agora vamos adicionar as funções semânticas para uma ALU genérica com três operandos.
instruções. Abra o arquivo rv32i_instructions.cc
e verifique se alguma
definições ausentes são adicionadas ao arquivo rv32i_instructions.h
à medida que avançamos.
As instruções que adicionaremos são:
add
: adição de inteiro de 32 bits.and
: 32 bits eor
: 32 bits ousll
: deslocamento lógico de 32 bits para a esquerda.sltu
: conjunto não assinado de 32 bits menor que.sra
: deslocamento aritmético para a direita de 32 bits.srl
: deslocamento lógico para a direita de 32 bits.sub
: subtração de números inteiros de 32 bits.xor
: xor bit a bit de 32 bits.
Se você já fez os tutoriais anteriores, deve se lembrar que nós diferenciamos entre as instruções de registro-registro e as instruções imediatas de registro em o decodificador. Quando se trata de funções semânticas, não precisamos mais fazer isso. As interfaces de operando lerão o valor do operando qualquer é registrado ou imediato, com a função semântica completamente agnóstica para o que é o operando de origem subjacente.
Com exceção de sra
, todas as instruções acima podem ser tratadas como se operando em
Valores não assinados de 32 bits. Para eles, podemos usar a função de modelo BinaryOp
.
que analisamos anteriormente com apenas um argumento de tipo de modelo. Preencha o
corpo da função em rv32i_instructions.cc
corretamente. Observe que apenas os 5
bits do segundo operando para as instruções de deslocamento são usados para a
de dados. Caso contrário, todas as operações estarão no formato 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
Para sra
, vamos usar o modelo BinaryOp
com três argumentos. Analisando
o primeiro argumento de tipo será o tipo de resultado uint32_t
. O segundo é
o tipo de operando de origem 0, neste caso, int32_t
, e o último é o tipo;
do operando de origem 1, neste caso, uint32_t
. Isso torna o corpo do sra
função semântica:
generic::BinaryOp<uint32_t, int32_t, uint32_t>(
instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });
Vá em frente, faça as alterações e crie. Você pode comparar seu trabalho rv32i_instructions.cc.
Adicionar instruções de ALU de dois operandos
Há apenas duas instruções de ALU com dois operandos: lui
e auipc
. O primeiro
copia o operando de origem pré-deslocado diretamente para o destino. A última opção
adiciona o endereço de instrução ao imediato antes de escrevê-lo no
destino. O endereço da instrução pode ser acessado pelo método address()
.
do objeto Instruction
.
Como há apenas um operando de origem, não podemos usar BinaryOp
.
precisamos usar UnaryOp
. Como podemos tratar a origem e o
operandos de destino como uint32_t
, podemos usar o modelo de argumento único
para a versão anterior.
// 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);
}
O corpo da função semântica de lui
é o mais trivial possível,
apenas retorna a origem. A função semântica de auipc
introduz uma
problema, já que você precisa acessar o método address()
no objeto Instruction
instância. A resposta é adicionar instruction
à captura de lambda, tornando
disponível para uso no corpo da função lambda. Em vez de usar [](uint32_t a) { ...
}
como antes, a lambda precisa ser escrita como [instruction](uint32_t a) { ... }
.
Agora, instruction
pode ser usado no corpo da lambda.
Vá em frente, faça as alterações e crie. Você pode comparar seu trabalho rv32i_instructions.cc.
Adicionar instruções de mudança do fluxo de controle
As instruções de mudança do fluxo de controle que você precisa implementar estão divididas em instruções de ramificação condicional (ramificações mais curtas que são executadas se uma comparação é verdadeira) e instruções de link direto, usadas para implementar chamadas de função (o -and-link é removido ao definir o link registro em zero, o que torna essas gravações em ambiente autônomo).
Adicionar instruções de ramificação condicional
Não há uma função auxiliar para instruções de ramificação, então há duas opções. Escreva as funções semânticas do zero ou uma função auxiliar local. Como precisamos implementar seis instruções de ramificação, a última parece valer a pena esforço de Antes de fazer isso, vamos conferir a implementação de uma ramificação e a função semântica de instruções do zero.
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();
}
}
A única coisa que varia entre as instruções da ramificação
e os tipos de dados, assinado ou não assinado de 32 bits int, dos dois
operandos de origem. Isso significa que precisamos de um parâmetro de modelo para o
operandos de origem. A função auxiliar precisa usar Instruction
.
instância e um objeto chamável, como std::function
, que retorna bool
como parâmetros. A função auxiliar ficaria assim:
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();
}
}
Agora podemos escrever a função semântica bge
(ramificação assinada maior ou igual)
como:
void RV32IBge(Instruction *instruction) {
BranchConditional<int32_t>(instruction,
[](int32_t a, int32_t b) { return a >= b; });
}
As instruções de ramificação restantes são as seguintes:
- Beq - ramificação igual.
- Bgeu - ramificação maior ou igual (não assinada).
- Blt - ramificação menor que (assinada).
- Bltu - ramificação menor que (não assinada).
- Bne - ramificação diferente.
Faça as alterações para implementar essas funções semânticas. reconstruir. Você pode comparar seu trabalho rv32i_instructions.cc.
Adicionar instruções de link direto
Não faz sentido escrever uma função auxiliar para o jump and link então precisamos escrevê-las do zero. Vamos começar analisando a semântica de instruções deles.
A instrução jal
usa um deslocamento do operando de origem 0 e o adiciona ao
pc atual (endereço de instrução) para calcular o alvo de salto. O alvo de salto
é gravado no operando de destino 0. O endereço de devolução é o endereço
próxima instrução sequencial. Ele pode ser calculado com a adição do
tamanho da instrução ao seu endereço. O endereço de devolução é gravado
operando de destino 1. Lembre-se de incluir o ponteiro do objeto de instrução
a captura lambda.
A instrução jalr
usa um registro base como operando de origem 0 e um deslocamento
como operando de origem 1 e os adiciona juntos para calcular o destino de salto.
Caso contrário, ela é idêntica à instrução jal
.
Com base nessas descrições da semântica de instrução, escreva as duas semânticas e criação. Você pode comparar seu trabalho rv32i_instructions.cc.
Adicionar instruções de armazenamento de memória
Precisamos implementar três instruções de armazenamento: byte de armazenamento
(sb
), armazenar meia palavra (sh
) e palavra da loja (sw
). Instruções da loja
diferentes das instruções que implementamos até agora
gravação no estado do processador local. Em vez disso, eles gravam em um recurso do sistema,
na memória principal. O MPACT-Sim não trata a memória como um operando de instrução.
Portanto, o acesso à memória precisa ser feito usando outra metodologia.
A resposta é adicionar métodos de acesso à memória ao objeto ArchState
MPACT-Sim,
ou, de forma mais adequada, crie um novo objeto de estado do RiscV derivado de ArchState
onde ela pode ser adicionada. O objeto ArchState
gerencia os recursos principais, como
e outros objetos de estado. Ele também gerencia as linhas de atraso usadas para
armazenar em buffer os buffers de dados do operando de destino até que eles possam ser gravados de volta no
os objetos de registro. A maioria das instruções pode ser implementada sem conhecimento de
dessa classe, mas algumas, como operações de memória e outras funções
precisam de funcionalidade para residir nesse objeto de estado.
Vamos dar uma olhada na função semântica da instrução fence
, que é
já implementado em rv32i_instructions.cc
como exemplo. O fence
a instrução mantém problemas de instrução até que certas operações de memória
concluído. É usado para garantir a ordem da memória entre as instruções
que são executadas antes e depois da instrução.
// 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);
}
A parte mais importante da função semântica da instrução fence
são as duas últimas.
linhas Primeiro, o objeto de estado é buscado usando um método na Instruction
.
e downcast<>
para a classe derivada específica do RiscV. Depois, o Fence
da classe RiscVState
é chamado para realizar a operação de limite.
As instruções para lojas funcionarão da mesma forma. Primeiro, o endereço efetivo da
o acesso à memória é calculado a partir dos operandos de origem de origem da instrução base e de deslocamento.
o valor a ser armazenado é buscado no próximo operando de origem. Em seguida,
O objeto de estado RiscV é recebido pela chamada de método state()
e
static_cast<>
, e o método apropriado é chamado.
O método StoreMemory
do objeto RiscVState
é relativamente simples, mas tem uma
algumas implicações das quais precisamos estar cientes:
void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);
Como podemos ver, o método usa três parâmetros: o ponteiro para o repositório
a instrução em si, o endereço da loja e um ponteiro para uma DataBuffer
que contém os dados do armazenamento. Observe que nenhum tamanho é necessário,
A instância DataBuffer
contém um método size()
. No entanto, não há
operando de destino acessível para a instrução que pode ser usado para
alocar uma instância DataBuffer
do tamanho apropriado. Em vez disso, precisamos
use uma fábrica DataBuffer
extraída do método db_factory()
na
a instância Instruction
. A fábrica tem um método Allocate(int size)
.
que retorna uma instância DataBuffer
do tamanho necessário. Aqui está um exemplo
de como usar isso para alocar uma instância de DataBuffer
para um repositório de meia palavra
Observe que auto
é um recurso em C++ que deduz o tipo da
da tarefa):
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(uint16_t));
Assim que tivermos a instância DataBuffer
, poderemos gravar nela normalmente:
db->Set<uint16_t>(0, value);
Em seguida, transmita-o para a interface de armazenamento de memória:
state->StoreMemory(instruction, address, db);
Ainda não terminamos. A instância DataBuffer
é contada como referência. Isso
normalmente é entendido e processado pelo método Submit
, de modo a manter o
caso de uso mais frequente o mais simples possível. No entanto, o StoreMemory
não é
escrita dessa maneira. Ele vai IncRef
a DataBuffer
instância enquanto opera
e DecRef
quando terminar. No entanto, se a função semântica não
DecRef
é a própria referência, ele nunca será recuperado. Assim, a última linha tem
para ser:
db->DecRef();
Há três funções de armazenamento, e a única diferença é o tamanho
o acesso à memória. Esta parece uma ótima oportunidade para outra empresa local
função auxiliar com modelo. A única coisa diferente na função da loja é
o tipo de valor da loja, portanto, o modelo precisa ter isso como argumento.
Fora isso, somente a instância Instruction
precisa ser transmitida:
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();
}
Conclua as funções semânticas de armazenamento e o build. Você pode conferir trabalhar contra rv32i_instructions.cc.
Adicionar instruções de carregamento de memória
As instruções de carregamento que precisam ser implementadas são:
lb
: byte de carregamento, extensão de sinal em uma palavra.lbu
: byte de carregamento sem assinatura, extensão zero em uma palavra.lh
- carregar meia palavra, extensão de sinal em uma palavra.lhu
: carrega meia palavra sem assinatura, extensão zero em uma palavra.lw
: carregar palavra.
As instruções de carregamento são as instruções mais complexas que temos para modelar
neste tutorial. Eles são semelhantes às instruções de armazenamento, porque precisam
acessar o objeto RiscVState
, mas aumenta a complexidade de cada carga
é dividida em duas funções semânticas separadas. A primeira é
semelhante à instrução store, que calcula o endereço efetivo
e inicia o acesso à memória. O segundo é executado quando a memória
acesso é concluído e grava os dados da memória no registro de destino
operando.
Vamos começar analisando a declaração do método LoadMemory
em RiscVState
:
void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
Instruction *child_inst, ReferenceCount *context);
Em comparação com o método StoreMemory
, o LoadMemory
usa dois
parâmetros: um ponteiro para uma instância Instruction
e outro para uma
o objeto context
da referência foi contabilizado. O primeiro é a instrução child que
implementa o write-back de registro (descrito no tutorial do decodificador da ISA). Ela
é acessado usando o método child()
na instância atual do Instruction
.
O último é um indicador para uma instância de uma classe derivada da
ReferenceCount
que, nesse caso, armazena uma instância de DataBuffer
que vai
que contêm os dados carregados. O objeto de contexto está disponível por meio do
Método context()
no objeto Instruction
. No entanto, para a maioria das instruções,
definido como nullptr
).
O objeto de contexto para carregamentos de memória do RiscV é definido como a seguinte estrutura:
// 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;
};
As instruções de carregamento são todas as mesmas, exceto pelo tamanho dos dados (byte, meia palavra e palavra) e se o valor carregado é estendido por sinal ou não. A O último fator é considerado na instrução filho. Vamos criar um modelo para as instruções de carregamento principais. Será muito semelhante à armazenar instrução, mas não acessará um operando de origem para obter um valor, e criará um objeto de contexto.
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();
}
A principal diferença é que a instância DataBuffer
alocada
são transmitidos para a chamada LoadMemory
como um parâmetro e armazenados no
objeto LoadContext
.
As funções semânticas da instrução child são muito semelhantes. Primeiro, os
LoadContext
é recebido chamando o método context()
do Instruction
.
transmitido estático para o LoadContext *
. Segundo, o valor (de acordo com o
type) é lida na instância DataBuffer
de dados de carregamento. Terceiro, um novo
A instância DataBuffer
é alocada do operando de destino. Por fim, o
o valor carregado é gravado na nova instância de DataBuffer
e Submit
.
Novamente, uma função auxiliar com modelo é uma boa ideia:
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();
}
Implemente as últimas funções auxiliares e semânticas. Pagar atenção ao tipo de dados usado no modelo para cada função auxiliar e que ela corresponde ao tamanho e à natureza assinada/não assinada da carga instrução.
Você pode comparar seu trabalho rv32i_instructions.cc.
Criar e executar o simulador final
Agora que já fizemos todos os últimos ajustes, podemos criar o simulador final. A
bibliotecas C++ de nível superior que unem todo o trabalho nestes tutoriais são
localizado em other/
. Não é necessário analisar muito esse código. Qa
vão acessar esse tópico em um tutorial avançado futuro.
Mude seu diretório de trabalho para other/
e crie. Ele deve ser criado sem
erros.
$ cd ../other
$ bazel build :rv32i_sim
Nesse diretório, há um código "Hello World" programa no arquivo
hello_rv32i.elf
: Para executar o simulador nesse arquivo e conferir os resultados:
$ bazel run :rv32i_sim -- other/hello_rv32i.elf
Você verá algo ao longo das linhas de:
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
$
O simulador também pode ser executado em modo interativo usando o comando bazel
run :rv32i_sim -- -i other/hello_rv32i.elf
. Isso traz à tona
shell de comando. Digite help
no prompt para conferir os comandos disponíveis.
$ 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] >
Isso conclui este tutorial. Esperamos que essas informações tenham sido úteis.