RiscV 통합 디코더

이 가이드의 목표는 다음과 같습니다.

  • 생성된 ISA와 바이너리 디코더가 어떻게 결합되는지 알아보세요.
  • RiscV용 전체 명령 디코더를 만드는 데 필요한 C++ 코드 작성 ISA와 바이너리 디코더를 결합하는 RV32I입니다.

명령 디코더 이해하기

명령 디코더는 명령 주소가 주어지면 명령 단어를 메모리에서 가져오고 완전히 초기화된 Instruction는 해당 안내를 나타냅니다.

최상위 디코더는 아래와 같이 generic::DecoderInterface를 구현합니다.

// 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;
};

보시다시피 구현해야 하는 메서드는 cpp virtual Instruction *DecodeInstruction(uint64_t address); 하나뿐입니다.

이제 제공되는 코드와 생성된 코드에서 무엇이 필요한지 살펴보겠습니다.

먼저 파일의 최상위 클래스 RiscV32IInstructionSet를 고려해 보세요. riscv32i_decoder.h - ISA 디코더. 콘텐츠를 새로 보려면 모든 것을 재구축할 수 있습니다

$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...

디렉터리를 다시 저장소 루트로 변경한 후 소스부터 검토됩니다 이를 위해 디렉터리를 bazel-out/k8-fastbuild/bin/riscv_isa_decoder (x86을 사용한다고 가정 호스트 - 다른 호스트의 경우 k8-fastbuild는 다른 문자열입니다).

$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder

생성된 C++ 코드가 포함된 소스 파일 4개가 표시됩니다.

  • riscv32i_decoder.h
  • riscv32i_decoder.cc
  • riscv32i_enums.h
  • riscv32i_enums.cc

첫 번째 파일 riscv32i_decoder.h를 엽니다. 우리가 사용하는 세 가지 클래스는 은(는) 다음을 살펴봐야 합니다.

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

클래스 이름을 확인합니다. 모든 클래스의 이름은 'isa'에 제공된 이름의 파스칼 표기법 선언을 포함하면 안 됩니다. isa RiscV32I { ... }

먼저 RiscVIInstructionSet 클래스부터 시작해 보겠습니다. 예를 들면 다음과 같습니다.

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_;
};

이 클래스에는 가상 메서드가 없으므로 독립형 클래스이지만, 두 가지를 주목하세요. 먼저 생성자는 RiscV32IInstructionSetFactory 클래스. 이 클래스는 디코더는 RiscV32Slot 클래스의 인스턴스를 만드는 데 사용하며, 이 인스턴스는 코드에 정의된 대로 slot RiscV32에 대해 정의된 모든 명령을 riscv32i.isa 파일 둘째, Decode 메서드는 추가 매개변수를 사용합니다. RiscV32IEncodingBase에 대한 포인터 유형이며, 이 클래스는 첫 번째 튜토리얼에서 생성된 I 디코더와 바이너리 디코더를 만들어 보겠습니다.

RiscV32IInstructionSetFactory 클래스는 추상 클래스이며 전체 디코더를 위한 자체 구현을 파생해야 합니다. 대부분의 경우 클래스는 간단합니다. 각 생성자에 대해 생성자를 호출하는 메서드를 제공하기만 하면 됩니다. .isa 파일에 정의된 슬롯 클래스와 일치합니다. 이 경우에는 아주 간단합니다. Riscv32Slot는 클래스 이름 riscv32의 파스칼 표기법입니다. Slot와 연결됨). 이 메서드는 서브클래스를 파생하는 데 유틸리티가 있을 수 있는 일부 고급 사용 사례 대신 슬롯의 생성자를 호출할 수 있습니다.

마지막 클래스 RiscV32IEncodingBase는 이 과정의 뒷부분에서 이 내용은 다른 실습에서 다루겠습니다


최상위 명령 디코더 정의

팩토리 클래스 정의

첫 번째 튜토리얼을 위해 프로젝트를 다시 빌드한 경우 riscv_full_decoder 디렉터리에 있습니다.

riscv32_decoder.h 파일을 엽니다. 필요한 모든 include 파일에는 이미 추가되었고 네임스페이스가 설정되었습니다.

