Hướng dẫn về hàm ngữ nghĩa của hướng dẫn

Mục tiêu của hướng dẫn này là:

  • Tìm hiểu cách dùng các hàm ngữ nghĩa để triển khai ngữ nghĩa hướng dẫn.
  • Tìm hiểu mối liên hệ giữa các hàm ngữ nghĩa với phần mô tả bộ giải mã ISA.
  • Viết các hàm ngữ nghĩa hướng dẫn cho các lệnh RiscV RV32I.
  • Kiểm thử trình mô phỏng cuối cùng bằng cách chạy một lệnh nhỏ "Hello World" ("Xin chào thế giới") tệp thực thi.

Tổng quan về các hàm ngữ nghĩa

Hàm ngữ nghĩa trong MPACT-Sim là hàm triển khai thao tác của một hướng dẫn để có thể nhìn thấy tác dụng phụ của hướng dẫn đó ở trạng thái mô phỏng giống như cách hiệu quả phụ của hướng dẫn có thể nhìn thấy khi được thực thi trong phần cứng. Bản trình bày nội bộ của trình mô phỏng về mỗi lệnh đã giải mã chứa một lệnh gọi được dùng để gọi hàm ngữ nghĩa cho chỉ dẫn.

Hàm ngữ nghĩa có chữ ký void(Instruction *), tức là hàm đưa con trỏ đến một thực thể của lớp Instruction và trả về void.

Lớp Instruction được định nghĩa trong instruction.h

Để viết các hàm ngữ nghĩa, chúng ta đặc biệt quan tâm đến vectơ giao diện toán hạng nguồn và đích được truy cập bằng cách sử dụng Lệnh gọi phương thức Source(int i)Destination(int i).

Dưới đây là các giao diện toán hạng nguồn và đích:

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

Cách cơ bản để viết một hàm ngữ nghĩa cho toán hạng 3 bình thường chẳng hạn như lệnh add 32 bit như sau:

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();
}

Hãy cùng phân tích các phần của hàm này. Hai dòng đầu tiên của phần thân hàm đọc từ toán hạng nguồn 0 và 1. Lệnh gọi AsUint32(0) diễn giải dữ liệu cơ bản dưới dạng mảng uint32_t và tìm nạp dữ liệu thứ 0 . Điều này đúng bất kể thanh ghi hoặc giá trị cơ bản là có giá trị hay không. Kích thước (trong các phần tử) của toán hạng nguồn có thể là thu được từ phương thức toán hạng nguồn shape(). Phương thức này trả về một vectơ chứa số lượng phần tử trong mỗi thứ nguyên. Phương thức đó trả về {1} cho đại lượng vô hướng, {16} cho vectơ gồm 16 phần tử và {4, 4} cho mảng 4x4.

  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);

Sau đó, một uint32_t tạm thời có tên là c được gán giá trị a + b.

Bạn có thể cần giải thích chi tiết hơn trên dòng tiếp theo:

  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();

DataBuffer là một đối tượng tham chiếu được tính được dùng để lưu trữ các giá trị trong trạng thái mô phỏng như thanh ghi. tương đối không có kiểu, mặc dù có kích thước dựa trên đối tượng mà nó được phân bổ. Trong trường hợp này, kích thước đó là sizeof(uint32_t). Câu lệnh này phân bổ một vùng đệm dữ liệu mới có kích thước cho đích là mục tiêu của toán hạng đích này – trong trường hợp này là Thanh ghi số nguyên 32 bit. DataBuffer cũng được khởi tạo bằng độ trễ kiến trúc cho hướng dẫn. Điều này được nêu rõ trong quá trình hướng dẫn giải mã.

Dòng tiếp theo coi thực thể vùng đệm dữ liệu là một mảng uint32_t và ghi giá trị đã lưu trữ trong c vào phần tử thứ 0.

  db->Set<uint32_t>(0, c);

