이 가이드의 목표는 다음과 같습니다.
- 생성된 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
선언입니다. 선언을 클래스 정의로 확장
여기서 RiscV32Decoder
는 generic::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
클래스에
기능을 제공합니다. 즉, 생성자를 선언해야 하므로
state
및 memory
를 가리키는 포인터를 사용할 수 있습니다.
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_;
또한
RiscV32IEncodingBase
를 RiscV32IEncoding
이라 부르겠습니다. 이 과정에서
연습 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 (함수 객체)
각각 SourceOpEnum
및 DestOpEnum
멤버의 숫자 값입니다.
이렇게 하면 이러한 메서드의 본문이
전달되고 반환을 반환하는 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 ®_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;
}
생성자 및 인터페이스 함수
생성자와 인터페이스 함수는 매우 간단합니다. 생성자 두 개의 초기화 메서드를 호출하여 호출 가능 함수와 관련된 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");
};
피연산자 kRs1
및 kRs2
는 다음을 제외하고 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_));
};
}
도움이 필요하거나 작업한 내용을 확인하려면 여기에서 확인할 수 있습니다.
이것으로 튜토리얼을 마칩니다. 이 가이드가 도움이 되었기를 바랍니다.