//Exercise 1 - step 1로 표시된 주석 다음에 클래스를 정의합니다. RiscV32IInstructionSetFactory에서 상속되는 RiscV32IsaFactory입니다.

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

다음으로 CreateRiscv32Slot의 재정의를 정의합니다. 우리는 Riscv32Slot의 파생 클래스를 사용하는 경우 간단히 다음을 사용하여 새 인스턴스를 할당합니다. std::make_unique입니다.

std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
  return std::make_unique<Riscv32Slot>(state);
}

도움이 필요하거나 작업한 내용을 확인하려면 여기에서 확인할 수 있습니다.

디코더 클래스 정의

생성자, 소멸자, 메서드 선언

이제 디코더 클래스를 정의할 차례입니다. 위와 동일한 파일에서 RiscV32Decoder 선언입니다. 선언을 클래스 정의로 확장 여기서 RiscV32Decodergeneric::DecoderInterface에서 상속받습니다.

class RiscV32Decoder : public generic::DecoderInterface {
  public:
};

다음으로, 생성자를 작성하기 전에 코드를 간단히 살펴보겠습니다. 바이너리 디코더에 관해 알아보겠습니다. 기존의 Extract 함수이면 DecodeRiscVInst32 함수가 있습니다.

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

이 함수는 디코딩해야 하는 명령 단어를 취하고 명령 부호를 반환합니다. 반면에 RiscV32Decoder가 구현하는 DecodeInterface 클래스는 있습니다. 따라서 RiscV32Decoder 클래스는 메모리에 액세스하여 DecodeRiscVInst32()에 전달할 안내 단어를 읽습니다. 이 프로젝트 메모리에 액세스하는 방법은 .../mpact/sim/util/memory의 적절한 이름을 util::MemoryInterface로 지정했습니다(아래 참고).

  // 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;

또한 state 클래스 인스턴스를 다른 디코더 클래스의 생성자로 전달됩니다. 적절한 상태 클래스는 generic::ArchState에서 파생된 riscv::RiscVState 클래스에 기능을 제공합니다. 즉, 생성자를 선언해야 하므로 statememory를 가리키는 포인터를 사용할 수 있습니다.

RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);

기본 생성자를 삭제하고 소멸자를 재정의합니다.

RiscV32Decoder() = delete;
~RiscV32Decoder() override;

다음으로 재정의해야 하는 DecodeInstruction 메서드를 선언합니다. generic::DecoderInterface입니다.

generic::Instruction *DecodeInstruction(uint64_t address) override;

도움이 필요하거나 작업한 내용을 확인하려면 여기에서 확인할 수 있습니다.


데이터 회원 정의

RiscV32Decoder 클래스에는 생성자 매개변수와 팩토리 클래스에 대한 포인터로 구성됩니다.

 private:
  riscv::RiscVState *state_;
  util::MemoryInterface *memory_;

또한 RiscV32IEncodingBaseRiscV32IEncoding이라 부르겠습니다. 이 과정에서 연습 2에서 설명) 또한 RiscV32IInstructionSet이므로 다음을 추가합니다.

  RiscV32IsaFactory *riscv_isa_factory_;
  RiscV32IEncoding *riscv_encoding_;
  RiscV32IInstructionSet *riscv_isa_;

마지막으로 메모리 인터페이스와 함께 사용할 데이터 멤버를 정의해야 합니다.

  generic::DataBuffer *inst_db_;

도움이 필요하거나 작업한 내용을 확인하려면 여기에서 확인할 수 있습니다.

디코더 클래스 메서드 정의

다음으로 생성자, 소멸자, DecodeInstruction 메서드를 사용하여 지도 가장자리에 패딩을 추가할 수 있습니다. riscv32_decoder.cc 파일을 엽니다. 빈 메서드뿐만 아니라 네임스페이스 선언과 총 using개의 선언 중

생성자 정의

생성자는 데이터 멤버만 초기화하면 됩니다. 먼저 state_memory_:

RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
                               util::MemoryInterface *memory)
    : state_(state), memory_(memory) {

다음으로 디코더 관련 클래스 각각의 인스턴스를 할당하고 매개변수를 제공합니다.

  // 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);

마지막으로 DataBuffer 인스턴스를 할당합니다. 팩토리를 사용하여 할당됩니다. state_ 멤버를 통해 액세스할 수 있습니다. 데이터를 저장하기 위해 크기가 조정된 데이터 버퍼를 할당합니다. 단일 uint32_t(명령어의 크기이므로)

  inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);