Cuối cùng, câu lệnh cuối cùng gửi vùng đệm dữ liệu đến trình mô phỏng để được sử dụng là giá trị mới của trạng thái máy mục tiêu (trong trường hợp này là một thanh ghi) sau độ trễ của lệnh được thiết lập khi lệnh được giải mã và được điền sẵn vectơ toán hạng đích.

Mặc dù hàm này có thời gian ngắn gọn nhưng hàm này có một chút mã nguyên mẫu mã bị lặp lại khi triển khai lệnh sau khi hướng dẫn. Ngoài ra, việc này có thể che khuất ngữ nghĩa thực tế của hướng dẫn. Đơn đặt hàng để đơn giản hoá hơn nữa việc viết các hàm ngữ nghĩa cho hầu hết các lệnh, có một số hàm trợ giúp theo mẫu được xác định trong instruction_helpers.h. Những trình trợ giúp này sẽ ẩn mã nguyên mẫu để tham khảo hướng dẫn bằng một, 2 hoặc 3 toán hạng nguồn và một toán hạng đích duy nhất. Hãy cùng xem xét hai yếu tố này hàm trợ giúp toán hạng:

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

Bạn sẽ nhận thấy rằng thay vì sử dụng một câu lệnh như:

  uint32_t a = inst->Source(0)->AsUint32(0);

Hàm trợ giúp sử dụng:

generic::GetInstructionSource<Argument>(instruction, 0);

GetInstructionSource là một nhóm hàm trợ giúp theo mẫu được dùng để cung cấp các phương thức truy cập theo mẫu vào nguồn hướng dẫn toán hạng. Nếu không có các API này, mỗi chức năng trợ giúp hướng dẫn sẽ có để chuyên biệt hoá cho từng loại để truy cập vào toán hạng nguồn bằng Hàm As<int type>(). Bạn có thể xem định nghĩa của các mẫu này hàm trong instruction.h.

Như bạn có thể thấy, có ba cách triển khai, tuỳ thuộc vào việc nguồn loại toán hạng giống với đích đến, cho dù đích đến có khác với các nguồn hay liệu chúng có khác nhau hay không. Từng phiên bản của hàm này sẽ đưa một con trỏ đến thực thể lệnh cũng như một lệnh gọi được (bao gồm các hàm lambda). Điều này có nghĩa là giờ đây chúng ta có thể viết lại add hàm ngữ nghĩa ở trên như sau:

void MyAddFunction(Instruction *inst) {
  generic::BinaryOp<uint32_t>(inst,
                              [](uint32_t a, uint32_t b) { return a + b; });
}

Khi được biên dịch bằng bazel build -c optcopts = ["-O3"] trong bản dựng tệp này, tệp này sẽ cùng dòng hoàn toàn mà không có hao tổn, mang lại cho chúng tôi các ký hiệu một cách ngắn gọn mà không có bất kỳ hình phạt nào về hiệu suất.

Như đã đề cập, có các hàm trợ giúp cho đại lượng vô hướng đơn phân, nhị phân và ba ngôi cũng như các vectơ tương đương. Chúng cũng đóng vai trò như các mẫu hữu ích để tạo trình trợ giúp của riêng bạn nhằm đưa ra các hướng dẫn không phù hợp với khuôn mẫu chung.


Bản dựng ban đầu

Nếu bạn chưa thay đổi thư mục thành riscv_semantic_functions, hãy thực hiện ngay bây giờ. Sau đó, tạo dự án như sau – bản dựng này sẽ thành công.

$  bazel build :riscv32i
...<snip>...

Không có tệp nào được tạo, vì vậy, đây thực sự chỉ là một lần chạy thử nghiệm hãy đảm bảo tất cả đều theo đúng thứ tự.


Thêm 3 lệnh ALU về toán hạng

Bây giờ, hãy thêm các hàm ngữ nghĩa cho một số ALU chung gồm 3 toán hạng . Mở tệp rv32i_instructions.cc và đảm bảo rằng định nghĩa bị thiếu sẽ được thêm vào tệp rv32i_instructions.h trong khi tiếp tục.

