Mục tiêu của hướng dẫn này là:
- Tìm hiểu cách ISA và bộ giải mã nhị phân đã tạo kết hợ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 bộ giải mã lệnh
Bộ giải mã lệnh chịu trách nhiệm đọc một địa chỉ lệnh được cung cấp
từ lệnh khỏi bộ nhớ và trả về một thực thể đã khởi tạo đầy đủ của
Instruction
đại diện cho lệnh đó.
Bộ giải mã cấp cao nhất triển khai generic::DecoderInterface
hiển thị bên dưới:
// 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ì mà mã được tạo cần có.
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à xây dựng 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 về thư mục gốc của kho lưu trữ, sau đó hãy cùng xem
tại các nguồn đã được tạo. Để làm được việc đó, hãy thay đổi thư mục thành
bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(giả sử bạn đang dùng x86
host - đố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
cần xem xét:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
Vui lòng lưu ý cách đặt tên cho lớp. Tất cả các lớp đều được đặt tên dựa trên
Phiên bản viết hoa Pascal của tên được đặt trong "isa" trong tệp đó:
isa RiscV32I { ... }
Trước tiên, hãy bắt đầu với lớp RiscVIInstructionSet
. Thông tin đó đượ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 trong lớp này, vì vậy, đây là một lớp độc lập, nhưng
chú ý hai điều. Đầu tiên, hàm khởi tạo lấy một con trỏ đến một thực thể của hàm
Lớp RiscV32IInstructionSetFactory
. Đây là một lớp mà lớp đã tạo
bộ giải mã dùng để tạo một thực thể của lớp RiscV32Slot
, được dùng để
giải mã tất cả lệnh được xác định cho slot RiscV32
như được xác định trong
riscv32i.isa
. Thứ hai, phương thức Decode
lấy thêm một tham số
con trỏ kiểu đến RiscV32IEncodingBase
, đây là 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à tệp nhị phân
được tạo trong phòng thí nghiệm thứ hai.
Lớp RiscV32IInstructionSetFactory
là một lớp trừu tượng mà từ đó chúng ta
phải tìm ra cách triển khai của riêng chúng ta cho bộ giải mã đầy đủ. Trong hầu hết các trường hợp,
lớp này không đáng kể: 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
đã xác định trong tệp .isa
. Trong trường hợp này, rất đơn giản
chỉ là 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ì
một số trường hợp sử dụng nâng cao có thể giúp ích cho việc lấy một lớp con
từ vị trí rồi gọi hàm khởi tạo của nó.
Chúng ta sẽ tìm hiểu về lớp cuối cùng RiscV32IEncodingBase
ở phần sau trong nội dung 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 đảm bảo bạn quay lại
thư mục riscv_full_decoder
.
Mở tệp riscv32_decoder.h
. Tất cả những thông tin cần thiết, bao gồm cả tệp có
đã được thêm và không gian tên đã được thiết lập.
Sau khi nhận xét được đánh dấu //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ì chúng tôi không sử dụng
các lớp dẫn xuất của Riscv32Slot
, chúng ta chỉ cần phân bổ một thực thể mới sử dụ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 tập của mình), câu trả lời đầy đủ là tại đây.
Xác định lớp bộ giải mã
Khai báo hàm khởi tạo, hàm khởi tạo và 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
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 của chúng tôi về bộ giải mã nhị phân. Ngoài tất cả
Hàm Extract
, 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ã vận hành khớp với lệnh đó. Mặt khác,
Lớp DecodeInterface
mà RiscV32Decoder
chỉ triển khai thẻ truyền trong một
của bạn. Do đó, lớp RiscV32Decoder
phải có khả năng truy cập bộ nhớ để
đọc từ hướng dẫn để chuyển đến DecodeRiscVInst32()
. Trong dự án này
bạn có thể truy cập bộ nhớ thông qua một giao diện bộ nhớ đơn giản được xác định trong
.../mpact/sim/util/memory
được đặt tên phù hợp là util::MemoryInterface
, được xem bên dưới:
// 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ó khả năng truyền một thực thể lớp state
cho
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
, đã được thêm vào
cho RiscV. Tức là chúng ta phải khai báo hàm khởi tạo để nó
có thể đưa 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 khởi tạo:
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
Tiếp theo, hãy khai báo phương thức DecodeInstruction
mà chúng ta cần ghi đè
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), câu trả lời đầy đủ là tại đây.
Định nghĩa về thành phần dữ liệu
Lớp RiscV32Decoder
sẽ cần các thành phần dữ liệu riêng tư để lưu trữ
các tham số hàm khởi tạo và một con trỏ đến lớp nhà máy.
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
Lớp mã hoá cũng cần một con trỏ tới lớp mã hoá bắt nguồn từ
RiscV32IEncodingBase
, hãy gọi hàm đó là RiscV32IEncoding
(chúng ta sẽ triển khai
trong bài tập 2). Ngoài ra, thành phần này cần 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 phầ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 tập của mình), câu trả lời đầy đủ là tại đây.
Xác định phương thức của lớp 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à hàm
DecodeInstruction
. Mở tệp riscv32_decoder.cc
. Khoảng trống
đã có trong tệp cũng như các khai báo không gian tên và một vài
trong số using
nội dung khai báo.
Định nghĩa hàm khởi tạo
Hàm khởi tạo chỉ cần khởi tạo 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 bản sao của từng lớp liên quan đến bộ giải mã, truyền vào các tham số phù 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
. Hàm này được phân bổ bằng một factory
có thể truy cập thông qua thành phần state_
. Chúng tôi 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ừ hướng dẫn.
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
Định nghĩa về thành phần phá huỷ
Hàm huỷ rất đơn giản, chỉ cần giải phóng các đối tượng chúng ta đã phân bổ trong hàm khởi tạo,
nhưng có một thay đổi nhỏ. Thực thể vùng đệm dữ liệu được tính tham chiếu, vì vậy, thay vào đó
khi 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 kiểm tra lỗi bổ sung là bắt buộc.
Trước tiên, từ lệnh phải được tìm nạp từ bộ nhớ bằng cách sử dụng bộ nhớ
giao diện 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ừ hướng dẫn.
bạn phải thực hiện việc này trước khi gọi chính bộ giải mã ISA. Xin nhắc lại rằng ISA
các lệnh gọi bộ giải mã vào thực thể RiscVIEncoding
trực tiếp để lấy mã hoạt động
và toán hạng do từ lệnh chỉ định. Chúng tôi chưa triển khai tính năng đó
nhưng hãy 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 đị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), câu trả lời đầy đủ là tại đây.
Lớp mã hoá
Lớp mã hoá triển khai một giao diện mà lớp bộ giải mã sử dụng để lấy opcode lệnh, toán hạng nguồn và toán hạng đích, và 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ừ tệp nhị phân bộ giải mã định dạng, chẳng hạn như mã vận hành, giá trị của các trường cụ thể trong từ chỉ dẫn, v.v. Lớp này được tách riêng với lớp bộ giải mã để giữ khả năng mã hoá độc lập 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. Tập hợp phương pháp chúng ta phải
triển khai trong lớp dẫn xuất của chúng tôi được hiển thị dưới đây.
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 thì có vẻ hơi phức tạp, đặc biệt là với số lượng nhưng đối với một kiến trúc đơn giản như RiscV, chúng tôi thực sự bỏ qua hầu hết các tham số, như giá trị của chúng 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
của giá trị hiện tại
lệnh, xác định mã hoạt động của lệnh. Lớp OpcodeEnum
là
được xác định trong tệp isa giải mã riscv32i_enums.h
đã tạo. Phương thức này sẽ lấy
hai tham số, cả hai đều có thể bị bỏ qua. Đầu tiên trong số
đây là loại ô (một lớp enum cũng được xác định trong riscv32i_enums.h
),
do RiscV chỉ có một vị trí duy nhất nên chỉ có một giá trị hợp lệ:
SlotEnum::kRiscv32
Thứ hai là số thực thể của vùng (trong trường hợp
có nhiều lần xuất hiện của vị trí, có thể xảy ra trong một số VLIW
cấu trúc).
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 pháp tiếp theo được dùng để lập mô hình tài nguyên phần cứng trong bộ xử lý
để 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
nên trong quá trình triển khai, chúng 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 dùng trong
các hàm ngữ nghĩa lệnh để truy cập vào giá trị của bất kỳ lệnh nào
phân tích toán hạng vị từ, từng toán hạng nguồn lệnh và viết mới
cho các toán hạng đích của lệnh. Vì RiscV không sử dụng
theo vị ngữ lệnh, thì 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. Đầu tiên, giống như
GetOpcode
ô và mục nhập được truyền vào. Sau đó, mã vận hành cho
lệnh mà toán hạng phải được tạo. Tính năng này chỉ được sử 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 toán hạng
mà chúng tôi 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. Đây là 3
OpEnums trong riscv32i_enums.h
như bên dưới:
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 nhìn lại
riscv32.isa
, bạn sẽ lưu ý rằng chúng tương ứng với các tập hợp nguồn và đích
tên toán hạng dùng trong phần khai báo của mỗi lệnh. Bằng cách sử dụng các
tên toán hạng cho các toán hạng đại diện cho các trường bit và toán hạng khác nhau
loại này, nó giúp việc viết lớp mã hoá dễ dàng hơn dưới dạng thành phần enum duy nhất
xác định chính xác kiểu toán hạng cần trả về và không cần thiết
hãy xem xét các giá trị của tham số vị trí, mục nhập hoặc mã hoạt động.
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 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 đích đến toán hạng, độ trễ (trong chu kỳ) trôi qua giữa thời điểm lệnh được phát hành và kết quả đích đến được cung cấp cho các hướng dẫn tiếp theo. Trong trình mô phỏng của chúng tôi, độ trễ này sẽ là 0, nghĩa là lệnh viết kết quả được gửi ngay đến sổ đăng ký.
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
Hàm cuối cùng dùng để lấy độ trễ của một đích đến cụ thể
toán hạng 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 của chúng tôi
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ả những thông tin cần thiết, bao gồm cả tệp có
đã được thêm và không gian tên đã được thiết lập. Tất cả thao tác thêm mã
đã hoàn tất sau khi theo dõi nhận xét // Exercise 2.
Hãy bắt đầu bằng cách xác định một lớp RiscV32IEncoding
kế thừa từ
tạo giao diện.
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
Tiếp theo, hàm khởi tạo phải đưa con trỏ đến thực thể trạng thái, trong trường hợp này
một 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 được gọi bằng
RiscV32Decoder
để phân tích cú pháp lệnh:
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 bỏ tên của các thông 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, thêm các ghi đè phương thức 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 callables (đối tượng hàm) được lập chỉ mục bởi
giá trị số của 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 lớp này của các phương thức được giảm xuống để gọi hàm
đối tượng hàm cho giá trị enum được truyền vào và trả về giá trị trả về
giá trị.
Để sắp xếp việc khởi tạo 2 mảng này, chúng ta xác định 2 phương thức sẽ được gọi từ hàm khởi tạo như sau:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
Thành viê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 dòng từ chỉ dẫn.opcode_
để lưu mã hoạt động của lệnh hiện tại được cập nhật bằng phương thứcParseInstruction
. Tham số này có kiểuOpcodeEnum
.source_op_getters_
một mảng để lưu trữ các lệnh gọi dùng để lấy nguồn các đối tượng toán hạng. Loại của phần tử mảng làabsl::AnyInvocable<SourceOperandInterface *>()>
.dest_op_getters_
một mảng để lưu trữ các hàm gọi dùng để lấy các đối tượng toán hạng đích. Loại của 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ụ: "không" 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), câu trả lời đầy đủ là tại đây.
Tệp nguồn (.cc).
Mở tệp riscv32i_encoding.cc
. Tất cả những thông tin cần thiết, bao gồm cả tệp có
đã được thêm và không gian tên đã được thiết lập. Tất cả thao tác thêm mã
đã hoàn tất sau khi theo dõi nhận xét // Exercise 2.
Chức năng trợ giúp
Chúng ta sẽ bắt đầu bằng cách viết một số hàm trợ giúp dùng để tạo
toán hạng của thanh ghi nguồn và đích. Các báo cáo này sẽ được tạo mẫu trên
đăng ký loại đăng ký và sẽ gọi đối tượng RiscVState
để xử lý
đăng ký đối tượng rồi gọi phương thức factory của toán hạng trong đối tượng thanh ghi.
Hãy bắt đầu với 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ó 2 hàm trợ giúp. Bước thứ hai thực hiện thêm
tham số op_name
cho phép toán hạng có tên hoặc chuỗi khác
khác với thanh ghi cơ bản.
Tương tự như vậy đố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 giao diện và hàm khởi tạo
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 tạo để khởi tạo các mảng callables cho phương thức getter của toán hạng.
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
lưu trữ từ hướng dẫn rồi mã hoạt động tương ứng
lấy được từ việc gọi vào mã do bộ giải mã nhị phân tạo.
// 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, các phương thức getter toán hạng sẽ trả về giá trị từ hàm getter mà nó gọi dựa trên tra cứu mảng bằng cách sử dụng giá trị enum toán hạng nguồn/đích.
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 là khởi tạo getter
nhưng đừng lo, bạn có thể thực hiện việc này bằng một mẫu đơn giản, lặp lại. Hãy
bắt đầu bằng InitializeDestinationOpGetters()
trước, vì chỉ có một
một vài toán hạng đích.
Hãy gọi lại lớp DestOpEnum
được 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 nhập, 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ỳ dạng lệnh gọi nào khác. Chữ ký
của hàm lambda là void(int latency)
.
Cho đến bây giờ, chúng tôi chưa nói nhiều về
các loại điểm đến khác nhau
toán hạng được xác định trong MPACT-Sim. Đối với bài tập này, chúng ta sẽ chỉ sử dụng hai
loại: generic::RegisterDestinationOperand
được xác định trong
register.h
,
và generic::DevNullOperand
được xác định trong
devnull_operand.h
.
Hiện tại, chi tiết của các toán hạng này không thực sự quan trọng, ngoại trừ việc
phần đầu dùng để ghi vào các thanh ghi còn phần sau bỏ qua tất cả các lượt ghi.
Mục nhập đầu tiên cho kNone
không đáng kể – chỉ cần trả về một giá trị nullptr và không bắt buộc
ghi một lỗi.
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
Tiếp theo là tin kCsr
. Ở đây, chúng ta sẽ ăn gian một chút. "Hello world" (Xin chào mọi người) chương trình
không dựa vào bất kỳ bản 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 lệnh CSR. Giải pháp là chỉ giả nguỵ trang này bằng cách sử dụng
sổ đăng ký thông thường có tên là "CSR" và chuyển tất cả những nội dung như vậy vào kênh.
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
Tiếp theo là kNextPc
, nghĩa là "máy tính" thanh ghi. Mục tiêu này được dùng làm mục tiêu
cho tất cả hướng dẫn chuyển nhánh và di chuyển. 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ỉ dùng để tham chiếu đến thanh ghi số nguyên được mã hoá trong phần "rd" trường
từ hướng dẫn, nên không có sự mơ hồ đối với từ hướng dẫn. Có
chỉ là một chức năng. Đăng ký x0
(tên abi zero
) được kết nối trực tiếp thành 0,
Vì vậy, đối với thanh ghi đó, chúng ta sẽ sử dụng DevNullOperand
.
Vì vậy, trong getter này, trước tiên, chúng ta 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 ta
trả về lỗi "DevNull" toán hạng, nếu không, chúng ta sẽ trả về đúng toán hạng đăng ký,
hãy nhớ sử dụng bí danh đăng ký 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 xem phương thức InitializeSourceOperandGetters()
, trong đó mẫu là
giống nhau nhiều, nhưng thông tin chi tiết sẽ 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à các 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
chân ái
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
Tiếp theo, hãy thực hiện về toán hạng thanh ghi. Chúng tôi sẽ xử lý kCsr
tương tự
về cách chúng tôi xử lý các toán hạng đích tương ứng – chỉ cần gọi
chức năng trợ giúp bằng cách sử dụng "CSR" làm tên sổ đăng ký.
// 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ù chúng tôi không muốn cập nhật x0
(hoặc zero
), nhưng chúng tôi vẫn muốn đảm bảo rằng
chúng ta luôn đọc 0 từ toán hạng đó. Để làm được điều đó, chúng tôi 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 (trái ngược với một giá trị mô phỏng
giá trị tức thì). Nếu không, mẫu sẽ giống nhau: trước tiên hãy trích xuất mẫu
Giá trị rs1/rs2 của từ lệnh, nếu giá trị bằng 0 thì hãy trả về giá trị bằng chữ
toán hạng có tham số mẫu 0, nếu không sẽ trả về một thanh ghi thông thường
toán hạng nguồn bằng hàm trợ giúp, trong đó bí danh abi làm 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. Giá trị tức thì là
được lưu trữ trong các thực thể của lớp generic::ImmediateOperand<>
được xác định trong
immediate_operand.h
.
Sự khác biệt duy nhất giữa các phương thức getter khác nhau cho các toán hạng tức thì
là chức năng Trích xuất nào được sử dụng và liệu loại bộ nhớ đã được ký hay
chưa có chữ ký, theo trường bit.
// 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 tập của mình), câu trả lời đầy đủ là tại đây.
Bài viết này sẽ kết thúc hướng dẫn này. Chúng tôi hy vọng thông tin này hữu ích.