소멸자 정의

소멸자는 단순합니다. 생성자에서 할당한 객체를 해제하기만 하면 됩니다. 그러나 한 가지 반전이 있습니다. 데이터 버퍼 인스턴스는 참조로 계산되므로 대신 해당 포인터에서 delete를 호출하여 객체를 DecRef()합니다.

RiscV32Decoder::~RiscV32Decoder() {
  inst_db_->DecRef();
  delete riscv_isa_;
  delete riscv_isa_factory_;
  delete riscv_encoding_;
}

메서드 정의

여기서 이 메서드의 구현은 매우 간단합니다. 이 주소가 올바르게 정렬되어 있고 추가 오류 검사가 필요합니다.

먼저, 명령 단어를 메모리에서 가져와야 하며 인터페이스 및 DataBuffer 인스턴스

  memory_->Load(address, inst_db_, nullptr, nullptr);
  uint32_t iword = inst_db_->Get<uint32_t>(0);

다음으로 RiscVIEncoding 인스턴스를 호출하여 명령 단어를 파싱합니다. 이 작업은 ISA 디코더 자체를 호출하기 전에 이루어져야 합니다. ISA는 디코더가 RiscVIEncoding 인스턴스를 직접 호출하여 명령 코드를 가져옵니다. 피연산자, 그리고 명령 단어에 의해 지정된 피연산자. 아직까지는 클래스를 아직 사용하지 않지만 이 메서드로 void ParseInstruction(uint32_t)를 사용하겠습니다.

  riscv_encoding_->ParseInstruction(iword);

마지막으로 ISA 디코더라고 부르며 주소와 인코딩 클래스를 전달합니다.

  auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
  return instruction;

도움이 필요하거나 작업한 내용을 확인하려면 여기에서 확인할 수 있습니다.


인코딩 클래스

인코딩 클래스는 디코더 클래스에서 사용하는 인터페이스를 구현합니다. 명령어 명령 코드, 소스 및 대상 피연산자 및 리소스 피연산자입니다. 이러한 객체는 모두 바이너리의 정보에 의존합니다 형식 디코더(예: 명령 코드), 디코더 클래스로부터 분리되어 여러 인코딩 방식을 지원할 수 있습니다. 있습니다.

RiscV32IEncodingBase는 추상 클래스입니다. 우리가 수행해야 하는 일련의 메서드는 아래에서는 파생된 클래스에서 구현하는 방법을 보여줍니다.

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;
};

언뜻 보기에는 약간 복잡하게 보입니다. 특히 그러나 RiscV와 같은 간단한 아키텍처의 경우 실제로 대부분의 해당 값이 암시되기 때문입니다.

차례대로 각 방법을 살펴보겠습니다.

OpcodeEnum GetOpcode(SlotEnum slot, int entry);

GetOpcode 메서드는 현재OpcodeEnum 명령, 명령 명령 코드를 식별합니다. OpcodeEnum 클래스는 디코더 파일 riscv32i_enums.h에 정의되어 있습니다. 이 메서드는 두 개의 매개변수가 있습니다. 두 매개변수 모두 목적에 따라 무시해도 됩니다. 첫 번째 슬롯 유형 (riscv32i_enums.h에서도 정의된 enum 클래스)입니다. RiscV에는 단일 슬롯만 있으므로 가능한 값이 하나만 있습니다. SlotEnum::kRiscv32입니다. 두 번째는 슬롯의 인스턴스 번호입니다 (이 경우 일부 VLIW에서는 발생할 수 있는 슬롯의 인스턴스가 여러 개 있습니다. 합니다.

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);

다음 두 메서드는 프로세서의 하드웨어 리소스를 모델링하는 데 사용됩니다. 해야 합니다. 튜토리얼 연습에서는 구현에서 스텁 처리되어 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);

이 세 가지 메서드는 내부에서 사용되는 피연산자 객체에 대한 포인터를 반환합니다. 모든 명령의 값에 액세스할 수 있는 명령 시맨틱 함수 조건자 피연산자, 각 명령어 소스 피연산자, 새 피연산자 값을 명령 대상 피연산자에 전달합니다. RiscV는 명령어 조건자가 있으면 해당 메서드는 nullptr만 반환하면 됩니다.