Sau đây là hướng dẫn mà chúng tôi sẽ thêm:

  • add – Thêm số nguyên 32 bit.
  • and – bitwise 32 bit và.
  • or – 32 bit bitwise hoặc.
  • sll – Dịch chuyển logic 32 bit sang trái.
  • sltu – 32 bit chưa ký được đặt nhỏ hơn.
  • sra – dịch chuyển sang phải trong số học 32 bit.
  • srl – Dịch chuyển sang phải logic 32 bit.
  • sub – Trừ số nguyên 32 bit.
  • xor – xor bitwise 32 bit.

Nếu đã xem các hướng dẫn trước, bạn có thể nhớ lại rằng chúng tôi đã phân biệt giữa hướng dẫn đăng ký tài khoản và hướng dẫn đăng ký ngay trong bộ giải mã. Khi nói đến các hàm ngữ nghĩa, chúng ta không cần làm như vậy nữa. Các giao diện toán hạng sẽ đọc giá trị toán hạng từ bất kỳ toán hạng nào là, đăng ký hoặc ngay lập tức, với hàm ngữ nghĩa hoàn toàn độc lập với toán hạng nguồn cơ bản thực sự là gì.

Ngoại trừ sra, tất cả hướng dẫn ở trên đều có thể được coi là hoạt động trên Giá trị 32 bit chưa ký, để đối với các giá trị này, chúng ta có thể sử dụng hàm mẫu BinaryOp mà chúng ta đã xem trước đó chỉ với đối số loại mẫu duy nhất. Điền vào trong rv32i_instructions.cc cho phù hợp. Xin lưu ý rằng chỉ 5 các bit của toán hạng thứ hai cho lệnh dịch chuyển số tiền. Nếu không, tất cả các toán tử đều có dạng 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

Đối với sra, chúng ta sẽ sử dụng mẫu ba đối số BinaryOp. Nhìn vào mẫu, đối số loại đầu tiên là loại kết quả uint32_t. Thứ hai là loại toán hạng nguồn 0, trong trường hợp này là int32_t, và toán hạng cuối cùng là loại của toán hạng nguồn 1, trong trường hợp này là uint32_t. Điều đó làm cho phần thân của sra hàm ngữ nghĩa:

  generic::BinaryOp<uint32_t, int32_t, uint32_t>(
      instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });

Hãy tiếp tục và thay đổi rồi tạo bản dựng. Bạn có thể đối chiếu bài làm của mình với rv32i_instructions.cc.


Thêm hai lệnh ALU về toán hạng

Chỉ có hai lệnh ALU 2 toán hạng: luiauipc. Tuỳ chọn trước đây sao chép trực tiếp toán hạng nguồn đã dịch chuyển trước sang đích. Chính sách sau thêm địa chỉ hướng dẫn vào ngay trước khi ghi vào lệnh đích. Địa chỉ lệnh có thể truy cập được qua phương thức address() của đối tượng Instruction.

Vì chỉ có một toán hạng nguồn duy nhất, nên chúng ta không thể sử dụng BinaryOp chúng ta cần dùng UnaryOp. Vì chúng ta có thể xử lý cả nguồn và toán hạng đích như uint32_t nên chúng ta có thể sử dụng mẫu đối số duy nhấ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);
}

Phần nội dung của hàm ngữ nghĩa cho lui không quá tầm thường, chỉ cần trả về nguồn. Hàm ngữ nghĩa cho auipc giới thiệu một phần phụ vì bạn cần truy cập phương thức address() trong Instruction thực thể. Câu trả lời là thêm instruction vào ảnh chụp lambda, giúp có sẵn để sử dụng trong phần nội dung hàm lambda. Thay vì [](uint32_t a) { ... } như trước đây, bạn nên viết hàm lambda là [instruction](uint32_t a) { ... }. Giờ đây, bạn có thể sử dụng instruction trong nội dung hàm lambda.

