Os objetivos deste tutorial são:
- Saiba como a ISA gerada e os decodificadores binários se encaixam.
- Escrever o código em C++ necessário para criar um decodificador de instruções completo para RiscV O RV32I que combina a ISA e os decodificadores binários.
Entender o decodificador de instruções
O decodificador de instruções é responsável por, dado um endereço de instrução, ler
a palavra de instrução da memória e retorna uma instância totalmente inicializada do
Instruction
que representa essa instrução.
O decodificador de nível superior implementa o generic::DecoderInterface
mostrado abaixo:
// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
public:
// Return a decoded instruction for the given address. If there are errors
// in the instruciton decoding, the decoder should still produce an
// instruction that can be executed, but its semantic action function should
// set an error condition in the simulation when executed.
virtual Instruction *DecodeInstruction(uint64_t address) = 0;
virtual ~DecoderInterface() = default;
};
Como você pode notar, só é preciso implementar um método: cpp
virtual Instruction *DecodeInstruction(uint64_t address);
Agora, vamos conferir o que é fornecido e o que é necessário para o código gerado.
Primeiro, considere a classe de nível superior RiscV32IInstructionSet
no arquivo
riscv32i_decoder.h
, que foi gerado no fim do tutorial da série
decodificador ISA. Para acessar o conteúdo novamente, navegue até o diretório de soluções do
esse tutorial e recriar tudo.
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
Agora mude seu diretório de volta para a raiz do repositório, depois vamos conferir
nas origens geradas. Para isso, mude o diretório para
bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(supondo que você esteja em uma arquitetura x86)
host: para outros hosts, o k8-fastbuild será outra string).
$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
Você verá uma lista de quatro arquivos de origem que contêm o código C++ gerado:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Abra o primeiro arquivo riscv32i_decoder.h
. Há três classes que
precisa dar uma olhada no seguinte:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
Observe a nomenclatura das classes. Todas as classes são nomeadas com base no
Versão Pascal Case do nome fornecido no "isa" no arquivo:
isa RiscV32I { ... }
Vamos começar com a classe RiscVIInstructionSet
. Ela é mostrada abaixo:
class RiscV32IInstructionSet {
public:
RiscV32IInstructionSet(ArchState *arch_state,
RiscV32IInstructionSetFactory *factory);
Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);
private:
std::unique_ptr<Riscv32Slot> riscv32_decoder_;
ArchState *arch_state_;
};
Não há métodos virtuais nesta classe, portanto, esta é uma classe autônoma, mas
perceba duas coisas. Primeiro, o construtor leva um ponteiro para uma instância do
RiscV32IInstructionSetFactory
. Essa é uma classe que o modelo
decodificador usa para criar uma instância da classe RiscV32Slot
, que é usada para
decodificar todas as instruções definidas para o slot RiscV32
conforme definido no
arquivo riscv32i.isa
. Segundo, o método Decode
usa um parâmetro adicional
do tipo ponteiro para RiscV32IEncodingBase
, essa é uma classe que fornecerá o
interface entre o decodificador de isa gerado no primeiro tutorial e o
gerado no segundo laboratório.
A RiscV32IInstructionSetFactory
é uma classe abstrata da qual
precisamos derivar nossa própria implementação para o decodificador completo. Na maioria dos casos,
é trivial: basta fornecer um método para chamar o construtor para cada
classe de slot definida no arquivo .isa
. No nosso caso, é muito simples
é apenas uma única classe assim: Riscv32Slot
(PascalCase do nome riscv32
)
concatenado com Slot
). O método não é gerado para você, pois há
alguns casos de uso avançados em que pode haver utilidade na derivação de uma subclasse
do slot e chamando seu construtor.
Vamos conferir a última classe RiscV32IEncodingBase
mais adiante
porque esse é o assunto de outro exercício.
Definir decodificador de instrução de nível superior
Definir a classe de fábrica
Se você reconstruiu o projeto para o primeiro tutorial, volte para
diretório riscv_full_decoder
.
Abra o arquivo riscv32_decoder.h
. Todos os arquivos de inclusão necessários têm
já foi adicionado e os namespaces foram configurados.
Após o comentário marcado como //Exercise 1 - step 1
, defina a classe
RiscV32IsaFactory
herdando de RiscV32IInstructionSetFactory
.
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
Em seguida, defina a substituição para CreateRiscv32Slot
. Como não usamos nenhum
classes derivadas de Riscv32Slot
, basta alocar uma nova instância usando
std::make_unique
.
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
Se você precisar de ajuda (ou quiser verificar seu trabalho), a resposta completa é aqui.
Definir a classe do decodificador
Construtores, destruidores e declarações de método
Em seguida, é hora de definir a classe de decodificador. No mesmo arquivo acima, vá para o
declaração de RiscV32Decoder
. Expanda a declaração em uma definição de classe
em que RiscV32Decoder
herda de generic::DecoderInterface
.
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
Em seguida, antes de criarmos o construtor, vamos dar uma olhada no código
gerada em nosso segundo tutorial sobre o decodificador binário. Além de todos os
Extract
, existe a função DecodeRiscVInst32
:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
Essa função pega a palavra de instrução que precisa ser decodificada e retorna
o código de operação que corresponde à instrução. Por outro lado,
Classe DecodeInterface
que RiscV32Decoder
implementa apenas transmissões em um
endereço IP. Assim, a classe RiscV32Decoder
precisa ser capaz de acessar a memória para
leia a palavra de instrução a ser transmitida para DecodeRiscVInst32()
. Neste projeto
acesso à memória é por meio de uma interface de memória simples definida em
.../mpact/sim/util/memory
, que chamamos corretamente de util::MemoryInterface
, pode ser vista abaixo:
// Load data from address into the DataBuffer, then schedule the Instruction
// inst (if not nullptr) to be executed (using the function delay line) with
// context. The size of the data access is based on size of the data buffer.
virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
ReferenceCount *context) = 0;
Além disso, precisamos transmitir uma instância da classe state
para o
construtores das outras classes decodificadores. A classe de estado apropriada é
A classe riscv::RiscVState
, derivada de generic::ArchState
, com a inclusão
do RiscV. Isso significa que precisamos declarar o construtor para que ele
pode pegar um ponteiro para state
e memory
:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
Exclua o construtor padrão e substitua o destrutor:
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
Em seguida, declare o método DecodeInstruction
que precisamos substituir
generic::DecoderInterface
.
generic::Instruction *DecodeInstruction(uint64_t address) override;
Se você precisar de ajuda (ou quiser verificar seu trabalho), a resposta completa é aqui.
Definições dos membros dos dados
A classe RiscV32Decoder
vai precisar de membros de dados particulares para armazenar o
parâmetros do construtor e um ponteiro para a classe de fábrica.
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
Ele também precisa de um ponteiro para a classe de codificação derivada da
RiscV32IEncodingBase
, vamos chamá-lo de RiscV32IEncoding
(implementaremos
isso no exercício 2). Além disso, ele precisa de um ponteiro para uma instância
RiscV32IInstructionSet
. Portanto, adicione:
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
Por fim, precisamos definir um membro de dados para uso com a interface de memória:
generic::DataBuffer *inst_db_;
Se você precisar de ajuda (ou quiser verificar seu trabalho), a resposta completa é aqui.
Definir os métodos da classe Decoder
Em seguida, é hora de implementar o construtor, o destrutor e a
DecodeInstruction
. Abra o arquivo riscv32_decoder.cc
. O vazio
métodos já estão no arquivo, bem como declarações de namespace e algumas
de declarações using
.
Definição de construtor
O construtor só precisa inicializar os membros de dados. Primeiro, inicialize o
state_
e memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
Em seguida, aloque instâncias de cada uma das classes relacionadas ao decodificador, passando o parâmetros apropriados.
// Allocate the isa factory class, the top level isa decoder instance, and
// the encoding parser.
riscv_isa_factory_ = new RiscV32IsaFactory();
riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
riscv_encoding_ = new RiscV32IEncoding(state);
Por fim, aloque a instância DataBuffer
. Ele é alocado usando um sistema
acessíveis pelo membro state_
. Alocamos um buffer de dados dimensionado para armazenar
uma única uint32_t
, que é o tamanho da palavra de instrução.
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
Definição de Destructor
O destrutor é simples: basta liberar os objetos alocados no construtor,
mas com uma reviravolta. A instância do buffer de dados é contada como referência, então em vez
Se desativarmos a chamada de delete
nesse ponteiro, DecRef()
o objeto:
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
Definição de método
No nosso caso, a implementação desse método é bastante simples. Vamos assumir se o endereço está corretamente alinhado e se não há verificações de erros adicionais obrigatórios.
Primeiro, a palavra de instrução deve ser buscada na memória usando a função
interface de usuário e a instância DataBuffer
.
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
Em seguida, chamamos a instância RiscVIEncoding
para analisar a palavra de instrução.
que precisa ser feito antes de chamar o próprio decodificador da ISA. Lembre-se de que a ISA
o decodificador chama a instância RiscVIEncoding
diretamente para receber o código de operação
e operandos especificados pela palavra de instrução. Ainda não implementamos isso
classe, mas vamos usar a void ParseInstruction(uint32_t)
como esse método.
riscv_encoding_->ParseInstruction(iword);
Finalmente, chamamos o decodificador ISA, passando o endereço e a classe Encoding.
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
Se você precisar de ajuda (ou quiser verificar seu trabalho), a resposta completa é aqui.
A classe de codificação
A classe de codificação implementa uma interface que é usada pela classe de para obter o código de operação da instrução, os operandos de origem e destino e operandos de recurso. Todos esses objetos dependem das informações do binário decodificador de formato, como o código de operação, os valores de campos específicos na palavra de instrução, etc. Ele é separado da classe de decodificador para mantê-lo qualquer codificação e ativar o suporte para vários esquemas de codificação diferentes no futuro.
RiscV32IEncodingBase
é uma classe abstrata. O conjunto de métodos que temos para
implementar na classe derivada é mostrado abaixo.
class RiscV32IEncodingBase {
public:
virtual ~RiscV32IEncodingBase() = default;
virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;
virtual ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end) = 0;
virtual ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end) = 0;
virtual PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op) = 0;
virtual SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no) = 0;
virtual DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency) = 0;
virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no) = 0;
};
À primeira vista, parece um pouco complicado, especialmente com o número de mas para uma arquitetura simples como o RiscV, nós ignoramos a maioria dos parâmetros, já que os valores deles estarão implícitos.
Vamos examinar cada um dos métodos por vez.
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
O método GetOpcode
retorna o membro OpcodeEnum
do
a instrução, identificando o código de operação da instrução. A classe OpcodeEnum
é
definido no arquivo decodificador de isa riscv32i_enums.h
gerado. O método usa
dois parâmetros, e ambos podem ser ignorados para nossos objetivos. A primeira
Esse é o tipo de slot (uma classe de enumeração também definida em riscv32i_enums.h
),
que, como o RiscV tem apenas um slot, tem apenas um valor possível:
SlotEnum::kRiscv32
. O segundo é o número da instância do slot (caso
há várias instâncias do slot, o que pode ocorrer em algumas VLIWs
arquiteturas de entrada e saída).
ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end)
ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end);
Os dois métodos a seguir são usados para modelar recursos de hardware no processador
para melhorar a precisão do ciclo. Para nossos exercícios de tutorial, não usaremos
para que, na implementação, eles sejam fragmentados, retornando nullptr
.
PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op);
SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no);
DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency);
Esses três métodos retornam ponteiros para objetos operando que são usados dentro
as funções semânticas da instrução para acessar o valor de qualquer instrução
operando de predicado, cada um dos operandos de origem da instrução, e escrever um novo
valores aos operandos de destino da instrução. Como o RiscV não usa
predicados de instrução, esse método só precisa retornar nullptr
.
O padrão dos parâmetros é semelhante entre essas funções. Primeiro, assim como
GetOpcode
o slot e a entrada são transmitidos. Em seguida, o código de operação
instrução para a qual o operando deve ser criado. Ela só é usada se o
códigos de operação diferentes precisam retornar diferentes objetos de operando para o mesmo operando
tipos, o que não é o caso deste simulador RiscV.
Em seguida, temos a entrada de enumeração de operandos, "Predicate", "Source" e "Destination",
identifica o operando que precisa ser criado. Elas vêm dos três
OpEnums no riscv32i_enums.h
, conforme mostrado abaixo:
enum class PredOpEnum {
kNone = 0,
kPastMaxValue = 1,
};
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
Se você analisar
riscv32.isa
você notará que eles correspondem aos conjuntos de origem e destino
de operandos usados na declaração de cada instrução. Ao usar diferentes
nomes de operandos para operandos que representam diferentes bitfields e operandos
tipos, isso torna a programação da classe de codificação mais fácil, pois o membro da enumeração
determina o tipo de operando exato a ser retornado e não é necessário
considere os valores dos parâmetros slot, input ou opcode.
Por fim, para operandos de origem e destino, a posição ordinal do operando é transmitido (de novo, podemos ignorar isso) e para o operador operando, a latência (em ciclos) decorrido entre o momento em que a instrução é emitido, e o resultado de destino fica disponível para as instruções subsequentes. No nosso simulador, essa latência será 0, o que significa que a instrução grava o resultado imediatamente para o registrador.
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
A função final é usada para conferir a latência de um destino específico
operando se ele tiver sido especificado como *
no arquivo .isa
. Isso é incomum,
e não é usado para este simulador RiscV, portanto, a implementação dessa função
retornará apenas 0.
Definir a classe de codificação
Arquivo de cabeçalho (.h)
Métodos
Abra o arquivo riscv32i_encoding.h
. Todos os arquivos de inclusão necessários têm
já foi adicionado e os namespaces foram configurados. Toda adição de código é
terminou de seguir o comentário // Exercise 2.
Vamos começar definindo uma classe RiscV32IEncoding
herdada da
interface gerada.
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
Em seguida, o construtor deve levar um ponteiro para a instância do estado, neste caso
Um ponteiro para riscv::RiscVState
. O destrutor padrão precisa ser usado.
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
Antes de adicionarmos todos os métodos de interface, vamos adicionar o método chamado pelo
RiscV32Decoder
para analisar a instrução:
void ParseInstruction(uint32_t inst_word);
A seguir, vamos adicionar os métodos que têm substituições triviais, ao mesmo tempo em que descartamos nomes dos parâmetros que não são usados:
// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
SimpleResourceVector &,
int) override {
return nullptr;
}
ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
ComplexResourceEnum ,
int, int) override {
return nullptr;
}
PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
PredOpEnum) override {
return nullptr;
}
int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }
Por fim, adicione as outras substituições de método da interface pública, mas com as implementações deferidas para o arquivo .cc.
OpcodeEnum GetOpcode(SlotEnum, int) override;
SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
SourceOpEnum source_op, int) override;
DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
DestOpEnum dest_op, int,
int latency) override;
Para simplificar a implementação de cada um dos métodos getter do operando
criaremos duas matrizes de chamáveis (objetos de função) indexadas pelo
valor numérico dos membros SourceOpEnum
e DestOpEnum
, respectivamente.
Dessa forma, os corpos desses métodos são reduzidos a chamar o
objeto de função para o valor de tipo enumerado que é transmitido e retorna seu retorno
.
Para organizar a inicialização dessas duas matrizes, definimos duas matrizes métodos que serão chamados no construtor da seguinte maneira:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
Participantes de dados
Os membros de dados necessários são os seguintes:
state_
para manter o valorriscv::RiscVState *
.inst_word_
do tipouint32_t
, que contém o valor do objeto atual ou palavra instrutiva.opcode_
para armazenar o código de operação da instrução atual que é atualizada pelo o métodoParseInstruction
. Tem o tipoOpcodeEnum
.source_op_getters_
: uma matriz para armazenar as chamáveis usadas para receber a origem operando. O tipo dos elementos da matriz éabsl::AnyInvocable<SourceOperandInterface *>()>
:dest_op_getters_
: uma matriz para armazenar as chamáveis usadas para receber objetos operando no destino. O tipo dos elementos da matriz éabsl::AnyInvocable<DestinationOperandInterface *>()>
:xreg_alias
: uma matriz de nomes ABI de registro de números inteiros do RiscV, por exemplo, "zero" e "ra" em vez de "x0" e "x1".
riscv::RiscVState *state_;
uint32_t inst_word_;
OpcodeEnum opcode_;
absl::AnyInvocable<SourceOperandInterface *()>
source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
absl::AnyInvocable<DestinationOperandInterface *(int)>
dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];
const std::string xreg_alias_[32] = {
"zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0",
"a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5",
"s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};
Se você precisar de ajuda (ou quiser verificar seu trabalho), a resposta completa é aqui.
Arquivo de origem (.cc).
Abra o arquivo riscv32i_encoding.cc
. Todos os arquivos de inclusão necessários têm
já foi adicionado e os namespaces foram configurados. Toda adição de código é
terminou de seguir o comentário // Exercise 2.
Funções auxiliares
Vamos começar escrevendo algumas funções auxiliares que usamos para criar
operandos de registro de origem e destino. Eles serão usados como modelo
de registro e vai chamar o objeto RiscVState
para receber um handle para
registrar objeto e, em seguida, chamar um método de fábrica de operandos no objeto de registro.
Vamos começar com os auxiliares de operando de destino:
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency);
}
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency, op_name);
}
Como você pode notar, há duas funções auxiliares. O segundo leva mais um tempo
parâmetro op_name
que permite que o operando tenha um nome ou uma string diferente
do que o registro subjacente.
Da mesma forma, para os auxiliares de operandos de origem:
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand();
return op;
}
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand(op_name);
return op;
}
Funções de construtor e interface
As funções de construtor e interface são muito simples. O construtor apenas chama os dois métodos de inicialização para inicializar as matrizes callables para os getters do operando.
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
armazena a palavra de instrução e, em seguida, o código de operação
recebe de chamar o código gerado pelo decodificador binário.
// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
inst_word_ = inst_word;
opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}
Por fim, os getter do operando retornam o valor da função getter que ela chama com base na pesquisa da matriz usando o valor de enumeração do operando de destino/origem.
DestinationOperandInterface *RiscV32IEncoding::GetDestination(
SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
return dest_op_getters_[static_cast<int>(dest_op)](latency);
}
SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
SourceOpEnum source_op, int) {
return source_op_getters_[static_cast<int>(source_op)]();
}
Métodos de inicialização de matrizes
Como você deve ter adivinhado, a maior parte do trabalho é inicializar o getter
matrizes, mas não se preocupe, isso é feito usando um padrão fácil e repetido. Vamos
comece com InitializeDestinationOpGetters()
primeiro, já que há apenas um
operandos de destino.
Chame novamente a classe DestOpEnum
gerada de riscv32i_enums.h
:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
Para dest_op_getters_
, precisamos inicializar quatro entradas, uma para cada kNone
.
kCsr
, kNextPc
e kRd
. Por conveniência, cada entrada é inicializada com
lambda, embora também seja possível usar qualquer outra forma de chamável. Assinatura
do lambda é void(int latency)
.
Até agora não falamos muito sobre os diferentes tipos de destino
operandos que são definidos no MPACT-Sim. Para este exercício, usaremos apenas duas
tipos: generic::RegisterDestinationOperand
definido em
register.h
,
e generic::DevNullOperand
definidos no
devnull_operand.h
.
Os detalhes desses operandos não são muito importantes agora, exceto que o
o primeiro é usado para gravar nos registros, e o segundo ignora todas as gravações.
A primeira entrada para kNone
é trivial. Basta retornar um nullptr e, opcionalmente,
registrar um erro.
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
A seguir, kCsr
. Aqui, vamos trapacear um pouco. O "Hello World" programa
não depende de nenhuma atualização real da CSR, mas há um código boilerplate que
executar instruções de CSR. A solução é simular isso com uma
registro padrão chamado "CSR" e canalize todas as gravações para ele.
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
A seguir temos kNextPc
, que se refere ao "pc". se registrar. Ele é usado como destino
para todas as instruções de ramificação e salto. O nome é definido em RiscVState
como
kPcName
.
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
Por fim, há o operando de destino kRd
. Em riscv32i.isa
, o operando
rd
é usado apenas para se referir ao registro inteiro codificado no "rd" campo
da palavra de instrução, por isso não há ambiguidade a que se refere.
é apenas uma complicação. O registro x0
(nome da abi zero
) está conectado a 0
Portanto, para esse registro, usamos DevNullOperand
.
Então, neste getter, primeiro extraímos o valor no campo rd
usando o
método Extract
gerado a partir do arquivo .bin_fmt. Se o valor for 0,
retorna um "DevNull" operando. Caso contrário, retornamos o operando de registro correto.
e tenha o cuidado de usar o alias de registro apropriado como o nome do operando.
dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
// First extract register number from rd field.
int num = inst32_format::ExtractRd(inst_word_);
// For register x0, return the DevNull operand.
if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
// Return the proper register operand.
return GetRegisterDestinationOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
xreg_alias_[num]);
)
}
}
Agora, vamos para o método InitializeSourceOperandGetters()
, em que o padrão é
são quase iguais, mas os detalhes são um pouco diferentes.
Primeiro vamos dar uma olhada no SourceOpEnum
que foi gerado
riscv32i.isa
no primeiro tutorial:
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
Ao analisar os membros, além de kNone
, eles se enquadram em dois grupos. Um
são operandos imediatos: kBimm12
, kImm12
, kJimm20
, kSimm12
, kUimm20
,
e kUimm5
. Os outros são operandos de registro: kCsr
, kRs1
e kRs2
.
O operando kNone
é processado da mesma forma que os operandos de destino. Retorne um
nullptr.
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
Em seguida, vamos trabalhar nos operandos de registro. Vamos processar o kCsr
da mesma forma
de como lidamos com os operandos de destino correspondentes. Basta chamar o método
função auxiliar usando "CSR" como o nome de registro.
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
Os operandos kRs1
e kRs2
são tratados de maneira equivalente a kRd
, exceto que
sem atualizar x0
(ou zero
), queremos ter certeza de que
sempre lemos 0 desse operando. Para isso, vamos usar
A classe generic::IntLiteralOperand<>
foi definida em
literal_operand.h
.
Esse operando é usado para armazenar um valor literal (em vez de um valor
valor imediato). Caso contrário, o padrão será o mesmo: primeiro extraia o
valor rs1/rs2 da palavra de instrução. Se for zero, retorne o valor literal
operando com um parâmetro de modelo 0; caso contrário, retorna um registro regular
operando de origem usando a função auxiliar e usando o alias abi como operando
nome.
source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs1(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs2(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
Por fim, processamos os diferentes operandos imediatos. Os valores imediatos são
armazenadas em instâncias da classe generic::ImmediateOperand<>
definida no
immediate_operand.h
.
A única diferença entre os diferentes getters para os operandos imediatos
é qual função do extrator é usada e se o tipo de armazenamento é assinado ou
não assinado, de acordo com o bitfield.
// Immediates.
source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractBImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractImm12(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm5(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractJImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractSImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm32(inst_word_));
};
}
Se você precisar de ajuda (ou quiser verificar seu trabalho), a resposta completa é aqui.
Isso conclui este tutorial. Esperamos que tenha sido útil.