매개변수 패턴은 이러한 함수 간에 유사합니다. 먼저, GetOpcode 슬롯과 항목이 전달됩니다. 그런 다음 피연산자를 생성해야 하는 명령입니다. 이 속성은 다른 명령 코드가 동일한 피연산자에 대해 다른 피연산자 객체를 반환해야 함 이는 이 RiscV 시뮬레이터에는 해당되지 않습니다.

다음은 Predicate, Source, Destination, 피연산자 열거형 항목이 나와 있습니다. 생성해야 하는 피연산자를 식별합니다. 이 세 가지는 OpEnums은 아래와 같습니다.riscv32i_enums.h

  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,
  };

지난 몇 년을 돌이켜보면 riscv32.isa 드림 파일을 살펴보면 이들은 소스 및 대상의 집합에 해당한다는 것을 알 수 있습니다. 각 명령어의 선언에 사용된 피연산자 이름입니다. 다양한 서로 다른 비트필드와 피연산자를 나타내는 피연산자의 피연산자 이름 형식의 경우 enum 멤버의 고유한 특성을 사용하여 인코딩 클래스를 더 쉽게 작성할 수 있습니다. 반환할 정확한 피연산자 유형을 결정하며, 슬롯, 항목 또는 명령 코드 매개변수의 값을 고려하세요.

마지막으로 소스 및 대상 피연산자의 경우 피연산자의 서수 위치 피연산자가 전달되고 (다시 말해도 무시해도 됨) 대상 피연산자: 명령이 실행되어 실행되고, 후속 명령에서 대상 결과를 사용할 수 있습니다. 시뮬레이터에서 이 지연 시간은 0이 되며, 이는 명령이 결과를 즉시 금전 등록기로 보냅니다.

int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no);

최종 함수는 특정 대상의 지연 시간을 가져오는 데 사용됩니다. .isa 파일에서 *로 지정된 경우 피연산자입니다. 드물지만 이 RiscV 시뮬레이터에는 사용되지 않으므로 이 함수를 구현한 다음 0만 반환합니다.


인코딩 클래스 정의

헤더 파일 (.h)

메서드

riscv32i_encoding.h 파일을 엽니다. 필요한 모든 include 파일에는 이미 추가되었고 네임스페이스가 설정되었습니다. 모든 코드 추가는 // Exercise 2. 댓글 팔로우 완료

먼저 RiscV32IEncoding 인터페이스입니다.

class RiscV32IEncoding : public RiscV32IEncodingBase {
 public:

};

다음으로 생성자는 상태 인스턴스를 가리키는 포인터를 취해야 합니다. 여기서는 riscv::RiscVState를 가리키는 포인터입니다. 기본 소멸자를 사용해야 합니다.

explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;

모든 인터페이스 메서드를 추가하기 전에 RiscV32Decoder를 사용하여 명령어를 파싱합니다.

void ParseInstruction(uint32_t inst_word);

이제 사용되지 않는 매개변수의 이름:

// 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; }

마지막으로 공개 인터페이스의 나머지 메서드 재정의를 추가하되 구현을 .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;

각 피연산자 getter 메서드의 구현을 간소화하기 위해 이 함수를 사용하여 색인을 생성한 callable (함수 객체) 각각 SourceOpEnumDestOpEnum 멤버의 숫자 값입니다. 이렇게 하면 이러한 메서드의 본문이 전달되고 반환을 반환하는 enum 값에 대한 함수 객체 값으로 사용됩니다.

이 두 배열의 초기화를 구성하기 위해 두 개의 private 이 생성자에서 호출되는 메서드는 다음과 같습니다.

 private:
  void InitializeSourceOperandGetters();
  void InitializeDestinationOperandGetters();

데이터 멤버