Hãy tiếp tục và thay đổi rồi tạo bản dựng. Bạn có thể đối chiếu bài làm của mình với rv32i_instructions.cc.


Thêm hướng dẫn thay đổi luồng điều khiển

Phần hướng dẫn thay đổi quy trình điều khiển mà bạn cần triển khai được chia thành các lệnh nhánh có điều kiện (các nhánh ngắn hơn được thực hiện nếu phép so sánh đúng) và các hướng dẫn chuyển cảnh nhanh, dùng để triển khai các lệnh gọi hàm (-và-link sẽ bị loại bỏ bằng cách đặt đường liên kết đăng ký về 0, khiến các lượt ghi đó không hoạt động).

Thêm hướng dẫn nhánh có điều kiện

Không có hàm trợ giúp cho hướng dẫn nhánh, vì vậy, có 2 lựa chọn. Viết các hàm ngữ nghĩa từ đầu hoặc viết một hàm trợ giúp cục bộ. Vì chúng ta cần triển khai 6 hướng dẫn nhánh, nên hướng dẫn sau có vẻ đáng giá của chúng tôi. Trước khi làm việc đó, hãy xem xét việc triển khai một nhánh hàm ngữ nghĩa lệnh từ đầu.

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();
  }
}

Điều duy nhất thay đổi trên các hướng dẫn nhánh là nhánh đó điều kiện và loại dữ liệu, int 32 bit có dấu và không có chữ ký, trong số hai toán hạng nguồn. Điều đó có nghĩa là chúng ta cần có thông số mẫu cho toán hạng nguồn. Bản thân hàm trợ giúp cần nhận Instruction thực thể và một đối tượng có thể gọi, chẳng hạn như std::function trả về bool dưới dạng tham số. Hàm trợ giúp sẽ có dạng như sau:

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();
  }
}

Bây giờ, chúng ta có thể viết hàm ngữ nghĩa bge (nhánh có chữ ký lớn hơn hoặc bằng) dưới dạng:

void RV32IBge(Instruction *instruction) {
  BranchConditional<int32_t>(instruction,
                             [](int32_t a, int32_t b) { return a >= b; });
}

Các hướng dẫn nhánh còn lại như sau:

  • Beq – nhánh bằng.
  • Bgeu – nhánh lớn hơn hoặc bằng (chưa ký).
  • Blt – nhánh nhỏ hơn (đã ký).
  • Bltu – nhánh nhỏ hơn (chưa được ký).
  • Bne - nhánh không bằng.

Hãy tiếp tục và thay đổi để triển khai các hàm ngữ nghĩa này, và tạo lại. Bạn có thể đối chiếu bài làm của mình với rv32i_instructions.cc.

Không cần phải viết hàm trợ giúp cho bước nhảy và liên kết nên chúng tôi phải viết những hướng dẫn này từ đầu. Hãy bắt đầu với xem xét ngữ nghĩa hướng dẫn của chúng.

Lệnh jal lấy một giá trị bù trừ từ toán hạng nguồn 0 rồi thêm toán hạng đó vào máy tính hiện tại (địa chỉ hướng dẫn) để tính toán đích nhảy. Mục tiêu nhảy được ghi vào toán hạng đích 0. Địa chỉ trả lại hàng là địa chỉ của hướng dẫn tuần tự tiếp theo. Giá trị này có thể được tính bằng cách thêm giá trị kích thước của lệnh hướng dẫn đến địa chỉ của nó. Địa chỉ trả hàng được viết cho toán hạng đích 1. Nhớ đưa con trỏ đối tượng lệnh vào ảnh chụp lambda.

Lệnh jalr lấy thanh ghi cơ sở làm toán hạng nguồn 0 và một giá trị bù trừ làm toán hạng nguồn 1 rồi thêm chúng lại với nhau để tính toán đích nhảy. Nếu không, lệnh này sẽ giống với lệnh jal.

