이 가이드의 목표는 다음과 같습니다.
- 시맨틱 함수를 사용하여 명령 시맨틱스를 구현하는 방법을 알아봅니다.
- 시맨틱 함수가 ISA 디코더 설명과 어떤 관련이 있는지 알아봅니다.
- RiscV RV32I 명령어의 명령어 시맨틱 함수를 작성합니다.
- 작은 'Hello World'를 실행하여 최종 시뮬레이터 테스트 있습니다.
시맨틱 함수 개요
MPACT-Sim의 시맨틱 함수는 작업을 구현하는 함수입니다. 시뮬레이션된 상태에서 부작용이 표시되도록 명령의 부작용이 사용할 수 있습니다 디코딩된 각 명령에 대한 시뮬레이터의 내부 표현 관련 시맨틱 함수를 호출하는 데 사용되는 호출 가능 함수를 포함합니다. 지시사항입니다.
시맨틱 함수에는 서명 void(Instruction *)
가 있습니다. 즉,
Instruction
클래스의 인스턴스를 가리키는 포인터를 사용하고
void
를 반환합니다.
Instruction
클래스는 다음에 정의되어 있습니다.
instruction.h
특히 관심 있는 시맨틱 함수를 작성할 때는
소스 및 대상 피연산자 인터페이스 벡터를
Source(int i)
및 Destination(int i)
메서드 호출
소스 및 대상 피연산자 인터페이스는 아래와 같습니다.
// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
public:
// Methods for accessing the nth value element.
virtual bool AsBool(int index) = 0;
virtual int8_t AsInt8(int index) = 0;
virtual uint8_t AsUint8(int index) = 0;
virtual int16_t AsInt16(int index) = 0;
virtual uint16_t AsUint16(int) = 0;
virtual int32_t AsInt32(int index) = 0;
virtual uint32_t AsUint32(int index) = 0;
virtual int64_t AsInt64(int index) = 0;
virtual uint64_t AsUint64(int index) = 0;
// Return a pointer to the object instance that implements the state in
// question (or nullptr) if no such object "makes sense". This is used if
// the object requires additional manipulation - such as a fifo that needs
// to be pop'ed. If no such manipulation is required, nullptr should be
// returned.
virtual std::any GetObject() const = 0;
// Return the shape of the operand (the number of elements in each dimension).
// For instance {1} indicates a scalar quantity, whereas {128} indicates an
// 128 element vector quantity.
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
virtual ~SourceOperandInterface() = default;
};
// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
public:
virtual ~DestinationOperandInterface() = default;
// Allocates a data buffer with ownership, latency and delay line set up.
virtual DataBuffer *AllocateDataBuffer() = 0;
// Takes an existing data buffer, and initializes it for the destination
// as if AllocateDataBuffer had been called.
virtual void InitializeDataBuffer(DataBuffer *db) = 0;
// Allocates and initializes data buffer as if AllocateDataBuffer had been
// called, but also copies in the value from the current value of the
// destination.
virtual DataBuffer *CopyDataBuffer() = 0;
// Returns the latency associated with the destination operand.
virtual int latency() const = 0;
// Return a pointer to the object instance that implmements the state in
// question (or nullptr if no such object "makes sense").
virtual std::any GetObject() const = 0;
// Returns the order of the destination operand (size in each dimension).
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
};
일반 3 피연산자에 관한 시맨틱 함수를 작성하는 기본적인 방법
명령(예: 32비트 add
명령)은 다음과 같습니다.
void MyAddFunction(Instruction *inst) {
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
uint32_t c = a + b;
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, c);
db->Submit();
}
이 함수의 각 부분을 분해해 보겠습니다. 아래의 첫 두 줄은
함수 본문은 소스 피연산자 0과 1에서 읽습니다. AsUint32(0)
호출
기본 데이터를 uint32_t
배열로 해석하여 0번째
요소가 포함됩니다. 이는 기본 레지스터 또는 값이
배열의 값인지 여부를 지정합니다. 소스 피연산자의 크기 (요소)는
벡터를 반환하는 소스 피연산자 메서드 shape()
에서 얻은 값
각 차원에 있는 요소의 수를 포함합니다. 이 메서드는 {1}
를 반환합니다.
16 요소 벡터의 경우 {16}
, 4x4 배열의 경우 {4, 4}
입니다.
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
그런 다음 c
라는 임시 uint32_t
에 a + b
값이 할당됩니다.
다음 줄에는 약간의 추가 설명이 필요할 수 있습니다.
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
DataBuffer는 참조 계산 객체로서 값을
시뮬레이션된 상태가 됩니다. 이 속성은
객체에 따라 다릅니다. 이 경우 해당 크기는
sizeof(uint32_t)
이 문은
이 대상 피연산자의 대상이 되는 대상 - 이 경우에는
32비트 정수 레지스터. DataBuffer 또한 다음과 같이 초기화됩니다.
아키텍처 지연 시간을 단축할 수 있습니다 명령어 중에 지정됩니다.
디코딩합니다.
다음 줄은 데이터 버퍼 인스턴스를 uint32_t
의 배열로 취급하고
c
에 저장된 값을 0번째 요소에 씁니다.
db->Set<uint32_t>(0, c);
마지막으로 마지막 문은 사용할 데이터 버퍼를 시뮬레이터에 제출합니다. 대상 머신 상태 (이 경우에는 레지스터)의 새 값으로 명령이 디코딩되었을 때 설정된 명령의 지연 시간 및 대상 피연산자 벡터가 채워져 있습니다.
상당히 간단한 함수이지만 약간의 상용구가 있습니다. 명령 뒤에 명령을 구현할 때 반복되는 코드가 될 수 있습니다. 또한 명령어의 실제 의미 체계를 모호하게 만들 수도 있습니다. 순서 대부분의 명령에 대한 시맨틱 함수 작성을 더욱 단순화하기 위해 이 클래스에는 다양한 템플릿 도우미 함수가 instruction_helpers.h 이러한 도우미는 1, 2, 3이 포함된 명령에 대한 상용구 코드를 숨깁니다. 소스 피연산자, 단일 대상 피연산자입니다. 이제 두 가지 옵션을 피연산자 도우미 함수:
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument1, Argument2)> operation) {
Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument, Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Result, Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
다음과 같은 명령문을 사용하는 대신
uint32_t a = inst->Source(0)->AsUint32(0);
도우미 함수는 다음을 사용합니다.
generic::GetInstructionSource<Argument>(instruction, 0);
GetInstructionSource
는 템플릿화된 도우미 함수 제품군으로,
안내 소스에 템플릿화된 액세스 메서드를 제공하는 데 사용됩니다.
피연산자. 그것이 없으면 각 명령 도우미 함수는
소스 피연산자에 액세스할 수 있도록 각 유형마다
As<int type>()
함수를 사용하세요. 이러한 템플릿의 정의를 확인하려면
함수
instruction.h
보시다시피 소스가 있는지 여부에 따라 세 가지 구현이 있습니다.
피연산자 유형은 대상과 동일하며 대상이
원본과 다른지, 아니면 모두 다른지 여부를 결정합니다. 각 버전의
함수는 명령 인스턴스에 대한 포인터와 호출 가능
(람다 함수 포함) 즉, 이제 add
를 다시 작성할 수 있습니다.
다음 시맨틱 함수를 작성합니다.
void MyAddFunction(Instruction *inst) {
generic::BinaryOp<uint32_t>(inst,
[](uint32_t a, uint32_t b) { return a + b; });
}
빌드에서 bazel build -c opt
및 copts = ["-O3"]
로 컴파일하는 경우
오버헤드 없이 완벽하게 인라인되어야 하므로
간결성을 유지합니다.
앞서 언급했듯이 단항, 바이너리, 3항 스칼라를 위한 도우미 함수가 있습니다. 명령 및 벡터 등가물입니다. 또한 맞춤 광고 단위를 위한 를 참조하세요.
초기 빌드
디렉터리를 riscv_semantic_functions
로 변경하지 않았다면 변경합니다.
있습니다. 그런 다음 다음과 같이 프로젝트를 빌드합니다. 이렇게 하면 빌드가 성공합니다.
$ bazel build :riscv32i
...<snip>...
생성되는 파일이 없기 때문에 연습 실행일 뿐입니다. 모든 것이 정상인지 확인합니다
3개의 피연산자 ALU 명령 추가
이제 일반적인 3-피연산자 ALU를 위한 시맨틱 함수를 추가해 보겠습니다.
참조하세요. rv32i_instructions.cc
파일을 열고
누락된 정의는 rv32i_instructions.h
파일에 추가됩니다.
추가할 지침은 다음과 같습니다.
add
- 32비트 정수 더하기입니다.and
- 32비트 비트 AND.or
- 32비트 비트 OR입니다.sll
- 32비트 왼쪽으로 논리 이동.sltu
- 32비트 부호 없는 세트보다 작음.sra
- 32비트 산술 오른쪽 시프트.srl
- 32비트 논리 오른쪽 시프트.sub
- 32비트 정수 뺄셈.xor
- 32비트 비트 xor.
이전 튜토리얼을 완료했다면 레지스터-등록 명령과 레지스터 즉시 명령의 차이점을 하위 클래스입니다. 시맨틱 함수의 경우 더 이상 그럴 필요가 없습니다. 피연산자 인터페이스는 피연산자로부터 피연산자 값을 읽습니다. 의미론적 함수는 어떤 의미인지에 대해 전혀 구속받지 않고 무엇인지 배웠습니다.
sra
을(를) 제외한 위의 모든 명령은 다음에서 작동하는 것으로 간주할 수 있습니다.
부호 없는 32비트 값이므로 BinaryOp
템플릿 함수를 사용할 수 있음
단일 템플릿 유형 인수로만 앞에서 살펴봤습니다.
rv32i_instructions.cc
에 함수 본문을 적절하게 추가해야 합니다. 하위 5개만
시프트 명령에 대한 두 번째 피연산자의 비트가
있습니다. 그 외의 경우에는 모든 연산이 src0 op src1
형식입니다.
add
:a + b
and
:a & b
or
:a | b
sll
:a << (b & 0x1f)
sltu
:(a < b) ? 1 : 0
srl
:a >> (b & 0x1f)
sub
:a - b
xor
:a ^ b
sra
에는 세 개의 인수 BinaryOp
템플릿을 사용합니다. 이
템플릿의 첫 번째 유형 인수는 결과 유형 uint32_t
입니다. 두 번째는
소스 피연산자 유형 0(이 경우 int32_t
)이며 마지막은 유형입니다.
소스 피연산자 1의 값(이 경우 uint32_t
)입니다. 이렇게 하면 sra
의 본문이
시맨틱 함수:
generic::BinaryOp<uint32_t, int32_t, uint32_t>(
instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });
계속 진행하여 변경하고 빌드합니다. 다른 애플리케이션과 비교하여 rv32i_instructions.cc
두 개의 피연산자 ALU 명령 추가
2개의 피연산자 ALU 명령, 즉 lui
와 auipc
만 있습니다. 이전
사전에 이동한 소스 피연산자를 대상에 직접 복사합니다. 후자
는
있습니다. address()
메서드에서 안내 주소에 액세스할 수 있습니다.
(Instruction
객체의 정의)
소스 피연산자가 하나뿐이므로 BinaryOp
를 대신 사용할 수 없습니다.
UnaryOp
를 사용해야 합니다. 소스와
대상 피연산자를 uint32_t
로 설정하고 단일 인수 템플릿을 사용할 수 있음
있습니다.
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
lui
에 관한 시맨틱 함수의 본문은 최대한 간단합니다.
소스를 반환하기만 하면 됩니다. auipc
의 시맨틱 함수는 마이너
Instruction
에서 address()
메서드에 액세스해야 하므로
인스턴스를 만들 수 있습니다 정답은 람다 캡처에 instruction
를 추가하여
람다 함수 본문에서 사용할 수 있습니다. 이전과 같이 [](uint32_t a) { ...
}
대신 람다를 [instruction](uint32_t a) { ... }
로 작성해야 합니다.
이제 람다 본문에서 instruction
를 사용할 수 있습니다.
계속 진행하여 변경하고 빌드합니다. 다른 애플리케이션과 비교하여 rv32i_instructions.cc
제어 흐름 변경 안내 추가
구현해야 하는 제어 흐름 변경 지침은 조건부 브랜치 명령( 비교도 가능), 점프 앤 링크 명령은 함수 호출을 구현합니다 (링크는 0으로 등록하여 쓰기가 노옵스(no-ops)입니다.
조건부 브랜치 안내 추가
브랜치 명령에는 도우미 함수가 없으므로 두 가지 옵션이 있습니다. 시맨틱 함수를 처음부터 작성하거나 로컬 도우미 함수를 작성합니다. 6개의 브랜치 명령을 구현해야 하므로 브랜치 명령이 노력할 것입니다. 그 전에 브랜치의 구현을 살펴보겠습니다. 명령 시맨틱 함수를 처음부터 만듭니다.
void MyConditionalBranchGreaterEqual(Instruction *instruction) {
int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
if (a >= b) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0,m target);
db->Submit();
}
}
브랜치 명령에 따라 달라지는 유일한 것은 브랜치
둘 중 부호 있는 32비트 정수와 부호 없는 32비트 정수의 데이터 유형
소스 피연산자. 즉, 피처스토어의 생성에 대한
소스 피연산자. 도우미 함수 자체는 Instruction
를 사용해야 합니다.
인스턴스 및 bool
를 반환하는 std::function
와 같은 호출 가능한 객체
매개변수로 전달하세요. 도우미 함수는 다음과 같습니다.
template <typename OperandType>
static inline void BranchConditional(
Instruction *instruction,
std::function<bool(OperandType, OperandType)> cond) {
OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
if (cond(a, b)) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, target);
db->Submit();
}
}
이제 bge
(부호 있는 브랜치 이상) 시맨틱 함수를 작성할 수 있습니다.
방향:
void RV32IBge(Instruction *instruction) {
BranchConditional<int32_t>(instruction,
[](int32_t a, int32_t b) { return a >= b; });
}
나머지 브랜치 안내는 다음과 같습니다.
- Beq - 브랜치가 같음.
- Bgeu - 브랜치 크거나 같음 (부호 없음).
- Blt - 브랜치 작음 (부호 있음).
- Bltu - 브랜치 미만 (부호 없음)
- Bne - 분기가 같지 않음.
이러한 시맨틱 함수를 구현하기 위해 변경합니다. 있습니다. 다른 애플리케이션과 비교하여 rv32i_instructions.cc
이동 및 링크 지침 추가
점프 및 링크에 관한 도우미 함수를 작성하는 것은 무의미함 따라서 처음부터 작성해야 합니다. 먼저 명령의 의미를 살펴보는 것입니다.
jal
명령어는 소스 피연산자 0에서 오프셋을 가져와서
현재 pc (명령 주소)로 이동하여 점프 대상을 계산합니다. 점프 타겟
대상 피연산자 0에 작성됩니다. 반품 주소는
순차적으로 전달될 수 있습니다. 현재 셀에 현재 값을 더해서
명령의 크기를 해당 주소로 변환합니다. 반품 주소는
대상 피연산자 1. 명령 객체 포인터를
람다 캡처
jalr
명령어는 기본 레지스터를 소스 피연산자 0 및 오프셋으로 사용합니다.
를 소스 피연산자 1로 설정하고, 함께 더하여 점프 타겟을 계산합니다.
그 외에는 jal
명령어와 동일합니다.
명령어 시맨틱스에 관한 이러한 설명에 따라 빌드하고 사용할 수 있습니다. 다른 애플리케이션과 비교하여 rv32i_instructions.cc
메모리 저장 안내 추가
구현해야 하는 세 가지 저장 명령은 저장 바이트입니다.
(sb
), 하프워드 저장 (sh
), 저장 워드 (sw
) 매장 안내
우리가 지금까지 구현한 지침과 다른 점은
로컬 프로세서 상태에 씁니다 대신 시스템 리소스에 씁니다.
기본 메모리로 변환합니다 MPACT-Sim은 메모리를 명령 피연산자로 취급하지 않습니다.
다른 방법을 사용하여 메모리 액세스를 수행해야 합니다.
답은 MPACT-Sim ArchState
객체에 메모리 액세스 메서드를 추가하는 것입니다.
또는 ArchState
에서 파생되는 새 RiscV 상태 객체를 만듭니다.
여기에 새 이름을 추가할 수 있습니다 ArchState
객체는 핵심 리소스를 관리합니다.
레지스터 및 기타 상태 객체를 정의합니다. 또한 디코더에 사용되는 지연선을 관리하여
다시 쓰여질 수 있을 때까지 대상 피연산자 데이터 버퍼를 버퍼링
레지스터 객체입니다. 대부분의 명령은 인코더-디코더 아키텍처를
메모리 연산이나 다른 특정 시스템과 같은 일부 클래스는
명령이 이 상태 객체에 상주해야 합니다.
이제 fence
명령어의 시맨틱 함수를 살펴보겠습니다.
예를 들어 rv32i_instructions.cc
에 이미 구현되어 있습니다. fence
명령은 특정 메모리 연산이
완료되었습니다. 명령 간의 메모리 순서를 보장하는 데 사용됩니다.
명령 이전에 실행되는 것과 명령 후에 실행되는 것들이 있습니다.
// Fence.
void RV32IFence(Instruction *instruction) {
uint32_t bits = instruction->Source(0)->AsUint32(0);
int fm = (bits >> 8) & 0xf;
int predecessor = (bits >> 4) & 0xf;
int successor = bits & 0xf;
auto *state = static_cast<RiscVState *>(instruction->state());
state->Fence(instruction, fm, predecessor, successor);
}
fence
명령어의 시맨틱 함수의 핵심 부분은 마지막 두 가지입니다.
있습니다. 먼저 Instruction
의 메서드를 사용하여 상태 객체를 가져옵니다.
클래스 및 downcast<>
를 RiscV 특정 파생 클래스에 연결합니다. 이후 Fence
RiscVState
클래스의 메서드가 호출되어 펜스 작업을 실행합니다.
매장 안내는 이와 유사하게 작동합니다. 먼저
메모리 액세스는 기본 및 오프셋 명령 소스 피연산자에서 계산되며,
저장할 값을 다음 소스 피연산자에서 가져옵니다. 다음으로
RiscV 상태 객체는 state()
메서드 호출을 통해 획득되며
static_cast<>
를 호출하고 적절한 메서드가 호출됩니다.
RiscVState
객체 StoreMemory
메서드는 비교적 단순하지만
다음과 같은 두 가지 의미가 있습니다.
void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);
보시다시피 이 메서드는 세 개의 매개변수, 즉 저장소를 가리키는 포인터를 받습니다.
명령 자체, 스토어의 주소, DataBuffer
를 가리키는 포인터
매장 데이터가 포함된 인스턴스입니다. 크기는 필요하지 않으며,
DataBuffer
인스턴스 자체에는 size()
메서드가 포함됩니다. 하지만 이와 관련된
명령에 액세스할 수 있는 대상 피연산자로서
적절한 크기의 DataBuffer
인스턴스를 할당합니다. 대신
다음의 db_factory()
메서드에서 가져온 DataBuffer
팩토리를 사용하세요.
Instruction
인스턴스 팩토리에는 Allocate(int size)
메서드가 있습니다.
필요한 크기의 DataBuffer
인스턴스를 반환합니다. 예를 들면 다음과 같습니다.
이를 사용하여 반단어 저장소에 DataBuffer
인스턴스를 할당하는 방법
(auto
는 오른쪽에서 유형을 추론하는 C++ 기능입니다.
)을 입력합니다.
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(uint16_t));
DataBuffer
인스턴스를 갖게 되면 평소처럼 이 인스턴스에 쓸 수 있습니다.
db->Set<uint16_t>(0, value);
그런 다음 메모리 저장소 인터페이스에 전달합니다.
state->StoreMemory(instruction, address, db);
아직 완성되지 않았습니다. DataBuffer
인스턴스는 참조로 계산됩니다. 이
는 일반적으로 Submit
메서드에서 이해되고 처리되므로
사용 사례를 최대한 단순하게 만듭니다. 하지만 StoreMemory
는
그렇게 쓸 수 있습니다. 작동하는 동안 DataBuffer
인스턴스를 IncRef
합니다.
그런 다음 완료되면 DecRef
를 호출합니다. 그러나 시맨틱 함수가
DecRef
자체 참조가 복원되지 않습니다. 따라서 마지막 줄에
다음과 같아야 합니다.
db->DecRef();
세 가지 저장 함수가 있는데, 유일한 차이점은 데이터의 크기입니다.
도움이 될 수 있습니다 다른 현지인에게 좋은 기회인 것 같음
템플릿 도우미 함수를 사용합니다. 저장 함수에서 한 가지 다른 점은
매장 값의 유형이므로 템플릿은 이를 인수로 가져야 합니다.
그 외에는 Instruction
인스턴스만 전달해야 합니다.
template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->Set<ValueType>(0, value);
state->StoreMemory(instruction, address, db);
db->DecRef();
}
계속해서 스토어 시맨틱 함수와 빌드를 마무리합니다. 다음에서 확인할 수 있습니다. 반대 rv32i_instructions.cc
메모리 로드 안내 추가
구현해야 하는 로드 명령어는 다음과 같습니다.
lb
- 바이트를 로드하고, 단어로 부호 확장.lbu
- 부호 없는 바이트를 로드하며, 0 확장 프로그램을 단어로 확장합니다.lh
: 반단어를 로드하고 부호 확장 단어를 단어로 확장합니다.lhu
- 부호 없는 반워드(0 확장)를 단어로 로드합니다.lw
- 단어 로드
로드 지침은 모델링해야 하는 가장 복잡한 명령입니다.
이 튜토리얼에서 확인할 수 있습니다. 이러한 모델은 일종의 작업 중 하나를 처리해야 한다는 점에서
RiscVState
객체에 액세스하지만 각 로드가
명령어는 두 개의 개별 시맨틱 함수로 나뉩니다 첫 번째는
유효한 주소를 계산한다는 점에서 저장 명령과 유사합니다.
메모리 액세스를 시작합니다. 두 번째는 메모리가
완료되면 메모리 데이터를 레지스터 대상에 씁니다.
피연산자입니다.
먼저 RiscVState
의 LoadMemory
메서드 선언을 살펴보겠습니다.
void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
Instruction *child_inst, ReferenceCount *context);
StoreMemory
메서드와 비교할 때 LoadMemory
는 두 가지
매개변수: Instruction
인스턴스를 가리키는 포인터와
참조된 context
객체입니다. 전자는 하위
레지스터 다시 쓰기를 구현합니다 (ISA 디코더 튜토리얼에 설명). 그것은
현재 Instruction
인스턴스의 child()
메서드를 사용하여 액세스합니다.
후자는
ReferenceCount
- 이 경우 다음을 실행할 DataBuffer
인스턴스를 저장합니다.
포함할 수 있습니다 컨텍스트 객체는
Instruction
객체의 context()
메서드
nullptr
로 설정됨).
RiscV 메모리 로드의 컨텍스트 객체는 다음 구조체로 정의됩니다.
// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
~LoadContext() override {
if (value_db != nullptr) value_db->DecRef();
}
// Override the base class method so that the data buffer can be DecRef'ed
// when the context object is recycled.
void OnRefCountIsZero() override {
if (value_db != nullptr) value_db->DecRef();
value_db = nullptr;
// Call the base class method.
generic::ReferenceCount::OnRefCountIsZero();
}
// Data buffers for the value loaded from memory (byte, half, word, etc.).
DataBuffer *value_db = nullptr;
};
로드 명령은 데이터 크기 (바이트, 반단어, 단어) 및 로드된 값이 부호 확장인지 여부를 지정할 수 있습니다. 이 후자는 하위 명령만 고려합니다. 이제 템플릿 형식인 도우미 함수를 제공합니다. 이 코드는 값을 구하기 위해 소스 피연산자에 액세스하지 않는다는 점을 제외하면 컨텍스트 객체를 만듭니다
template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->set_latency(0);
auto *context = new riscv::LoadContext(db);
state->LoadMemory(instruction, address, db, instruction->child(), context);
context->DecRef();
}
보시다시피 주요 차이점은 할당된 DataBuffer
인스턴스는
둘 다 LoadMemory
호출에 매개변수로 전달될 뿐 아니라
LoadContext
객체
하위 명령어 시맨틱 함수는 모두 매우 유사합니다. 먼저
LoadContext
는 Instruction
메서드 context()
를 호출하여 가져옵니다.
LoadContext *
에 정적으로 전송됩니다. 둘째, 가치 (데이터에 따라
유형)가 load-data DataBuffer
인스턴스에서 읽습니다. 셋째, 새로운
DataBuffer
인스턴스가 대상 피연산자에서 할당됩니다. 마지막으로
로드된 값은 새 DataBuffer
인스턴스에 쓰이고 Submit
됩니다.
다시 말하지만, 다음과 같이 템플릿화된 도우미 함수를 사용하는 것이 좋습니다.
template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
auto *context = down_cast<riscv::LoadContext *>(instruction->context());
uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, value);
db->Submit();
}
이제 마지막 도우미 함수와 시맨틱 함수를 구현해 보세요. 결제 각 도우미 함수의 템플릿에서 사용하는 데이터 유형에 주의 호출이며 로드의 크기 및 부호 있는/부호 없는 특성에 해당해야 합니다. 지시사항입니다.
다른 애플리케이션과 비교하여 rv32i_instructions.cc
최종 시뮬레이터 빌드 및 실행
어려운 단어를 모두 입력했으니 이제 최종 시뮬레이터를 빌드해 보겠습니다. 이
이 튜토리얼의 모든 작업을 하나로 묶는 최상위 C++ 라이브러리는
other/
에 있습니다. 해당 코드를 너무 자세히 살펴보지 않아도 됩니다.
이후 고급 튜토리얼에서 해당 주제를 방문할 것입니다.
작업 디렉터리를 other/
로 변경하고 빌드합니다. 포드는 Cloud Storage에서
오류가 발생했습니다.
$ cd ../other
$ bazel build :rv32i_sim
이 디렉터리에는 간단한 'hello world' 프로그램에 있어야 합니다
hello_rv32i.elf
이 파일에서 시뮬레이터를 실행하고 결과를 보려면 다음 단계를 따르세요.
$ bazel run :rv32i_sim -- other/hello_rv32i.elf
다음과 같은 줄이 표시됩니다.
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$
시뮬레이터는 bazel
run :rv32i_sim -- -i other/hello_rv32i.elf
명령어를 사용하여 대화형 모드에서 실행할 수도 있습니다. 이렇게 하면
사용할 수 있습니다 프롬프트에 help
를 입력하여 사용 가능한 명령어를 확인합니다.
$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000 addi ra, 0, 0
[0] > help
quit - exit command shell.
core [N] - direct subsequent commands to core N
(default: 0).
run - run program from current pc until a
breakpoint or exit. Wait until halted.
run free - run program in background from current pc
until breakpoint or exit.
wait - wait for any free run to complete.
step [N] - step [N] instructions (default: 1).
halt - halt a running program.
reg get NAME [FORMAT] - get the value or register NAME.
reg NAME [FORMAT] - get the value of register NAME.
reg set NAME VALUE - set register NAME to VALUE.
reg set NAME SYMBOL - set register NAME to value of SYMBOL.
mem get VALUE [FORMAT] - get memory from location VALUE according to
format. The format is a letter (o, d, u, x,
or X) followed by width (8, 16, 32, 64).
The default format is x32.
mem get SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem set VALUE [FORMAT] VALUE - set memory at location VALUE(1) to VALUE(2)
according to FORMAT. Default format is x32.
mem set SYMBOL [FORMAT] VALUE - set memory at location SYMBOL to VALUE
according to FORMAT. Default format is x32.
break set VALUE - set breakpoint at address VALUE.
break set SYMBOL - set breakpoint at value of SYMBOL.
break VALUE - set breakpoint at address VALUE.
break SYMBOL - set breakpoint at value of SYMBOL.
break clear VALUE - clear breakpoint at address VALUE.
break clear SYMBOL - clear breakpoint at value of SYMBOL.
break clear all - remove all breakpoints.
help - display this message.
_start:
80000000 addi ra, 0, 0
[0] >
이것으로 튜토리얼을 마칩니다. 이 가이드가 도움이 되었기를 바랍니다.