필요한 데이터 멤버는 다음과 같습니다.

  • state_: riscv::RiscVState * 값을 보유합니다.
  • 현재 값을 보유하는 uint32_t 유형의 inst_word_ 표시됩니다.
  • opcode_: 다음으로 업데이트하는 현재 명령어의 명령 코드 보유 ParseInstruction 메서드 OpcodeEnum 유형입니다.
  • source_op_getters_: 소스를 가져오는 데 사용되는 callables를 저장할 배열 피연산자 객체입니다. 배열 요소의 유형은 다음과 같습니다. absl::AnyInvocable<SourceOperandInterface *>()>
  • dest_op_getters_: 가져오는 데 사용되는 callables를 저장할 배열 대상 피연산자 객체입니다. 배열 요소의 유형은 다음과 같습니다. absl::AnyInvocable<DestinationOperandInterface *>()>
  • xreg_alias는 RiscV 정수 레지스터 ABI 이름의 배열입니다. 예: '0' 및 'ra' 'x0' 대신 '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"};

도움이 필요하거나 작업한 내용을 확인하려면 여기에서 확인할 수 있습니다.

소스 파일 (.cc)

riscv32i_encoding.cc 파일을 엽니다. 필요한 모든 include 파일에는 이미 추가되었고 네임스페이스가 설정되었습니다. 모든 코드 추가는 // Exercise 2. 댓글 팔로우 완료

도우미 함수

먼저 코드 생성에 사용하는 몇 가지 도우미 함수를 소스 및 대상 레지스터 피연산자. 이는 RiscVState 객체를 호출하여 등록된 다음 레지스터 객체에서 피연산자 팩토리 메서드를 호출해야 합니다.

대상 피연산자 도우미부터 시작하겠습니다.

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);
}

보시다시피 두 가지 도우미 함수가 있습니다. 두 번째 옵션은 피연산자의 이름이나 문자열이 다른 매개변수 op_name 기본 레지스터가 아닌 표현이 사용됩니다.

소스 피연산자 도우미의 경우에도 마찬가지입니다.

template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
                                                   const std::string &reg_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 &reg_name,
                                                   const std::string &op_name) {
  auto *reg = state->GetRegister<RegType>(reg_name).first;
  auto *op = reg->CreateSourceOperand(op_name);
  return op;
}

생성자 및 인터페이스 함수

생성자와 인터페이스 함수는 매우 간단합니다. 생성자 두 개의 초기화 메서드를 호출하여 호출 가능 함수와 관련된 callables 배열을 초기화합니다. 피연산자 getter입니다.

RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
  InitializeSourceOperandGetters();
  InitializeDestinationOperandGetters();
}

ParseInstruction는 명령 단어와 이를 나타내는 명령 코드를 저장합니다. 바이너리 디코더 생성 코드로 호출함으로써 얻게 됩니다.

// 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_);
}

마지막으로 피연산자 getter는 피연산자 getter가 호출하는 getter 함수의 값을 반환합니다. 대상/소스 피연산자 enum 값을 사용한 배열 조회를 기반으로 합니다.


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)]();
}

배열 초기화 메서드

짐작하셨겠지만, 대부분의 작업은 getter를 걱정하지 마세요. 쉽고 반복되는 패턴을 사용하여 수행됩니다. 자, 오직 하나의 숫자만 있기 때문에 먼저 InitializeDestinationOpGetters()로 시작합니다. 몇 개의 대상 피연산자가 있습니다.

riscv32i_enums.h에서 생성된 DestOpEnum 클래스를 다시 호출합니다.

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kNextPc = 2,
    kRd = 3,
    kPastMaxValue = 4,
  };

dest_op_getters_의 경우 kNone에 하나씩 총 4개의 항목을 초기화해야 합니다. kCsr, kNextPc, kRd 편의를 위해 각 항목은 하지만 다른 형태의 callable 함수도 사용할 수 있습니다. 서명 람다의 값은 void(int latency)입니다.

지금까지 우리는 다양한 종류의 목적지에 대해 많이 이야기하지 않았습니다. MPACT-Sim에 정의된 피연산자 이 연습에서는 유형: generic::RegisterDestinationOperand 다음에 정의됨 register.h, 및 generic::DevNullOperand의 정의 devnull_operand.h 이러한 피연산자의 세부 사항은 별로 중요하지 않습니다. 다만 전자는 레지스터에 쓰는 데 사용되고 후자는 모든 쓰기를 무시합니다.

kNone의 첫 번째 항목은 사소합니다. nullptr을 반환하고 선택적으로 오류를 로깅합니다.

void RiscV32IEncoding::InitializeDestinationOperandGetters() {
  // Destination operand getters.
  dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
    return nullptr;
  };