Dựa trên những mô tả này về ngữ nghĩa hướng dẫn, hãy viết 2 ngữ nghĩa hàm và bản dựng. Bạn có thể đối chiếu bài làm của mình với rv32i_instructions.cc.


Thêm hướng dẫn lưu trữ bộ nhớ

Có 3 hướng dẫn lưu trữ mà chúng ta cần triển khai: lưu trữ byte (sb), nửa từ của cửa hàng (sh) và từ cửa hàng (sw). Hướng dẫn lưu trữ khác với các hướng dẫn mà chúng tôi đã triển khai cho đến nay ở chỗ chúng không ghi vào trạng thái bộ xử lý cục bộ. Thay vào đó, chúng ghi vào tài nguyên hệ thống – bộ nhớ chính. MPACT-Sim không coi bộ nhớ là toán hạng lệnh, vì vậy, việc truy cập bộ nhớ phải được thực hiện bằng một phương pháp khác.

Câu trả lời là thêm các phương thức truy cập bộ nhớ vào đối tượng ArchState MPACT-Sim, hoặc đúng hơn là tạo một đối tượng trạng thái RiscV mới bắt nguồn từ ArchState mà có thể thêm vào. Đối tượng ArchState quản lý các tài nguyên chính, chẳng hạn như và các đối tượng trạng thái khác. API này cũng quản lý các tuyến trễ được dùng để lưu vào vùng đệm dữ liệu toán hạng đích cho đến khi chúng có thể được ghi trở lại vào các đối tượng thanh ghi. Hầu hết các hướng dẫn có thể được triển khai mà không cần biết lớp này, nhưng một số, chẳng hạn như hoạt động bộ nhớ và các hệ thống cụ thể khác các hướng dẫn yêu cầu chức năng phải nằm trong đối tượng trạng thái này.

Hãy xem hàm ngữ nghĩa cho lệnh fence đã được triển khai trong rv32i_instructions.cc làm ví dụ. fence sẽ giữ vấn đề lệnh cho đến khi một số thao tác bộ nhớ nhất định đã hoàn tất. Hàm này dùng để đảm bảo thứ tự bộ nhớ giữa các lệnh thực thi trước lệnh và những thực thi sau.

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

Phần quan trọng trong hàm ngữ nghĩa của lệnh fence là 2 phần cuối đường. Trước tiên, đối tượng trạng thái được tìm nạp bằng một phương thức trong Instructiondowncast<> sang lớp dẫn xuất dành riêng cho RiscV. Sau đó, Fence của lớp RiscVState được gọi để thực hiện thao tác hàng rào.

Hướng dẫn về cửa hàng sẽ hoạt động tương tự. Trước tiên, địa chỉ có hiệu lực của quyền truy cập bộ nhớ được tính toán từ các toán hạng nguồn lệnh cơ số và bù trừ, thì giá trị cần lưu trữ sẽ được tìm nạp từ toán hạng nguồn tiếp theo. Tiếp theo, Đối tượng trạng thái RiscV được lấy thông qua lệnh gọi phương thức state()static_cast<> và gọi phương thức thích hợp.

Phương thức StoreMemory của đối tượng RiscVState tương đối đơn giản, nhưng có một số tác động mà chúng ta cần lưu ý:

  void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);

Như chúng ta có thể thấy, phương thức này nhận 3 tham số, con trỏ đến cửa hàng bản thân lệnh, địa chỉ cửa hàng và con trỏ đến DataBuffer thực thể chứa dữ liệu cửa hàng. Lưu ý, không yêu cầu kích thước, Bản thân thực thể DataBuffer chứa một phương thức size(). Tuy nhiên, sẽ không có toán hạng đích truy cập được theo lệnh để có thể dùng để phân bổ một thực thể DataBuffer có kích thước phù hợp. Thay vào đó, chúng ta cần sử dụng nhà máy DataBuffer lấy từ phương thức db_factory() trong thực thể Instruction. Nhà máy có một phương thức Allocate(int size) sẽ trả về một thực thể DataBuffer có kích thước bắt buộc. Sau đây là ví dụ về cách sử dụng hàm này để phân bổ một thực thể DataBuffer cho một kho lưu trữ nửa chữ (lưu ý rằng auto là một tính năng C++ suy ra kiểu dữ liệu từ bên phải của bài tập):

  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(uint16_t));

