RISC V ISA 디코더

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

  • MPACT-Sim 시뮬레이터에 명령이 표시되는 방식을 알아봅니다.
  • ISA 설명 파일의 구조와 문법을 알아봅니다.
  • RiscV RV32I 명령어 하위 집합의 ISA 설명 작성

개요

MPACT-Sim에서는 타겟 명령어가 디코딩되어 내부 표현으로 저장되므로 정보를 더 쉽게 사용할 수 있고 시맨틱을 더 빠르게 실행할 수 있습니다. 이러한 명령 인스턴스는 명령 캐시에 캐시되어 자주 실행되는 명령이 실행되는 횟수를 줄입니다.

안내 클래스

시작하기 전에 MPACT-Sim에서 명령어가 어떻게 표현되는지 살펴보는 것이 좋습니다. Instruction 클래스는 mpact-sim/mpact/sim/generic/instruction.h에 정의되어 있습니다.

Instruction 클래스 인스턴스에는 '실행'될 때 명령어를 시뮬레이션하는 데 필요한 모든 정보(예:

  1. 안내 주소, 시뮬레이션된 안내 크기(예: .text 형식 크기)
  2. 명령어 opcode입니다.
  3. 조건자 피연산자 인터페이스 포인터(해당하는 경우)
  4. 소스 피연산자 인터페이스 포인터의 벡터입니다.
  5. 대상 피연산자 인터페이스 포인터의 벡터입니다.
  6. 시맨틱 함수를 호출할 수 있습니다.
  7. 아키텍처 상태 객체의 포인터입니다.
  8. 컨텍스트 객체 포인터입니다.
  9. 하위 및 다음 명령 인스턴스를 가리키는 포인터입니다.
  10. 분해 문자열입니다.

이러한 인스턴스는 일반적으로 명령어(인스턴스) 캐시에 저장되며 명령어가 다시 실행될 때마다 재사용됩니다. 따라서 런타임 중 성능이 향상됩니다.

컨텍스트 객체에 대한 포인터를 제외하고 모두 ISA 설명에서 생성된 명령 디코더에 의해 채워집니다. 이 튜토리얼에서는 이러한 항목을 직접 사용하지 않으므로 이러한 항목에 대한 세부정보를 알 필요가 없습니다. 대신 사용 방법을 대략적으로 파악하는 것으로 충분합니다.

시맨틱 함수 호출 가능 항목은 명령의 시맨틱을 구현하는 C++ 함수/메서드/함수 객체(람다 포함)입니다. 예를 들어 add 명령어의 경우 각 소스 피연산자를 로드하고 두 피연산자를 더한 후 결과를 단일 대상 피연산자에 씁니다. 시맨틱 함수에 대한 주제는 시맨틱 함수 가이드에서 자세히 다룹니다.

명령어 피연산자

명령어 클래스에는 조건자, 소스, 대상이라는 세 가지 유형의 피연산자 인터페이스에 대한 포인터가 포함되어 있습니다. 이러한 인터페이스를 사용하면 기본 명령어 피연산자의 실제 유형과 관계없이 시맨틱 함수를 작성할 수 있습니다. 예를 들어, 레지스터와 즉시 값의 액세스는 동일한 인터페이스를 통해 이루어집니다. 즉, 동일한 연산을 실행하지만 다른 피연산자(예: 레지스터와 즉시 값)를 사용하는 명령어는 동일한 시맨틱 함수를 사용하여 구현할 수 있습니다.

조건자 피연산자 인터페이스는 조건자 명령 실행을 지원하는 ISA의 경우 (다른 ISA의 경우 null임) 조건자의 불리언 값에 따라 지정된 명령을 실행해야 하는지 결정하는 데 사용됩니다.

// The predicte operand interface is intended primarily as the interface to
// read the value of instruction predicates. It is separated from source
// predicates to avoid mixing it in with the source operands needed for modeling
// the instruction semantics.
class PredicateOperandInterface {
 public:
  virtual bool Value() = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
  virtual ~PredicateOperandInterface() = default;
};

소스 피연산자 인터페이스를 사용하면 명령어 시맨틱 함수가 기본 피연산자 유형과 관계없이 명령어 피연산자에서 값을 읽을 수 있습니다. 인터페이스 메서드는 스칼라 값과 벡터 값 피연산자를 모두 지원합니다.

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

대상 피연산자 인터페이스는 DataBuffer 인스턴스(레지스터 값을 저장하는 데 사용되는 내부 데이터 유형)를 할당하고 처리하는 메서드를 제공합니다. 또한 대상 피연산자에는 명령어 시맨틱 함수에 의해 할당된 데이터 버퍼 인스턴스가 타겟 레지스터의 값을 업데이트하는 데 사용될 때까지 기다리는 사이클 수인 지연 시간이 연결되어 있습니다. 예를 들어 add 명령어의 지연 시간은 1일 수 있지만 mpy 명령어의 지연 시간은 4일 수 있습니다. 이 내용은 시맨틱 함수 가이드에서 더 자세히 다룹니다

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

ISA 설명

프로세서의 ISA(명령 집합 아키텍처)는 소프트웨어가 하드웨어와 상호작용하는 추상적 모델을 정의합니다. 사용 가능한 명령어 집합, 데이터 유형, 레지스터, 명령어가 작동하는 기타 머신 상태, 동작(시맨틱)을 정의합니다. MPACT-Sim의 목적상 ISA에는 명령의 실제 인코딩이 포함되지 않습니다. 이는 별도로 취급됩니다.

프로세서 ISA는 인코딩에 상관없이 추상적인 수준에서 명령 집합을 설명하는 설명 파일에 표현됩니다. 설명 파일에는 사용 가능한 명령 집합이 열거되어 있습니다. 각 명령어의 경우 이름, 피연산자의 수 및 이름, 시맨틱을 구현하는 C++ 함수/호출 가능한 함수에 대한 바인딩을 나열해야 합니다. 또한 디스어셈블리 형식 지정 문자열과 명령의 하드웨어 리소스 이름 사용을 지정할 수 있습니다. 전자는 디버그, 추적 또는 대화형 사용을 위한 명령어의 텍스트 표현을 생성하는 데 유용합니다. 후자는 시뮬레이션에 더 많은 주기 정확성을 내장하는 데 사용할 수 있습니다.

ISA 설명 파일은 표현 제약이 없는 명령 디코더의 코드를 생성하는 isa 파서에 의해 파싱됩니다. 이 디코더는 명령 객체의 필드를 채웁니다. 구체적인 값(예: 대상 레지스터 번호)은 형식별 명령 디코더에서 가져옵니다. 이러한 디코더 중 하나가 바이너리 디코더로, 다음 가이드에서 중점적으로 다룹니다.

이 튜토리얼에서는 간단한 스칼라 아키텍처의 ISA 설명 파일을 작성하는 방법을 설명합니다. 이를 설명하기 위해 RiscV RV32I 명령 세트의 하위 집합을 사용하고 다른 튜토리얼과 함께 'Hello World' 프로그램을 시뮬레이션할 수 있는 시뮬레이터를 빌드합니다. RiscV ISA에 관한 자세한 내용은 Risc-V 사양을 참고하세요.

먼저 파일을 엽니다. riscv_isa_decoder/riscv32i.isa

파일의 콘텐츠가 여러 섹션으로 분할됩니다. 첫 번째는 ISA 선언입니다.

isa RiscV32I {
  namespace mpact::sim::codelab;
  slots { riscv32; }
}

이렇게 하면 RiscV32I가 ISA의 이름으로 선언되고 코드 생성기는 생성된 디코더가 opcode 및 피연산자 정보를 가져오는 데 사용할 인터페이스를 정의하는 RiscV32IEncodingBase라는 클래스를 만듭니다. 이 클래스의 이름은 ISA 이름을 파스칼 표기법으로 변환한 다음 EncodingBase와 연결하여 생성됩니다. 선언 slots { riscv32; }는 VLIW 명령의 여러 슬롯과 달리 RiscV32I ISA에 단일 명령 슬롯 riscv32만 있고 유효한 명령은 riscv32에서 실행되도록 정의된 명령뿐이라고 지정합니다.

// First disasm fragment is 15 char wide and left justified.
disasm widths = {-15};

이렇게 하면 모든 분해 사양 (아래 참조)의 첫 번째 분해 프래그먼트가 15자 너비 필드에 왼쪽 정렬되도록 지정됩니다. 후속 프래그먼트는 추가 공백 없이 이 필드에 추가됩니다.

그 아래에는 riscv32i, zicsr, riscv32의 세 가지 슬롯 선언이 있습니다. 위의 isa 정의를 기반으로 riscv32 슬롯에 정의된 명령어만 RiscV32I isa의 일부가 됩니다. 다른 두 슬롯은 어떤 용도인가요?

슬롯을 사용하여 명령을 개별 그룹으로 분해한 다음 끝에 단일 슬롯으로 결합할 수 있습니다. riscv32 슬롯 선언에서 : riscv32i, zicsr 표기법을 확인합니다. 이는 슬롯 riscv32가 슬롯 zicsrriscv32i에 정의된 모든 안내를 상속한다고 지정합니다. RiscV 32비트 ISA는 RV32I라는 기본 ISA로 구성되며 여기에 선택적 확장 프로그램 집합을 추가할 수 있습니다. 슬롯 메커니즘을 사용하면 이러한 확장 프로그램의 명령을 별도로 지정한 다음 필요에 따라 결합하여 전반적인 ISA를 정의할 수 있습니다. 이 경우 RiscV 'I' 그룹의 명령어는 'zicsr' 그룹의 명령어와 별도로 정의됩니다. 원하는 최종 RiscV ISA에 필요한 경우 'M'(곱셈/나눗셈), 'F'(단정밀도 부동 소수점), 'D'(배정밀도 부동 소수점), 'C'(소형 16비트 명령어) 등에 관한 추가 그룹을 정의할 수 있습니다.

// The RiscV 'I' instructions.
slot riscv32i {
  ...
}

// RiscV32 CSR manipulation instructions.
slot zicsr {
  ...
}

// The final instruction set combines riscv32i and zicsr.
slot riscv32 : riscv32i, zicsr {
  ...
}

zicsrriscv32 슬롯 정의는 변경할 필요가 없습니다. 하지만 이 튜토리얼에서는 riscv32i 슬롯에 필요한 정의를 추가하는 방법을 중점적으로 다룹니다. 현재 이 슬롯에 정의된 내용을 자세히 살펴보겠습니다.

// The RiscV 'I' instructions.
slot riscv32i {
  // Include file that contains the declarations of the semantic functions for
  // the 'I' instructions.
  includes {
    #include "learning/brain/research/mpact/sim/codelab/riscv_semantic_functions/solution/rv32i_instructions.h"
  }
  // These are all 32 bit instructions, so set default size to 4.
  default size = 4;
  // Model these with 0 latency to avoid buffering the result. Since RiscV
  // instructions have sequential semantics this is fine.
  default latency = 0;
  // The opcodes.
  opcodes {
    fence{: imm12 : },
      semfunc: "&RV32IFence"c
      disasm: "fence";
    ebreak{},
      semfunc: "&RV32IEbreak",
      disasm: "ebreak";
  }
}

첫 번째는 이 슬롯이 최종 ISA에서 직접 또는 간접적으로 참조될 때 생성된 코드에 포함되어야 하는 헤더 파일을 나열하는 includes {} 섹션입니다. 포함 파일은 전역 범위의 includes {} 섹션에 나열할 수도 있으며, 이 경우 항상 포함됩니다. 이렇게 하면 동일한 포함 파일을 모든 슬롯 정의에 추가해야 하는 경우 유용할 수 있습니다.

default sizedefault latency 선언은 달리 지정하지 않는 한 명령어 크기가 4이고 대상 피연산자 쓰기의 지연 시간이 0주기임을 정의합니다. 여기에 지정된 명령의 크기는 시뮬레이션된 프로세서에서 실행할 다음 순차 명령의 주소를 계산하기 위한 프로그램 카운터 증가의 크기입니다. 이는 입력 실행 파일의 명령어 표현 크기(바이트)와 동일할 수도 있고 그렇지 않을 수도 있습니다.

슬롯 정의의 중심에는 명령 코드 섹션이 있습니다. 보시다시피 지금까지는 두 개의 명령 코드 (명령어) fenceebreakriscv32i에 정의되었습니다. fence opcode는 이름(fence)과 피연산자 사양({: imm12 : })을 지정한 다음 선택적 디스어셈블리 형식("fence")과 시맨틱 함수("&RV32IFence")로 바인딩할 호출 가능 함수를 지정하여 정의됩니다.

명령어 피연산자는 삼중으로 지정되며, 각 구성요소는 세미콜론(predicate ':' source operand list ':' destination operand list)로 구분됩니다. 소스 및 대상 피연산자 목록은 쉼표로 구분된 피연산자 이름 목록입니다. 보시다시피 fence 명령어의 명령어 피연산자에는 조건자 피연산자가 없고 단일 소스 피연산자 이름 imm12만 있으며 대상 피연산자는 없습니다. RiscV RV32I 하위 집합은 사전 실행을 지원하지 않으므로 이 튜토리얼에서는 조건자 피연산자가 항상 비어 있습니다.

시맨틱 함수는 시맨틱 함수를 호출하는 데 사용할 C++ 함수 또는 호출 가능 함수를 지정하는 데 필요한 문자열로 지정됩니다. 시맨틱 함수/호출 가능한 함수의 서명은 void(Instruction *)입니다.

분해 사양은 쉼표로 구분된 문자열 목록으로 구성됩니다. 일반적으로 두 개의 문자열만 사용됩니다. 하나는 opcode에, 하나는 피연산자에 사용됩니다. 명령에서 AsString() 호출을 사용하여 형식을 지정할 때 각 문자열은 위에서 설명한 disasm widths 사양에 따라 필드 내에서 형식이 지정됩니다.

다음 연습에서는 'Hello World' 프로그램을 시뮬레이션하기에 충분한 안내를 riscv32i.isa 파일에 추가하는 방법을 보여줍니다. 서둘러야 하는 경우 솔루션은 riscv32i.isarv32i_instructions.h에서 확인할 수 있습니다.


초기 빌드 실행

디렉터리를 riscv_isa_decoder로 변경하지 않았다면 지금 변경합니다. 그런 다음 다음과 같이 프로젝트를 빌드하면 성공적으로 빌드됩니다.

$ cd riscv_isa_decoder
$ bazel build :all

이제 디렉터리를 저장소 루트로 다시 변경한 다음 생성된 소스를 살펴보겠습니다. 이를 위해 디렉터리를 bazel-out/k8-fastbuild/bin/riscv_isa_decoder로 변경합니다 (x86 호스트에 있다고 가정하고 다른 호스트의 경우 k8-fastbuild가 다른 문자열입니다).

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

이 디렉터리에는 다른 파일과 함께 다음과 같이 생성된 C++ 파일이 있습니다.

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

브라우저에서 riscv32i_enums.h를 클릭하여 살펴보겠습니다. 다음과 같은 내용이 포함되어 있는지 확인합니다.

#ifndef RISCV32I_ENUMS_H
#define RISCV32I_ENUMS_H

namespace mpact {
namespace sim {
namespace codelab {
  enum class SlotEnum {
    kNone = 0,
    kRiscv32,
  };

  enum class PredOpEnum {
    kNone = 0,
    kPastMaxValue = 1,
  };

  enum class SourceOpEnum {
    kNone = 0,
    kCsr = 1,
    kImm12 = 2,
    kRs1 = 3,
    kPastMaxValue = 4,
  };

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

  enum class OpcodeEnum {
    kNone = 0,
    kCsrs = 1,
    kCsrsNw = 2,
    kCsrwNr = 3,
    kEbreak = 4,
    kFence = 5,
    kPastMaxValue = 6
  };

  constexpr char kNoneName[] = "none";
  constexpr char kCsrsName[] = "Csrs";
  constexpr char kCsrsNwName[] = "CsrsNw";
  constexpr char kCsrwNrName[] = "CsrwNr";
  constexpr char kEbreakName[] = "Ebreak";
  constexpr char kFenceName[] = "Fence";
  extern const char *kOpcodeNames[static_cast<int>(
      OpcodeEnum::kPastMaxValue)];

  enum class SimpleResourceEnum {
    kNone = 0,
    kPastMaxValue = 1
  };

  enum class ComplexResourceEnum {
    kNone = 0,
    kPastMaxValue = 1
  };

  enum class AttributeEnum {
    kPastMaxValue = 0
  };

}  // namespace codelab
}  // namespace sim
}  // namespace mpact

#endif  // RISCV32I_ENUMS_H

보시다시피 riscv32i.isa 파일에 정의된 각 슬롯, 명령 코드, 피연산자는 열거형 중 하나로 정의됩니다. 또한 모든 opcode 이름을 저장하는 OpcodeNames 배열이 있습니다(riscv32i_enums.cc에 정의됨). 다른 파일에는 생성된 디코더가 포함되어 있으며 이는 다른 튜토리얼에서 자세히 다룹니다.

Bazel 빌드 규칙

Bazel의 ISA 디코더 타겟은 mpact-sim 저장소의 mpact/sim/decoder/mpact_sim_isa.bzl에서 로드되는 mpact_isa_decoder라는 맞춤 규칙 매크로를 사용하여 정의됩니다. 이 튜토리얼에서 riscv_isa_decoder/BUILD에 정의된 빌드 타겟은 다음과 같습니다.

mpact_isa_decoder(
    name = "riscv32i_isa",
    src = "riscv32i.isa",
    includes = [],
    isa_name = "RiscV32I",
    deps = [
        "//riscv_semantic_functions:riscv32i",
    ],
)

이 규칙은 ISA 파서 도구와 생성기를 호출하여 C++ 코드를 생성한 다음 생성된 코드를 //riscv_isa_decoder:riscv32i_isa 라벨을 사용하여 다른 규칙에 종속될 수 있는 라이브러리에 컴파일합니다. includes 섹션은 소스 파일에 포함될 수 있는 추가 .isa 파일을 지정하는 데 사용됩니다. isa_name는 디코더를 생성할 소스 파일에서 하나 이상 지정된 경우 필요한 특정 isa를 지정하는 데 사용됩니다.


레지스터-레지스터 ALU 명령 추가

이제 riscv32i.isa 파일에 몇 가지 새로운 안내를 추가해 보겠습니다. 첫 번째 명령 그룹은 add, and와 같은 레지스터-레지스터 ALU 명령입니다. RiscV32에서는 모두 R 유형 바이너리 명령 형식을 사용합니다.

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 rs2 rs1 func3 rd 명령 코드

.isa 파일은 형식에 구애받지 않는 디코더를 생성하는 데 사용되지만 바이너리 형식과 그 레이아웃을 고려하여 항목을 안내하는 것도 여전히 유용합니다. 보시다시피 명령어 객체를 채우는 디코더와 관련된 세 가지 필드(rs2, rs1, rd)가 있습니다. 이 시점에서 모든 명령의 동일한 명령 필드에서 동일한 방식 (비트 시퀀스)으로 인코딩된 정수 레지스터에 이 이름을 사용하도록 선택합니다.

추가할 안내는 다음과 같습니다.

  • add - 정수 덧셈.
  • and - 비트 AND.
  • or - 비트 OR입니다.
  • sll - 왼쪽으로 논리 이동
  • sltu - 보다 작음, 부호 없음으로 설정합니다.
  • sub - 정수 뺄셈.
  • xor - 비트 xor.

이러한 각 안내는 riscv32i 슬롯 정의의 opcodes 섹션에 추가됩니다. 각 명령어의 이름, opcode, 디스어셈블리, 시맨틱 함수를 지정해야 합니다. 이름은 간단하므로 위의 명령 코드 이름을 사용하겠습니다 또한 모두 동일한 피연산자를 사용하므로 피연산자 사양에 { : rs1, rs2 : rd}를 사용할 수 있습니다. 즉, rs1에 의해 지정된 레지스터 소스 피연산자는 명령 객체의 소스 피연산자 벡터에 색인 0이 있고, rs2에 의해 지정된 레지스터 소스 피연산자의 색인은 1이며, rd가 지정한 레지스터 대상 피연산자는 색인 0의 대상 피연산자 벡터에서 유일한 요소가 됩니다.

다음은 시맨틱 함수 사양입니다. 이는 semfunc 키워드와 std::function에 할당하는 데 사용할 수 있는 호출 가능한 함수를 지정하는 C++ 문자열을 사용하여 실행됩니다. 이 튜토리얼에서는 함수를 사용하므로 호출 가능한 문자열은 "&MyFunctionName"입니다. fence 안내에서 제안한 이름 지정 스키마를 사용하면 "&RV32IAdd", "&RV32IAnd" 등이 됩니다.

마지막은 분해 사양입니다. 키워드 disasm로 시작하고 그 뒤에 명령을 문자열로 인쇄해야 하는 방법을 지정하는 쉼표로 구분된 문자열 목록이 옵니다. 피연산자 이름 앞에 % 기호를 사용하면 해당 피연산자의 문자열 표현을 사용한 문자열 대체를 나타냅니다. add 명령어의 경우 disasm: "add", "%rd, %rs1,%rs2"입니다. 즉, add 명령어의 항목은 다음과 같이 표시되어야 합니다.

    add{ : rs1, rs2 : rd},
      semfunc: "&RV32IAdd",
      disasm: "add", "%rd, %rs1, %rs2";

riscv32i.isa 파일을 수정하고 이러한 모든 안내를 .isa 설명에 추가합니다. 도움이 필요하거나 작업을 확인하려면 여기에서 전체 설명 파일을 확인하세요.

명령어가 riscv32i.isa 파일에 추가되면 `../semantic_functions/에 있는 rv32i_instructions.h 파일에 참조된 각 새 시맨틱 함수의 함수 선언을 추가해야 합니다. 도움이 필요하거나 작업을 확인하려면 여기에서 확인하세요.

이 작업이 완료되면 riscv_isa_decoder 디렉터리로 다시 변경하고 다시 빌드합니다. 생성된 소스 파일을 자유롭게 검사할 수 있습니다.


즉시 입력으로 ALU 안내 추가

다음으로 추가할 명령어 세트는 레지스터 중 하나가 아닌 즉시 값을 사용하는 ALU 명령어입니다. 이러한 명령어에는 세 가지 그룹이 있습니다(즉시 필드를 기반으로 함). 12비트 부호 있는 즉시 값이 있는 I-Type 즉시 명령어, 전환을 위한 특수화된 I-Type 즉시 명령어, 20비트 부호 없는 즉시 값이 있는 U-Type 즉시 명령어입니다. 형식은 다음과 같습니다.

I-Type 즉시 형식:

2020년 31월 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd 명령 코드

특수화된 I-Type 즉시 형식:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd 명령 코드

U-Type 즉시 형식:

31..12 11~7 6..0
20 5 7
uimm20 번째 명령 코드

보시다시피 피연산자 이름 rs1rd는 이전과 동일한 비트 필드를 참조하며 정수 레지스터를 나타내는 데 사용되므로 이러한 이름을 유지할 수 있습니다. 즉시 값 필드는 길이와 위치가 다르며, 두 필드(uimm5uimm20)는 부호가 없는 반면 imm12는 부호가 있습니다. 각각 자체 이름을 사용합니다.

따라서 I-Type 명령어의 피연산자는 { : rs1, imm12 :rd }여야 합니다. 특수한 I-Type 명령어의 경우 { : rs1, uimm5 : rd}이어야 합니다. U-Type 명령 피연산자 사양은 { : uimm20 : rd }여야 합니다.

추가해야 하는 I-Type 안내는 다음과 같습니다.

  • addi - 즉시 추가합니다.
  • andi - 비트 및 즉시.
  • ori - 비트 OR(즉시 포함)
  • xori - 즉시 값을 사용한 비트 XOR

추가해야 하는 특수 I-Type 명령은 다음과 같습니다.

  • slli - 즉시 왼쪽 논리를 왼쪽으로 이동합니다.
  • srai - 즉시 오른쪽 산술을 이동합니다.
  • srli - 즉시 오른쪽으로 오른쪽으로 이동합니다.

추가해야 하는 U-Type 안내는 다음과 같습니다.

  • auipc - pc에 상위 즉시 추가
  • lui - 상위 즉시 로드

명령 코드에 사용할 이름은 위의 명령어 이름을 자연스럽게 따릅니다 (모두 고유하므로 새로 만들 필요가 없음). 시맨틱 함수를 지정할 때 명령어 객체가 기본 피연산자 유형에 구애받지 않는 소스 피연산자에 대한 인터페이스를 인코딩한다는 점을 기억하세요. 즉, 동일한 연산을 실행하지만 피연산자 유형이 다를 수 있는 명령어의 경우 동일한 시맨틱 함수를 공유할 수 있습니다. 예를 들어 addi 명령어는 피연산자 유형을 무시하면 add 명령어와 동일한 작업을 실행하므로 동일한 시맨틱 함수 사양 "&RV32IAdd"를 사용할 수 있습니다. andi, ori, xori, slli도 마찬가지입니다. 다른 명령어는 새 시맨틱 함수를 사용하지만 피연산자가 아닌 연산을 기반으로 이름을 지정해야 하므로 srai의 경우 "&RV32ISra"를 사용합니다. U 유형 명령어 auipclui에는 레지스터 등가 항목이 없으므로 "&RV32IAuipc""&RV32ILui"를 사용해도 됩니다.

디스어셈블리 문자열은 이전 실습의 문자열과 매우 비슷하지만 %rs2에 대한 참조는 필요에 따라 %imm12, %uimm5 또는 %uimm20로 대체됩니다.

변경사항을 적용하고 빌드합니다. 생성된 출력을 확인합니다. 이전과 마찬가지로 riscv32i.isarv32i_instructions.h에 대해 작업을 확인할 수 있습니다.


브랜치와 점프 및 링크 명령어는 모두 명령어 자체에만 암시되는 대상 피연산자, 즉 다음 pc 값을 사용합니다. 이 단계에서는 이를 이름이 next_pc인 적절한 피연산자로 취급합니다. 이는 이후 튜토리얼에서 자세히 정의됩니다.

브랜치 안내

추가하는 브랜치는 모두 B-Type 인코딩을 사용합니다.

31 2025년 30월 25일 24..20 19..15 14..12 11..8 7 6..0
1 6 5 5 3 4 1 7
imm imm rs2 rs1 func3 imm imm 명령 코드

서로 다른 즉시 필드는 12비트 부호 있는 즉시 값으로 연결됩니다. 형식이 실제로 관련이 없으므로 12비트 브랜치의 경우 즉시 bimm12를 호출합니다. 단편화는 바이너리 디코더 만들기에 관한 다음 가이드에서 다룹니다. 모든 분기 명령어는 rs1 및 rs2로 지정된 정수 레지스터를 비교합니다. 조건이 참이면 즉시 값이 현재 pc 값에 추가되어 실행할 다음 명령어의 주소가 생성됩니다. 따라서 브랜치 명령어의 피연산자는 { : rs1, rs2, bimm12 : next_pc }여야 합니다.

추가해야 하는 브랜치 명령어는 다음과 같습니다.

  • beq - 같으면 브랜치
  • bge - 크거나 같은 경우 브랜치
  • bgeu - 부호가 없는 경우보다 크거나 같은 경우 분기합니다.
  • blt - 미만이면 분기합니다.
  • bltu - 부호 없는 경우보다 작은 경우 분기합니다.
  • bne - 같지 않으면 브랜치합니다.

이러한 명령 코드 이름은 모두 고유하므로 .isa 설명에서 재사용할 수 있습니다. 물론 새로운 시맨틱 함수 이름을 추가해야 합니다(예: "&RV32IBeq"

디스어셈블리 사양은 이제 좀 더 까다로워졌습니다. 명령어 주소가 실제로 명령어 피연산자의 일부가 아니어도 대상을 계산하는 데 사용되기 때문입니다. 그러나 명령어 객체에 저장된 정보의 일부이므로 사용할 수 있습니다. 해결 방법은 분해 문자열에 표현식 구문을 사용하는 것입니다. 피연산자 이름 앞에 '%'를 사용하는 대신 %(expression: print format)을 입력할 수 있습니다. 매우 간단한 표현식만 지원되지만 주소와 오프셋도 그중 하나이며 현재 명령어 주소에 @ 기호가 사용됩니다. 인쇄 형식은 C 스타일 printf 형식과 비슷하지만 앞에 %가 없습니다. 그러면 beq 명령어의 디스어셈블리 형식은 다음과 같이 됩니다.

    disasm: "beq", "%rs1, %rs2, %(@+bimm12:08x)"

jal(점프 및 연결) 및 jalr(간접 점프 및 연결)의 두 가지 점프 및 연결 명령어만 추가하면 됩니다.

jal 명령은 J 유형 인코딩을 사용합니다.

31 30..21 20 19..12 11..7 6..0
1 10 1 8 5 7
imm imm imm imm rd 명령 코드

브랜치 명령어와 마찬가지로 20비트 즉시는 여러 필드에 걸쳐 분할되므로 jimm20로 이름을 지정합니다. 현재는 단편화가 중요하지 않지만 바이너리 디코더 만들기에 관한 다음 튜토리얼에서 다룹니다. 그러면 피연산자 사양은 { : jimm20 : next_pc, rd }가 됩니다. 두 개의 대상 피연산자, 즉 다음 pc 값과 명령에 지정된 링크 레지스터가 있습니다.

위의 브랜치 명령과 마찬가지로 디스어셈블리 형식은 다음과 같습니다.

    disasm: "jal", "%rd, %(@+jimm20:08x)"

간접 점프 및 링크는 12비트 즉시 입력과 함께 I-Type 형식을 사용합니다. 부호 확장 즉시 값을 rs1로 지정된 정수 레지스터에 추가하여 타겟 명령어 주소를 생성합니다. 링크 레지스터는 rd에 의해 지정된 정수 레지스터입니다.

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd 명령 코드

패턴을 확인한 후에는 jalr의 피연산자 사양이 { : rs1, imm12 : next_pc, rd }이고 역어셈블리 사양은 다음과 같다고 추론할 수 있습니다.

    disasm: "jalr", "%rd, %rs1, %imm12"

변경한 다음 빌드하세요. 생성된 출력을 확인합니다. 이전과 마찬가지로 riscv32i.isarv32i_instructions.h를 기준으로 작업을 확인할 수 있습니다.


매장 안내 추가

매장 안내는 매우 간단합니다. 모두 S-Type 형식을 사용합니다.

2025년 31월 25일 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
imm rs2 rs1 func3 imm 명령 코드

보시다시피 이는 프래그먼트화된 12비트 즉시의 또 다른 사례입니다. 이것을 simm12라고 하겠습니다. 저장 명령어는 모두 rs2로 지정된 정수 레지스터의 값을 rs1로 지정된 정수 레지스터의 값을 12비트 즉시 값의 부호 확장 값에 더하여 얻은 메모리의 유효 주소에 저장합니다. 모든 스토어 명령어의 피연산자 형식은 { : rs1, simm12, rs2 }이어야 합니다.

구현해야 하는 스토어 안내는 다음과 같습니다.

  • sb - 저장 바이트입니다.
  • sh - 반단어를 저장합니다.
  • sw - 단어 저장.

sb의 디스어셈블리 사양은 다음과 같습니다.

    disasm: "sb", "%rs2, %simm12(%rs1)"

시맨틱 함수 사양도 예상과 같습니다(예: "&RV32ISb").

변경한 후 빌드합니다. 생성된 출력을 확인합니다. 이전과 마찬가지로 riscv32i.isarv32i_instructions.h를 기준으로 작업을 확인할 수 있습니다.


로드 안내 추가

로드 명령어는 시뮬레이터의 다른 명령어와 약간 다르게 모델링됩니다. 로드 지연 시간이 불확실한 사례를 모델링할 수 있도록 로드 명령어는 1) 유효 주소 계산 및 메모리 액세스, 2) 결과 리턴백이라는 두 가지 작업으로 나뉩니다. 시뮬레이터에서는 로드의 의미적 작업을 기본 명령과 하위 명령, 이렇게 두 개의 개별 명령으로 분할하여 이를 수행합니다. 또한 피연산자를 지정할 때는 main 명령어와 child 명령어 모두에 피연산자를 지정해야 합니다. 이는 피연산자 사양을 3개 항목의 목록으로 취급하여 실행됩니다. 구문은 다음과 같습니다.

{(predicate : sources : destinations), (predicate : sources : destinations), ... }

로드 안내에서는 모두 이전 안내와 마찬가지로 I-Type 형식을 사용합니다.

2020년 31월 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd 명령 코드

피연산자 사양은 주소를 계산하는 데 필요한 피연산자를 분할하고 로드 데이터의 레지스터 대상에서 메모리 액세스를 시작합니다({( : rs1, imm12 : ), ( : : rd) }).

시맨틱 작업은 두 명령어로 분할되므로 시맨틱 함수도 마찬가지로 두 개의 호출 가능 항목을 지정해야 합니다. lw(단어 로드)의 경우 다음과 같이 작성됩니다.

    semfunc: "&RV32ILw", "&RV32ILwChild"

디스어셈블리 사양은 더 일반적입니다. 하위 안내는 언급되지 않습니다. lw의 경우 다음과 같아야 합니다.

    disasm: "lw", "%rd, %imm12(%rs1)"

구현해야 하는 로드 안내는 다음과 같습니다.

  • lb - 바이트 로드
  • lbu - 부호 없는 바이트 로드
  • lh - 하프워드 로드.
  • lhu - 부호 없는 절반 단어 로드
  • lw - 단어를 로드합니다.

변경한 후 빌드합니다. 생성된 출력을 확인합니다. 이전과 마찬가지로 riscv32i.isarv32i_instructions.h를 기준으로 작업을 확인할 수 있습니다.

문의해 주셔서 감사합니다. 이 정보가 도움이 되었기를 바랍니다.