다음은 kCsr입니다. 여기서는 약간의 치트를 해보겠습니다. 'hello world' 프로그램 실제 CSR 업데이트에 의존하지 않지만 CSR 명령을 실행합니다. 해결책은 'CSR'이라는 일반 금전등록기 모든 쓰기 작업을 여기에 채널링해야 합니다.

  dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
  };

다음은 'pc'를 나타내는 kNextPc입니다. 레지스터. 타겟으로 사용됩니다. 를 참조하세요. 이름은 RiscVState에서 다음과 같이 정의됩니다. kPcName입니다.

  dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
  }

마지막으로 kRd 대상 피연산자가 있습니다. riscv32i.isa에서 피연산자 rd는 'rd'로 인코딩된 정수 레지스터를 참조하는 데만 사용됩니다. 필드 와 일치하므로 그것이 참조하는 모호성이 없습니다. 거기 하나의 정보 표시일 뿐입니다 x0 (별칭 zero)이 0으로 기본 설정된 등록 이 레지스터에는 DevNullOperand를 사용합니다.

따라서 이 getter에서는 먼저 rd 필드 값을 추출하여 Extract 메서드를 호출합니다. 값이 0이면 'DevNull'을 반환 그렇지 않으면 올바른 레지스터 피연산자를 반환합니다. 적절한 레지스터 별칭을 피연산자 이름으로 사용해야 합니다.

  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]);
    )
  }
}

이제 InitializeSourceOperandGetters() 메서드로 이동합니다. 여기서 패턴은 다음과 같습니다. 매우 동일하지만 세부 사항이 약간 다릅니다.

먼저 SourceOpEnum 첫 번째 튜토리얼의 riscv32i.isa:

  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,
  };

구성원을 검토한 결과 kNone 외에도 두 그룹으로 나뉩니다. 1개 즉시 피연산자: kBimm12, kImm12, kJimm20, kSimm12, kUimm20, 및 kUimm5 다른 하나는 레지스터 피연산자(kCsr, kRs1, kRs2)입니다.

kNone 피연산자는 대상 피연산자와 마찬가지로 처리되며 nullptr을 반환합니다.

void RiscV32IEncoding::InitializeSourceOperandGetters() {
  // Source operand getters.
  source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
    return nullptr;
  };

다음으로 레지스터 피연산자에 관해 작업해 보겠습니다. kCsr는 유사하게 처리합니다. 대상 피연산자를 처리하는 방법을 다루었습니다. 이 메서드를 호출하기만 하면 'CSR'을 사용하는 도우미 함수 를 레지스터 이름으로 지정합니다.

  // Register operands.
  source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
    return GetRegisterSourceOp<RV32Register>(state_, "CSR");
  };

피연산자 kRs1kRs2는 다음을 제외하고 kRd와 동일하게 처리됩니다. x0 (또는 zero)를 업데이트하지는 않으셨지만 우리는 항상 해당 피연산자에서 0을 읽습니다. 이를 위해 generic::IntLiteralOperand<> 클래스 정의 위치 literal_operand.h 이 피연산자는 시뮬레이션된 피연산자가 아닌 리터럴 값을 저장하는 데 즉각적인 값). 그렇지 않으면 패턴이 동일합니다. 먼저 명령 단어의 rs1/rs2 값이며, 이 값이 0이면 리터럴을 반환합니다. 템플릿 매개변수가 0인 피연산자, 그렇지 않은 경우 일반 레지스터 반환 abi 별칭을 피연산자로 사용: 도우미 함수를 사용하는 소스 피연산자 있습니다.

  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]);
  };

마지막으로, 다양한 직접 피연산자를 처리합니다. 즉각적인 값은 다음에 정의된 generic::ImmediateOperand<> 클래스의 인스턴스에 저장됩니다. immediate_operand.h 바로 피연산자의 서로 다른 getter 간 유일한 차이점 추출기 함수가 사용되는지, 스토리지 유형이 부호 있는 것인지 비트필드에 따라 부호 없음.

  // 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_));
  };
}

도움이 필요하거나 작업한 내용을 확인하려면 여기에서 확인할 수 있습니다.

이것으로 튜토리얼을 마칩니다. 이 가이드가 도움이 되었기를 바랍니다.