Sau khi có thực thể DataBuffer, chúng ta có thể ghi vào thực thể đó như bình thường:

  db->Set<uint16_t>(0, value);

Sau đó, truyền mã này vào giao diện lưu trữ bộ nhớ:

  state->StoreMemory(instruction, address, db);

Chúng tôi vẫn chưa hoàn tất. Thực thể DataBuffer được tính. Chiến dịch này thường được phương thức Submit hiểu và xử lý, để đảm bảo trường hợp sử dụng thường xuyên nhất, càng đơn giản càng tốt. Tuy nhiên, StoreMemory không phải là viết theo cách đó. Thao tác này sẽ IncRef thực thể DataBuffer trong khi hoạt động trên đó rồi DecRef khi hoàn tất. Tuy nhiên, nếu hàm ngữ nghĩa không DecRef tệp tham chiếu riêng nên sẽ không bao giờ được lấy lại. Do đó, dòng cuối cùng có trở thành:

  db->DecRef();

Có ba hàm cửa hàng và điều duy nhất khác biệt là kích thước của quyền truy cập bộ nhớ. Đây có vẻ là một cơ hội tuyệt vời cho một doanh nghiệp địa phương khác hàm trợ giúp theo mẫu. Điều duy nhất khác biệt về chức năng cửa hàng là loại của giá trị cửa hàng, vì vậy mẫu phải có loại đối số đó làm đối số. Ngoài ra, chỉ có thực thể Instruction phải được truyền vào:

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();
}

Hãy tiếp tục và hoàn tất các hàm ngữ nghĩa lưu trữ và tạo. Bạn có thể kiểm tra chống lại rv32i_instructions.cc.


Thêm hướng dẫn tải bộ nhớ

Hướng dẫn tải cần được triển khai như sau:

  • lb – tải byte, ký hiệu mở rộng thành một từ.
  • lbu – tải byte chưa ký, số mở rộng bằng 0 thành một từ.
  • lh – tải nửa từ, ký hiệu mở rộng thành một từ.
  • lhu – tải nửa từ không dấu, mở rộng bằng 0 thành một từ.
  • lw - tải từ.

Hướng dẫn tải là những chỉ dẫn phức tạp nhất mà chúng tôi phải lập mô hình hướng dẫn này. Các hướng dẫn này tương tự với hướng dẫn lưu trữ, ở chỗ chúng cần truy cập vào đối tượng RiscVState, nhưng thêm độ phức tạp trong việc mỗi lần tải lệnh được chia thành hai hàm ngữ nghĩa riêng biệt. Đầu tiên là tương tự như lệnh lưu trữ, trong đó lệnh này tính toán địa chỉ có hiệu lực và bắt đầu truy cập bộ nhớ. Phương thức thứ hai được thực thi khi bộ nhớ quyền truy cập đã hoàn tất và ghi dữ liệu bộ nhớ vào đích của thanh ghi toán hạng.

Hãy bắt đầu bằng cách xem phần khai báo phương thức LoadMemory trong RiscVState:

  void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
                  Instruction *child_inst, ReferenceCount *context);

So với phương thức StoreMemory, LoadMemory cần thêm 2 tham số: một con trỏ đến thực thể Instruction và một con trỏ đến một thực thể tham chiếu đếm được đối tượng context. Nội dung hướng dẫn dành cho trẻ em triển khai tính năng ghi lại thanh ghi (được mô tả trong hướng dẫn bộ giải mã ISA). Nó được truy cập bằng phương thức child() trong thực thể Instruction hiện tại. Chính sách thứ hai là một con trỏ tới một thực thể của lớp bắt nguồn từ ReferenceCount rằng trong trường hợp này sẽ lưu trữ một thực thể DataBuffer chứa dữ liệu đã tải. Đối tượng ngữ cảnh có sẵn thông qua Phương thức context() trong đối tượng Instruction (mặc dù đối với hầu hết các hướng dẫn giá trị này được đặt thành nullptr).

