Mục tiêu của hướng dẫn này là:
- Tìm hiểu cách ISA được tạo và bộ giải mã nhị phân khớp với nhau.
- Viết mã C++ cần thiết để tạo bộ giải mã lệnh đầy đủ cho RiscV RV32I kết hợp ISA và bộ giải mã nhị phân.
Tìm hiểu về bộ giải mã hướng dẫn
Bộ giải mã hướng dẫn chịu trách nhiệm đọc từ hướng dẫn từ bộ nhớ và trả về một thực thể được khởi chạy đầy đủ của Instruction
đại diện cho hướng dẫn đó, dựa trên địa chỉ hướng dẫn.
Bộ giải mã cấp cao nhất triển khai generic::DecoderInterface
như dưới đây:
// 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;
};
Như bạn có thể thấy, chỉ có một phương thức phải được triển khai: cpp
virtual Instruction *DecodeInstruction(uint64_t address);
Bây giờ, hãy xem những gì được cung cấp và những gì cần thiết cho mã được tạo.
Trước tiên, hãy xem xét lớp cấp cao nhất RiscV32IInstructionSet
trong tệp riscv32i_decoder.h
, được tạo ở cuối hướng dẫn về bộ giải mã ISA. Để xem lại nội dung, hãy chuyển đến thư mục giải pháp của hướng dẫn đó và tạo lại tất cả.
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
Bây giờ, hãy thay đổi thư mục của bạn trở lại thư mục gốc của kho lưu trữ, sau đó hãy xem các nguồn đã được tạo. Do đó, hãy thay đổi thư mục thành bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(giả sử bạn đang sử dụng máy chủ x86 – đối với các máy chủ lưu trữ khác, k8-fastbuild sẽ là một chuỗi khác).
$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
Bạn sẽ thấy bốn tệp nguồn chứa mã C++ đã tạo được liệt kê:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Mở tệp đầu tiên riscv32i_decoder.h
. Có 3 lớp mà chúng ta cần xem xét:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
Lưu ý cách đặt tên các lớp. Tất cả các lớp được đặt tên dựa trên phiên bản Pascal-case của tên được cung cấp trong phần khai báo "isa" trong tệp đó: isa RiscV32I { ... }
Trước tiên, hãy bắt đầu với lớp RiscVIInstructionSet
. Mã này được hiển thị bên dưới:
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_;
};
Không có phương thức ảo nào trong lớp này, vì vậy đây là một lớp độc lập, nhưng hãy lưu ý hai điều. Trước tiên, hàm khởi tạo sẽ lấy con trỏ trỏ đến một thực thể của lớp RiscV32IInstructionSetFactory
. Đây là lớp mà bộ giải mã được tạo sử dụng để tạo một thực thể của lớp RiscV32Slot
. Lớp này được dùng để giải mã tất cả các lệnh được xác định cho slot RiscV32
như được xác định trong tệp riscv32i.isa
. Thứ hai, phương thức Decode
sẽ lấy thêm một tham số thuộc loại con trỏ đến RiscV32IEncodingBase
. Đây là một lớp sẽ cung cấp giao diện giữa bộ giải mã isa được tạo trong hướng dẫn đầu tiên và bộ giải mã nhị phân được tạo trong lớp học thực hành thứ hai.
Lớp RiscV32IInstructionSetFactory
là một lớp trừu tượng mà chúng ta phải lấy phương thức triển khai riêng cho bộ giải mã đầy đủ. Trong hầu hết các trường hợp, lớp này không quan trọng: chỉ cần cung cấp một phương thức để gọi hàm khởi tạo cho mỗi lớp khe được xác định trong tệp .isa
. Trong trường hợp của chúng ta, việc này rất đơn giản vì chỉ có một lớp như vậy: Riscv32Slot
(Pascal-case của tên riscv32
nối với Slot
). Phương thức này không được tạo cho bạn vì có một số trường hợp sử dụng nâng cao có thể hữu ích trong việc lấy một lớp con từ khe và gọi hàm khởi tạo của lớp con đó.
Chúng ta sẽ tìm hiểu lớp cuối cùng RiscV32IEncodingBase
trong phần sau của hướng dẫn này, vì đó là chủ đề của một bài tập khác.
Xác định bộ giải mã lệnh cấp cao nhất
Xác định lớp factory
Nếu bạn đã tạo lại dự án cho hướng dẫn đầu tiên, hãy nhớ thay đổi trở lại thư mục riscv_full_decoder
.
Mở tệp riscv32_decoder.h
. Tất cả các tệp cần thiết bao gồm (include) đều đã được thêm và không gian tên đã được thiết lập.
Sau khi nhận xét được đánh dấu là //Exercise 1 - step 1
, hãy xác định lớp RiscV32IsaFactory
kế thừa từ RiscV32IInstructionSetFactory
.
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
Tiếp theo, hãy xác định chế độ ghi đè cho CreateRiscv32Slot
. Vì không sử dụng bất kỳ lớp phái sinh nào của Riscv32Slot
, nên chúng ta chỉ cần phân bổ một thực thể mới bằng std::make_unique
.
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài làm của mình), hãy xem đáp án đầy đủ tại đây.
Xác định lớp bộ giải mã
Hàm khởi tạo, hàm huỷ và khai báo phương thức
Tiếp theo là thời điểm xác định lớp bộ giải mã. Trong cùng tệp như trên, hãy chuyển đến phần khai báo của RiscV32Decoder
. Mở rộng phần khai báo thành một định nghĩa lớp, trong đó RiscV32Decoder
kế thừa từ generic::DecoderInterface
.
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
Tiếp theo, trước khi viết hàm khởi tạo, hãy xem nhanh mã được tạo trong hướng dẫn thứ hai về bộ giải mã nhị phân. Ngoài tất cả các hàm Extract
, còn có hàm DecodeRiscVInst32
:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
Hàm này lấy từ lệnh cần được giải mã rồi trả về mã hoạt động khớp với lệnh đó. Mặt khác, lớp DecodeInterface
mà RiscV32Decoder
triển khai chỉ truyền trong một địa chỉ. Do đó, lớp RiscV32Decoder
phải truy cập được vào bộ nhớ để đọc từ lệnh nhằm truyền đến DecodeRiscVInst32()
. Trong dự án này, cách truy cập bộ nhớ là thông qua một giao diện bộ nhớ đơn giản được xác định trong .../mpact/sim/util/memory
có tên là util::MemoryInterface
, như dưới đây:
// 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;
Ngoài ra, chúng ta cần có thể truyền một thực thể lớp state
đến các hàm khởi tạo của các lớp bộ giải mã khác. Lớp trạng thái thích hợp là lớp riscv::RiscVState
, bắt nguồn từ generic::ArchState
, với chức năng bổ sung cho RiscV. Điều này có nghĩa là chúng ta phải khai báo hàm khởi tạo để hàm này có thể lấy con trỏ đến state
và memory
:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
Xoá hàm khởi tạo mặc định và ghi đè hàm huỷ:
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
Tiếp theo, hãy khai báo phương thức DecodeInstruction
mà chúng ta cần ghi đè từ generic::DecoderInterface
.
generic::Instruction *DecodeInstruction(uint64_t address) override;
Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài tập của mình), thì câu trả lời đầy đủ sẽ có tại đây.
Định nghĩa về thành phần dữ liệu
Lớp RiscV32Decoder
sẽ cần các thành viên dữ liệu riêng tư để lưu trữ tham số hàm khởi tạo và con trỏ đến lớp nhà máy.
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
Lớp này cũng cần một con trỏ đến lớp mã hoá bắt nguồn từ RiscV32IEncodingBase
, hãy gọi lớp đó là RiscV32IEncoding
(chúng ta sẽ triển khai lớp này trong bài tập 2). Ngoài ra, lớp này cần một con trỏ đến một thực thể của RiscV32IInstructionSet
, vì vậy, hãy thêm:
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
Cuối cùng, chúng ta cần xác định một thành viên dữ liệu để sử dụng với giao diện bộ nhớ:
generic::DataBuffer *inst_db_;
Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài làm của mình), hãy xem đáp án đầy đủ tại đây.
Xác định các phương thức của lớp bộ giải mã
Tiếp theo, đã đến lúc triển khai hàm khởi tạo, hàm khởi tạo và phương thức DecodeInstruction
. Mở tệp riscv32_decoder.cc
. Các phương thức trống đã có trong tệp, cũng như các nội dung khai báo không gian tên và một vài nội dung khai báo using
.
Định nghĩa hàm khởi tạo
Hàm khởi tạo chỉ cần khởi chạy các thành phần dữ liệu. Trước tiên, hãy khởi chạy state_
và memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
Tiếp theo, phân bổ các thực thể của từng lớp liên quan đến bộ giải mã, truyền các tham số thích hợp.
// 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);
Cuối cùng, hãy phân bổ thực thể DataBuffer
. Nó được phân bổ bằng cách sử dụng một nhà máy có thể truy cập thông qua thành phần state_
. Chúng ta phân bổ một vùng đệm dữ liệu có kích thước để lưu trữ một uint32_t
, vì đó là kích thước của từ lệnh.
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
Định nghĩa về thành phần phá huỷ
Hàm huỷ là đơn giản, chỉ cần giải phóng các đối tượng mà chúng ta đã phân bổ trong hàm khởi tạo, nhưng có một điểm khác biệt. Thực thể vùng đệm dữ liệu được tính tham chiếu, vì vậy, thay vì gọi delete
trên con trỏ đó, chúng ta DecRef()
đối tượng:
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
Định nghĩa phương thức
Trong trường hợp của chúng ta, việc triển khai phương thức này khá đơn giản. Chúng tôi sẽ giả định rằng địa chỉ đã được căn chỉnh đúng cách và không cần kiểm tra thêm lỗi nào.
Trước tiên, bạn phải tìm nạp từ lệnh từ bộ nhớ bằng cách sử dụng giao diện bộ nhớ và thực thể DataBuffer
.
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
Tiếp theo, chúng ta gọi vào thực thể RiscVIEncoding
để phân tích cú pháp từ lệnh. Việc này phải được thực hiện trước khi gọi chính bộ giải mã ISA. Hãy nhớ rằng bộ giải mã ISA gọi trực tiếp vào thực thể RiscVIEncoding
để lấy opcode và toán hạng do từ lệnh chỉ định. Chúng ta chưa triển khai lớp đó, nhưng hãy sử dụng void ParseInstruction(uint32_t)
làm phương thức đó.
riscv_encoding_->ParseInstruction(iword);
Cuối cùng, chúng ta gọi bộ giải mã ISA, truyền vào địa chỉ và lớp Encoding.
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài tập của mình), thì câu trả lời đầy đủ sẽ có tại đây.
Lớp mã hoá
Lớp mã hoá sẽ triển khai một giao diện mà lớp bộ giải mã sử dụng để lấy mã hoạt động theo lệnh, toán hạng nguồn và toán hạng đích, cũng như toán hạng tài nguyên. Tất cả các đối tượng này phụ thuộc vào thông tin từ bộ giải mã định dạng nhị phân, chẳng hạn như mã vận hành, giá trị của các trường cụ thể trong từ hướng dẫn, v.v. Các đối tượng này được tách biệt với lớp bộ giải mã để có thể mã hoá khả năng bất khả tri và cho phép hỗ trợ nhiều lược đồ mã hoá trong tương lai.
RiscV32IEncodingBase
là một lớp trừu tượng. Dưới đây là tập hợp các phương thức mà chúng ta phải triển khai trong lớp phái sinh.
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;
};
Thoạt nhìn, mã này có vẻ hơi phức tạp, đặc biệt là số lượng tham số, nhưng đối với một cấu trúc đơn giản như RiscV, chúng ta thực sự bỏ qua hầu hết các tham số vì giá trị của các tham số này sẽ được ngụ ý.
Hãy lần lượt tìm hiểu từng phương pháp.
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
Phương thức GetOpcode
trả về thành phần OpcodeEnum
cho lệnh hiện tại, xác định mã hoạt động của lệnh. Lớp OpcodeEnum
được định nghĩa trong tệp bộ giải mã isa đã tạo riscv32i_enums.h
. Phương thức này có hai tham số, cả hai đều có thể được bỏ qua cho mục đích của chúng ta. Loại đầu tiên trong số này là loại khe (một lớp enum cũng được xác định trong riscv32i_enums.h
), vì RiscV chỉ có một khe nên chỉ có một giá trị có thể có: SlotEnum::kRiscv32
. Thứ hai là số thực thể của vùng (trong trường hợp có nhiều thực thể của vùng, có thể xuất hiện trong một số kiến trúc 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);
Hai phương thức tiếp theo được dùng để lập mô hình tài nguyên phần cứng trong bộ xử lý nhằm cải thiện độ chính xác của chu kỳ. Đối với các bài tập hướng dẫn, chúng tôi sẽ không sử dụng các mã này, vì vậy, trong quá trình triển khai, các bài tập này sẽ bị loại bỏ và trả về 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);
Ba phương thức này trả về con trỏ đến các đối tượng toán hạng được sử dụng trong các hàm ngữ nghĩa của lệnh để truy cập giá trị của bất kỳ toán hạng mệnh đề lệnh nào, mỗi toán hạng nguồn lệnh và ghi các giá trị mới vào toán hạng đích của lệnh. Vì RiscV không sử dụng các mệnh đề lệnh, nên phương thức đó chỉ cần trả về nullptr
.
Mẫu tham số tương tự nhau trong các hàm này. Trước tiên, giống như GetOpcode
, khe và mục nhập sẽ được truyền vào. Sau đó, mã opcode cho instruction (lệnh) mà toán hạng phải được tạo. Phương thức này chỉ được dùng nếu các mã hoạt động khác nhau cần trả về các đối tượng toán hạng khác nhau cho cùng một loại toán hạng. Trường hợp này không áp dụng cho trình mô phỏng RiscV này.
Tiếp theo là mục Thuộc tính, Nguồn và Đích đến, mục liệt kê toán hạng xác định toán hạng phải được tạo. Các giá trị này đến từ ba OpEnums trong riscv32i_enums.h
như dưới đây:
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,
};
Nếu xem lại tệp riscv32.isa
, bạn sẽ thấy các tên này tương ứng với các nhóm tên toán hạng nguồn và đích dùng trong phần khai báo của từng lệnh. Bằng cách sử dụng nhiều tên toán hạng cho các toán hạng đại diện cho nhiều trường bit và loại toán hạng, việc viết lớp mã hoá sẽ dễ dàng hơn vì thành viên enum xác định duy nhất loại toán hạng chính xác cần trả về và không cần phải xem xét giá trị của các tham số vị trí, mục nhập hoặc mã opcode.
Cuối cùng, đối với toán hạng nguồn và đích, vị trí thứ tự của toán hạng được truyền vào (một lần nữa, chúng ta có thể bỏ qua điều này) và đối với toán hạng đích, độ trễ (tính bằng chu kỳ) trôi qua giữa thời điểm lệnh được đưa ra và kết quả đích có sẵn cho các lệnh tiếp theo. Trong trình mô phỏng của chúng tôi, độ trễ này sẽ bằng 0, nghĩa là lệnh sẽ ghi kết quả ngay lập tức vào thanh ghi.
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
Hàm cuối cùng được dùng để lấy độ trễ của một toán hạng đích cụ thể nếu toán hạng đó được chỉ định là *
trong tệp .isa
. Trường hợp này không phổ biến và không được dùng cho trình mô phỏng RiscV này, vì vậy, việc triển khai hàm này sẽ chỉ trả về 0.
Xác định lớp mã hoá
Tệp tiêu đề (.h)
Phương thức
Mở tệp riscv32i_encoding.h
. Tất cả các tệp cần thiết đã được thêm và các không gian tên đã được thiết lập. Tất cả việc thêm mã đều được thực hiện sau chú thích // Exercise 2.
Hãy bắt đầu bằng cách xác định một lớp RiscV32IEncoding
kế thừa từ giao diện được tạo.
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
Tiếp theo, hàm khởi tạo sẽ lấy con trỏ đến thực thể trạng thái, trong trường hợp này là con trỏ đến riscv::RiscVState
. Bạn nên sử dụng hàm huỷ mặc định.
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
Trước khi thêm tất cả các phương thức giao diện, hãy thêm phương thức do RiscV32Decoder
gọi để phân tích cú pháp hướng dẫn:
void ParseInstruction(uint32_t inst_word);
Tiếp theo, hãy thêm các phương thức có cơ chế ghi đè không đáng kể trong khi xoá tên của tham số không được sử dụng:
// 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; }
Cuối cùng, hãy thêm các phương thức ghi đè còn lại của giao diện công khai nhưng với việc triển khai được trì hoãn đến tệp .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;
Để đơn giản hoá việc triển khai từng phương thức getter toán hạng, chúng ta sẽ tạo hai mảng có thể gọi (đối tượng hàm) được lập chỉ mục theo giá trị số của các thành phần SourceOpEnum
và DestOpEnum
tương ứng.
Bằng cách này, phần nội dung của các phương thức này được giảm xuống để gọi đối tượng hàm cho giá trị enum được truyền vào và trả về giá trị trả về.
Để sắp xếp quá trình khởi tạo hai mảng này, chúng ta xác định hai phương thức riêng tư sẽ được gọi từ hàm khởi tạo như sau:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
Thành phần dữ liệu
Sau đây là những thành phần dữ liệu cần có:
state_
để lưu giá trịriscv::RiscVState *
.inst_word_
thuộc loạiuint32_t
chứa giá trị của từ hướng dẫn hiện tại.opcode_
để lưu giữ mã opcode của lệnh hiện tại được cập nhật bằng phương thứcParseInstruction
. Loại này có kiểuOpcodeEnum
.source_op_getters_
là một mảng để lưu trữ các đối tượng có thể gọi dùng để lấy đối tượng toán hạng nguồn. Loại của các phần tử mảng làabsl::AnyInvocable<SourceOperandInterface *>()>
.dest_op_getters_
là một mảng để lưu trữ các đối tượng có thể gọi dùng để lấy đối tượng toán hạng đích. Loại của các phần tử mảng làabsl::AnyInvocable<DestinationOperandInterface *>()>
.xreg_alias
một mảng tên ABI của thanh ghi số nguyên RiscV, ví dụ: "zero" và "ra" thay vì "x0" và "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"};
Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài tập của mình), thì câu trả lời đầy đủ sẽ có tại đây.
Tệp nguồn (.cc).
Mở tệp riscv32i_encoding.cc
. Tất cả các tệp cần thiết đã được thêm và các không gian tên đã được thiết lập. Tất cả việc thêm mã đều được thực hiện sau chú thích // Exercise 2.
Chức năng trợ giúp
Chúng ta sẽ bắt đầu bằng cách viết một vài hàm trợ giúp mà chúng ta sử dụng để tạo toán hạng đăng ký nguồn và đích. Các phương thức này sẽ được tạo mẫu trên loại thanh ghi và sẽ gọi vào đối tượng RiscVState
để lấy một xử lý cho đối tượng đăng ký, sau đó gọi một phương thức nhà máy toán hạng trong đối tượng đăng ký.
Hãy bắt đầu với các trình trợ giúp toán hạng đích:
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);
}
Như bạn có thể thấy, có hai hàm trợ giúp. Phương thức thứ hai sẽ lấy thêm một tham số op_name
cho phép toán hạng có tên hoặc cách biểu thị chuỗi khác với thanh ghi cơ bản.
Tương tự như đối với trình trợ giúp toán hạng nguồn:
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;
}
Hàm khởi tạo và hàm giao diện
Hàm khởi tạo và các hàm giao diện rất đơn giản. Hàm khởi tạo chỉ gọi 2 phương thức khởi chạy để khởi chạy các mảng có thể gọi cho các phương thức getter toán hạng.
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
lưu trữ từ lệnh, sau đó là opcode mà nó nhận được từ lệnh gọi vào mã được tạo bởi bộ giải mã nhị phân.
// 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_);
}
Cuối cùng, phương thức getter của toán hạng trả về giá trị từ hàm getter mà phương thức này gọi dựa trên hoạt động tra cứu mảng bằng cách sử dụng giá trị enum của toán hạng đích/nguồn.
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)]();
}
Phương thức khởi tạo mảng
Như bạn có thể đoán, hầu hết công việc đều tập trung vào việc khởi tạo các mảng getter, nhưng đừng lo, việc này được thực hiện bằng cách sử dụng một mẫu lặp lại dễ dàng. Trước tiên, hãy bắt đầu với InitializeDestinationOpGetters()
, vì chỉ có một vài toán hạng đích.
Hãy nhớ lớp DestOpEnum
đã tạo từ riscv32i_enums.h
:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
Đối với dest_op_getters_
, chúng ta cần khởi tạo 4 mục, mỗi mục cho kNone
, kCsr
, kNextPc
và kRd
. Để thuận tiện, mỗi mục nhập được khởi tạo bằng một lambda, mặc dù bạn cũng có thể sử dụng bất kỳ hình thức gọi nào khác. Chữ ký của lambda là void(int latency)
.
Cho đến nay, chúng ta chưa nói nhiều về các loại toán hạng đích được xác định trong MPACT-Sim. Đối với bài tập này, chúng ta sẽ chỉ sử dụng 2 loại: generic::RegisterDestinationOperand
được xác định trong register.h
và generic::DevNullOperand
được xác định trong devnull_operand.h
.
Thông tin chi tiết về các toán hạng này hiện không thực sự quan trọng, ngoại trừ việc toán hạng trước được dùng để ghi vào thanh ghi và toán hạng sau bỏ qua tất cả các hoạt động ghi.
Mục đầu tiên cho kNone
không quan trọng – chỉ cần trả về một nullptr và ghi lỗi (không bắt buộc).
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
Tiếp theo là kCsr
. Ở đây, chúng ta sẽ ăn gian một chút. Chương trình "xin chào thế giới" không dựa vào bất kỳ nội dung cập nhật CSR thực tế nào, nhưng có một số mã nguyên mẫu thực thi các hướng dẫn CSR. Giải pháp là chỉ cần tạo bản sao bằng cách sử dụng một thanh ghi thông thường có tên là "CSR" và chuyển tất cả các hoạt động ghi như vậy đến thanh ghi đó.
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
Tiếp theo là kNextPc
, tham chiếu đến thanh ghi "pc". Giá trị này được dùng làm đích cho tất cả lệnh nhánh và lệnh nhảy. Tên được định nghĩa trong RiscVState
là kPcName
.
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
Cuối cùng là toán hạng đích kRd
. Trong riscv32i.isa
, toán hạng rd
chỉ được dùng để tham chiếu đến thanh ghi số nguyên được mã hoá trong trường "rd" của từ lệnh, do đó, không có sự mơ hồ mà toán hạng này tham chiếu. Chỉ có một chức năng. Đăng ký x0
(tên abi zero
) được kết nối cứng với 0, vì vậy, đối với thanh ghi đó, chúng ta sử dụng DevNullOperand
.
Vì vậy, trong phương thức getter này, trước tiên, chúng ta sẽ trích xuất giá trị trong trường rd
bằng cách sử dụng phương thức Extract
được tạo từ tệp .bin_fmt. Nếu giá trị bằng 0, chúng tôi sẽ trả về toán hạng "DevNull", nếu không sẽ trả về đúng toán hạng đăng ký, chú ý sử dụng bí danh thanh ghi thích hợp làm tên toán hạng.
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]);
)
}
}
Bây giờ, hãy chuyển sang phương thức InitializeSourceOperandGetters()
, trong đó hoa văn
rất giống nhau, nhưng thông tin chi tiết hơi khác nhau.
Trước tiên, hãy xem SourceOpEnum
được tạo từ riscv32i.isa
trong hướng dẫn đầu tiên:
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,
};
Việc kiểm tra các thành viên, ngoài kNone
, họ chia thành hai nhóm. Một là toán hạng tức thì: kBimm12
, kImm12
, kJimm20
, kSimm12
, kUimm20
và kUimm5
. Còn lại là các toán hạng đăng ký: kCsr
, kRs1
và kRs2
.
Toán hạng kNone
được xử lý giống như toán hạng đích – trả về một nullptr.
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
Tiếp theo, hãy xử lý toán hạng đăng ký. Chúng ta sẽ xử lý kCsr
tương tự như cách xử lý toán hạng đích tương ứng – chỉ cần gọi hàm trợ giúp bằng cách sử dụng "CSR" làm tên thanh ghi.
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
Toán hạng kRs1
và kRs2
được xử lý tương đương với kRd
, ngoại trừ việc mặc dù không muốn cập nhật x0
(hoặc zero
), nhưng chúng ta muốn đảm bảo rằng luôn đọc được 0 từ toán hạng đó. Để làm việc đó, chúng ta sẽ sử dụng lớp generic::IntLiteralOperand<>
được xác định trong literal_operand.h
.
Toán hạng này được dùng để lưu trữ một giá trị cố định (thay vì giá trị tức thì được mô phỏng). Nếu không, mẫu sẽ giống nhau: trước tiên, hãy trích xuất giá trị rs1/rs2 từ từ lệnh, nếu giá trị này bằng 0, hãy trả về toán hạng cố định bằng tham số mẫu 0, nếu không, hãy trả về toán hạng nguồn đăng ký thông thường bằng hàm trợ giúp, sử dụng bí danh abi làm tên toán hạng.
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]);
};
Cuối cùng, chúng ta xử lý các toán hạng tức thì khác nhau. Các giá trị tức thì được lưu trữ trong các thực thể của lớp generic::ImmediateOperand<>
được xác định trong immediate_operand.h
.
Điểm khác biệt duy nhất giữa các phương thức getter cho toán hạng tức thì là hàm Extractor được sử dụng và loại bộ nhớ có được ký hay không, theo 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_));
};
}
Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài làm của mình), hãy xem đáp án đầy đủ tại đây.
Bài viết này đã kết thúc phần hướng dẫn. Chúng tôi hy vọng thông tin này hữu ích với bạn.