Đối tượng ngữ cảnh cho quá trình tải bộ nhớ RiscV được xác định theo cấu trúc sau:

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

Các hướng dẫn tải đều giống nhau, ngoại trừ kích thước dữ liệu (byte, nửa từ và từ) và liệu giá trị đã tải có mở rộng ký hiệu hay không. Chiến lược phát hành đĩa đơn chỉ được tính vào hướng dẫn dành cho trẻ em. Hãy tạo một mẫu hàm trợ giúp cho các hướng dẫn tải chính. Cách này sẽ rất giống với lưu trữ lệnh lưu trữ, ngoại trừ trường hợp lệnh này sẽ không truy cập vào toán hạng nguồn để nhận giá trị, để tạo một đối tượng ngữ cảnh.

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();
}

Như bạn có thể thấy, điểm khác biệt chính là thực thể DataBuffer được phân bổ được truyền đến lệnh gọi LoadMemory dưới dạng tham số, cũng như được lưu trữ trong Đối tượng LoadContext.

Các hàm ngữ nghĩa của lệnh con đều rất giống nhau. Đầu tiên, Có được LoadContext bằng cách gọi phương thức Instruction context(), và được truyền tĩnh đến LoadContext *. Thứ hai, giá trị (theo dữ liệu type) được đọc từ thực thể tải dữ liệu DataBuffer. Thứ ba, một mô hình Thực thể DataBuffer được phân bổ từ toán hạng đích. Cuối cùng, giá trị đã tải được ghi vào thực thể DataBuffer mới và Submit được ghi. Xin nhắc lại rằng bạn nên sử dụng hàm trợ giúp theo mẫu:

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();
}

Hãy tiếp tục và triển khai các hàm trợ giúp gần đây nhất và hàm ngữ nghĩa. Thanh toán hãy chú ý đến loại dữ liệu bạn sử dụng trong mẫu cho mỗi hàm trợ giúp và tương ứng với kích thước cũng như tính chất đã ký/chưa được ký của tải chỉ dẫn.

Bạn có thể đối chiếu bài làm của mình với rv32i_instructions.cc.


Tạo và chạy trình mô phỏng cuối cùng

Bây giờ, sau khi đã hoàn thành các nhiệm vụ khó, chúng ta có thể xây dựng trình mô phỏng cuối cùng. Chiến lược phát hành đĩa đơn các thư viện C++ cấp cao nhất liên kết với nhau tất cả nội dung trong những hướng dẫn này đều đặt tại other/. Bạn không cần phải nhìn quá khó vào mã đó. T4 sẽ truy cập chủ đề đó trong một hướng dẫn nâng cao trong tương lai.

Thay đổi thư mục đang hoạt động thành other/ rồi tạo bản dựng. Công cụ này sẽ được tạo mà không cần .

$ cd ../other
$ bazel build :rv32i_sim

Trong thư mục đó, có một dòng chữ "xin chào thế giới" đơn giản chương trình trong tệp hello_rv32i.elf. Cách chạy trình mô phỏng trên tệp này và xem kết quả:

$ bazel run :rv32i_sim -- other/hello_rv32i.elf

Bạn sẽ thấy nội dung nào đó dọc theo các dòng:

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
$

Bạn cũng có thể chạy trình mô phỏng ở chế độ tương tác bằng lệnh bazel run :rv32i_sim -- -i other/hello_rv32i.elf. Điều này mang lại một đơn giản shell lệnh. Nhập help tại lời nhắc để xem các lệnh hiện có.

$ 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] >

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 cho bạn.