Tutorial fungsi semantik petunjuk

Tujuan tutorial ini adalah:

  • Pelajari cara fungsi semantik digunakan untuk menerapkan semantik petunjuk.
  • Mempelajari hubungan fungsi semantik dengan deskripsi decoder ISA.
  • Menulis fungsi semantik instruksi untuk instruksi RiscV RV32I.
  • Uji simulator akhir dengan menjalankan "Hello World" kecil file yang dapat dieksekusi.

Ringkasan fungsi semantik

Fungsi semantik di MPACT-Sim adalah fungsi yang mengimplementasikan operasi suatu instruksi sehingga efek sampingnya terlihat dalam keadaan simulasi dengan cara yang sama efek samping instruksi terlihat ketika dieksekusi dalam perangkat keras. Representasi internal simulator untuk setiap petunjuk yang didekode berisi {i>callable<i} yang digunakan untuk memanggil fungsi semantik untuk itu instruksi.

Fungsi semantik memiliki tanda tangan void(Instruction *), yaitu fungsi yang membawa pointer ke instance class Instruction dan akan menampilkan void.

Class Instruction ditentukan di instruction.h

Untuk keperluan penulisan fungsi semantik, kita sangat tertarik vektor antarmuka operand sumber dan tujuan yang diakses menggunakan Panggilan metode Source(int i) dan Destination(int i).

Antarmuka operand sumber dan tujuan ditampilkan di bawah ini:

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

Cara dasar menulis fungsi semantik untuk operand 3 normal seperti instruksi add 32-bit adalah sebagai berikut:

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

Mari kita uraikan bagian-bagian dari fungsi ini. Dua baris pertama isi fungsi membaca dari operand sumber 0 dan 1. Panggilan AsUint32(0) menafsirkan data pokok sebagai array uint32_t dan mengambil string ke-0 . Hal ini berlaku terlepas dari apakah register atau nilai yang mendasarinya {i>array<i} yang bernilai atau tidak. Ukuran (dalam elemen) operand sumber dapat yang diperoleh dari metode operand sumber shape(), yang menampilkan vektor yang berisi jumlah elemen di setiap dimensi. Metode tersebut akan menampilkan {1} untuk skalar, {16} untuk vektor 16 elemen, dan {4, 4} untuk array 4x4.

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

Kemudian, uint32_t yang sementara bernama c diberi nilai a + b.

Baris berikutnya mungkin memerlukan lebih banyak penjelasan:

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

DataBuffer adalah objek yang dihitung sebagai referensi dan digunakan untuk menyimpan nilai kondisi simulasi seperti register. Relatif tidak terformat, meskipun memiliki ukuran berdasarkan objek yang dialokasikan. Dalam hal ini, ukuran itu adalah sizeof(uint32_t). Pernyataan ini mengalokasikan {i>buffer<i} data baru berukuran tujuan yang merupakan target operand tujuan ini - dalam hal ini Pendaftaran bilangan bulat 32-bit. DataBuffer juga diinisialisasi dengan latensi arsitektural untuk petunjuk. Ini ditentukan selama pengajaran melakukan dekode.

Baris berikutnya memperlakukan instance buffer data sebagai array uint32_t dan menulis nilai yang disimpan dalam c ke elemen ke-0.

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

Akhirnya, pernyataan terakhir mengirimkan {i>buffer<i} data ke simulator yang akan digunakan sebagai nilai baru dari status mesin target (dalam hal ini register) setelah latensi instruksi yang ditetapkan saat instruksi didekode dan vektor operand tujuan diisi.

Meskipun prosesnya cukup singkat, ini memiliki sedikit boilerplate kode yang menjadi berulang saat mengimplementasikan instruksi setelah instruksi. Selain itu, tindakan ini dapat mengaburkan semantik instruksi yang sebenarnya. Secara berurutan untuk lebih menyederhanakan penulisan fungsi semantik untuk sebagian besar instruksi, ada sejumlah fungsi helper dengan template yang ditentukan di instruction_helpers.h. Bantuan ini menyembunyikan kode boilerplate untuk petunjuk dengan satu, dua, atau tiga operand sumber, dan satu operand tujuan. Mari kita lihat dua fungsi bantuan operand:

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

Anda akan melihat bahwa alih-alih menggunakan pernyataan seperti:

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

Fungsi bantuan menggunakan:

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

GetInstructionSource adalah kumpulan fungsi bantuan dengan template yang digunakan untuk menyediakan metode akses template ke sumber petunjuk operand. Tanpa mereka, masing-masing fungsi {i>help <i}instruksi akan memiliki untuk setiap tipe guna mengakses operand sumber dengan Fungsi As<int type>(). Anda dapat melihat definisi-definisi template ini fungsi dalam instruction.h.

Seperti yang Anda lihat ada tiga implementasi, tergantung pada apakah jenis operand sama dengan tujuan, terlepas dari apakah tujuan berbeda dari sumbernya, atau apakah semuanya berbeda. Setiap versi fungsi membawa pointer ke instance instruksi serta callable (yang mencakup fungsi lambda). Artinya, sekarang kita dapat menulis ulang add fungsi semantik di atas sebagai berikut:

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

Saat dikompilasi dengan bazel build -c opt dan copts = ["-O3"] dalam build , ini harus inline sepenuhnya tanpa overhead, yang memberi kita notasi yang ringkas tanpa ada penalti performa.

Seperti yang telah disebutkan ada fungsi bantuan untuk skalar unary, biner, dan terner serta vektor yang setara. {i>Mockup <i}juga berfungsi sebagai {i>template <i}yang berguna guna membuat {i>helper<i} Anda sendiri untuk instruksi yang tidak sesuai dengan standar umum.


Build awal

Jika Anda belum mengubah direktori ke riscv_semantic_functions, lakukanlah sekarang. Kemudian, bangun project sebagai berikut - build ini akan berhasil.

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

Tidak ada file yang dihasilkan, jadi ini hanyalah uji coba untuk memastikan semuanya berurutan.


Menambahkan tiga petunjuk ALU operand

Sekarang mari kita tambahkan fungsi semantik untuk beberapa ALU 3 operasi generik petunjuk. Buka file rv32i_instructions.cc, dan pastikan bahwa definisi yang hilang ditambahkan ke file rv32i_instructions.h saat kita melanjutkan.

Petunjuk yang akan kami tambahkan adalah:

  • add - Penambahan bilangan bulat 32-bit.
  • and - bitwise 32-bit dan.
  • or - 32-bit bitwise atau.
  • sll - Pergeseran logis 32-bit ke kiri.
  • sltu - Kumpulan 32-bit tanpa label kurang dari.
  • sra - Pergeseran kanan aritmetika 32 bit.
  • srl - Pergeseran kanan logis 32-bit.
  • sub - pengurangan bilangan bulat 32-bit.
  • xor - Xor bitwise 32-bit.

Jika Anda telah melakukan tutorial sebelumnya, Anda mungkin ingat bahwa kami membedakan antara instruksi pendaftaran-pendaftaran dan instruksi pendaftaran-segera decoder. Untuk fungsi semantik, kita tidak perlu lagi melakukannya. Antarmuka operand akan membaca nilai operand dari operand mana pun adalah, terdaftar atau langsung, dengan fungsi semantik yang sepenuhnya agnostik terhadap apa sebenarnya operand sumber yang mendasarinya.

Kecuali untuk sra, semua petunjuk di atas dapat dianggap beroperasi di Nilai 32-bit tanpa tanda tangan, jadi untuk ini kita dapat menggunakan fungsi template BinaryOp yang telah kita lihat sebelumnya hanya dengan argumen jenis {i>template<i} tunggal. Isilah isi fungsi di rv32i_instructions.cc. Perhatikan bahwa hanya 5 angka yang bit operand kedua ke instruksi {i>shift<i} digunakan untuk {i>shift<i} jumlah tersebut. Jika tidak, semua operasi akan memiliki bentuk 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

Untuk sra, kita akan menggunakan template BinaryOp tiga argumen. Melihat template, argumen jenis pertama adalah jenis hasil uint32_t. Yang kedua adalah jenis operand sumber 0, dalam hal ini int32_t, dan yang terakhir adalah jenis dari operand 1 sumber, dalam hal ini uint32_t. Ini membuat isi sra fungsi semantik:

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

Lanjutkan membuat perubahan, lalu bangun. Anda dapat memeriksa pekerjaan Anda dengan rv32i_instructions.cc.


Menambahkan dua petunjuk ALU operand

Hanya ada dua petunjuk ALU 2 operan: lui dan auipc. Mantan menyalin operand sumber yang telah digeser sebelumnya secara langsung ke tujuan. Terakhir menambahkan alamat instruksi ke alamat terdekat sebelum menulisnya ke tujuan. Alamat petunjuk dapat diakses dari metode address() dari objek Instruction.

Karena hanya ada satu operand sumber, kita tidak dapat menggunakan BinaryOp, sebagai gantinya kita perlu menggunakan UnaryOp. Karena kita bisa memperlakukan sumber dan operand tujuan sebagai uint32_t kita dapat menggunakan template argumen tunggal .

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

Isi fungsi semantik untuk lui sesederhana mungkin, cukup tampilkan sumbernya. Fungsi semantik untuk auipc memperkenalkan minor karena Anda harus mengakses metode address() di Instruction di instance Compute Engine. Jawabannya adalah menambahkan instruction ke rekaman lambda, sehingga tersedia untuk digunakan dalam isi fungsi lambda. Sebagai ganti [](uint32_t a) { ... } seperti sebelumnya, lambda harus ditulis [instruction](uint32_t a) { ... }. Sekarang instruction dapat digunakan dalam isi lambda.

Lanjutkan membuat perubahan, lalu bangun. Anda dapat memeriksa pekerjaan Anda dengan rv32i_instructions.cc.


Tambahkan petunjuk perubahan alur kontrol

Petunjuk perubahan alur kontrol yang perlu Anda terapkan dibagi menjadi instruksi cabang bersyarat (cabang lebih pendek yang dilakukan jika perbandingan berlaku), dan petunjuk lompat dan tautkan, yang digunakan mengimplementasikan panggilan fungsi (, -and-link dihapus dengan menyetel link mendaftar ke nol, sehingga operasi tulis tersebut tanpa pengoperasian).

Tambahkan petunjuk cabang bersyarat

Tidak ada fungsi bantuan untuk instruksi cabang, jadi ada dua opsi. Tulis fungsi semantik dari awal, atau tulis fungsi bantuan lokal. Karena kita perlu mengimplementasikan 6 instruksi cabang, instruksi yang terakhir tampaknya sepadan dengan usaha. Sebelum kita melakukannya, mari kita lihat implementasi cabang fungsi semantik petunjuk dari awal.

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

Satu-satunya hal yang bervariasi di seluruh instruksi cabang adalah cabang kondisi, dan tipe datanya, int 32 bit bertanda tangan vs tidak bertanda tangan, operand sumber. Itu berarti kita perlu memiliki parameter {i>template<i} untuk operand sumber. Fungsi bantuan itu sendiri harus mengambil Instruction dan objek callable seperti std::function yang menampilkan bool sebagai parameter. Fungsi bantuan akan terlihat seperti ini:

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

Sekarang kita dapat menulis fungsi semantik bge (cabang bertanda tangan lebih besar atau sama) sebagai:

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

Instruksi cabang yang tersisa adalah sebagai berikut:

  • Beq - cabang sama.
  • Bgeu - cabang yang lebih besar atau sama (tidak bertanda tangan).
  • Blt - cabang kurang dari (bertanda tangan).
  • Bltu - cabang kurang dari (tidak bertanda tangan).
  • Bne - cabang tidak sama.

Lanjutkan membuat perubahan untuk menerapkan fungsi semantik ini, dan membangun kembali. Anda dapat memeriksa pekerjaan Anda dengan rv32i_instructions.cc.

Tidak ada gunanya menulis fungsi bantuan untuk jump dan link instruksi, jadi kita perlu menulisnya dari awal. Mari kita mulai dengan melihat semantik instruksinya.

Instruksi jal mengambil offset dari operand sumber 0 dan menambahkannya ke pc saat ini (alamat petunjuk) untuk menghitung target lompatan. Target lompatan ditulis ke operand tujuan 0. Alamat pengembalian adalah alamat petunjuk berurutan berikutnya. Hal ini dapat dihitung dengan menambahkan nilai ukuran petunjuk ke alamatnya. Alamat pengembalian akan ditulis ke operand tujuan 1. Ingatlah untuk menyertakan pointer objek petunjuk di pengambilan lambda.

Instruksi jalr menggunakan register dasar sebagai operand sumber 0 dan offset sebagai sumber operand 1, dan menambahkannya bersama-sama untuk menghitung target lompatan. Jika tidak, ID ini sama dengan instruksi jal.

Berdasarkan deskripsi semantik petunjuk ini, tulis dua semantik fungsi, dan build. Anda dapat memeriksa pekerjaan Anda dengan rv32i_instructions.cc.


Menambahkan petunjuk penyimpanan memori

Ada tiga instruksi penyimpanan yang perlu kita implementasikan: menyimpan byte (sb), simpan setengah kata (sh), dan simpan kata (sw). Petunjuk toko berbeda dari instruksi yang telah kita terapkan sejauh ini karena mereka tidak menulis ke status prosesor lokal. Sebagai gantinya, mereka menulis ke sumber daya sistem - memiliki memori utama. MPACT-Sim tidak memperlakukan memori sebagai operand instruksi, jadi akses memori harus dilakukan dengan menggunakan metodologi lain.

Jawabannya adalah dengan menambahkan metode akses memori ke objek ArchState MPACT-Sim, atau lebih tepatnya, buat objek status RiscV baru yang berasal dari ArchState yang dapat ditambahkan. Objek ArchState mengelola resource inti, seperti register dan objek status lainnya. Sistem ini juga mengelola saluran tunda yang digunakan untuk buffer data operand tujuan hingga dapat ditulis kembali ke objek register. Sebagian besar instruksi dapat diimplementasikan tanpa mengetahui tetapi beberapa di antaranya, seperti operasi memori dan sistem spesifik lainnya memerlukan fungsionalitas untuk berada di objek status ini.

Mari kita lihat fungsi semantik untuk instruksi fence yang sudah diterapkan di rv32i_instructions.cc sebagai contoh. fence menyimpan masalah instruksi sampai operasi memori tertentu memiliki selesai. Protokol ini digunakan untuk menjamin pengurutan memori di antara instruksi yang dieksekusi sebelum instruksi dan yang dieksekusi setelahnya.

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

Bagian penting dari fungsi semantik petunjuk fence adalah dua yang terakhir penting. Pertama, objek status diambil menggunakan metode di Instruction dan downcast<> ke class turunan khusus RiscV. Lalu Fence dari class RiscVState dipanggil untuk melakukan operasi fence.

Petunjuk toko akan berfungsi serupa. Pertama, alamat efektif dari akses memori dihitung dari operand sumber instruksi dasar dan offset, maka nilai yang akan disimpan diambil dari operand sumber berikutnya. Berikutnya, Objek status RiscV diperoleh melalui panggilan metode state() dan static_cast<>, dan metode yang sesuai akan dipanggil.

Metode StoreMemory objek RiscVState relatif sederhana, tetapi memiliki beberapa implikasi yang perlu kita ketahui:

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

Seperti yang dapat kita lihat, metode ini mengambil tiga parameter, yakni pointer petunjuk itu sendiri, alamat toko, dan pointer ke DataBuffer yang berisi data toko. Perhatikan, tidak ada ukuran yang diperlukan, Instance DataBuffer sendiri berisi metode size(). Namun, tidak ada operand tujuan yang dapat diakses oleh instruksi yang dapat digunakan untuk mengalokasikan instance DataBuffer dengan ukuran yang sesuai. Sebaliknya, kita perlu gunakan factory DataBuffer yang diperoleh dari metode db_factory() di instance Instruction. Factory ini memiliki metode Allocate(int size) yang menampilkan instance DataBuffer dengan ukuran yang diperlukan. Berikut contohnya cara menggunakannya untuk mengalokasikan instance DataBuffer untuk penyimpanan setengah kata (perhatikan bahwa auto adalah fitur C++ yang menyimpulkan jenis dari sebelah kanan sisi tugas):

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

Setelah memiliki instance DataBuffer, kita dapat menulisnya seperti biasa:

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

Lalu, teruskan ke antarmuka penyimpanan memori:

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

Kita belum selesai. Instance DataBuffer dihitung sebagai referensi. Ini biasanya dipahami dan ditangani oleh metode Submit, sehingga kasus penggunaan paling sering sesederhana mungkin. Namun, StoreMemory tidak yang ditulis seperti itu. Fungsi ini akan melakukan IncRef instance DataBuffer saat beroperasi dan kemudian DecRef setelah selesai. Namun, jika fungsi semantik tidak DecRef sebagai referensinya sendiri, dan tidak akan pernah dapat diklaim kembali. Jadi, baris terakhir memiliki menjadi:

  db->DecRef();

Ada tiga fungsi toko, dan satu-satunya yang berbeda adalah ukuran akses memori. Hal ini terdengar seperti peluang bagus bagi warga lokal lainnya dengan template fungsi bantuan. Satu-satunya hal yang berbeda di seluruh fungsi store adalah jenis nilai yang disimpan, jadi template harus memilikinya sebagai argumen. Selain itu, hanya instance Instruction yang harus diteruskan:

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

Lanjutkan dan selesaikan fungsi dan build semantik toko. Anda dapat memeriksa melawan rv32i_instructions.cc.


Tambahkan petunjuk pemuatan memori

Petunjuk pemuatan yang perlu diterapkan adalah sebagai berikut:

  • lb - memuat byte, memperluas tanda menjadi sebuah kata.
  • lbu - memuat byte tanpa tanda tangan, memperluas nol ke sebuah kata.
  • lh - memuat setengah kata, menambahkan tanda ke sebuah kata.
  • lhu - memuat setengah kata tanpa tanda tangan, diperluas nol menjadi sebuah kata.
  • lw - memuat kata.

Petunjuk pemuatan adalah instruksi paling kompleks yang harus kita buat dalam model dalam tutorial ini. Mirip dengan instruksi penyimpanan, dalam hal ini, mereka harus mengakses objek RiscVState, tetapi akan menambahkan kompleksitas pada setiap pemuatan dibagi menjadi dua fungsi semantik terpisah. Yang pertama adalah mirip dengan instruksi toko, karena menghitung alamat efektif dan memulai akses memori. Yang kedua dieksekusi ketika akses selesai, dan menulis data memori ke tujuan register operand.

Mari kita mulai dengan melihat deklarasi metode LoadMemory di RiscVState:

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

Dibandingkan dengan metode StoreMemory, LoadMemory memerlukan dua tambahan parameter: pointer ke instance Instruction dan pointer ke referensi objek context yang dihitung. Yang pertama adalah instruksi turunan yang menerapkan write-back register (dijelaskan dalam tutorial decoder ISA). Ini diakses menggunakan metode child() pada instance Instruction saat ini. Yang kedua adalah pointer ke instance class yang berasal dari ReferenceCount yang dalam hal ini menyimpan instance DataBuffer yang akan memuat data yang dimuat. Objek konteks tersedia melalui Metode context() di objek Instruction (meskipun untuk sebagian besar petunjuk ini disetel ke nullptr).

Objek konteks untuk pemuatan memori RiscV ditentukan sebagai struct berikut:

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

Petunjuk pemuatan semuanya sama kecuali untuk ukuran data (byte, setengah kata, dan kata) dan apakah nilai yang dimuat adalah {i>sign-extended<i} atau tidak. Tujuan yang kedua hanya memperhitungkan petunjuk child. Mari kita buat {i>template<i} fungsi bantuan untuk petunjuk pemuatan utama. Ini akan sangat mirip dengan instruksi simpan, hanya saja tidak akan mengakses operand sumber untuk mendapatkan nilai, dan akan membuat objek konteks.

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

Seperti yang dapat Anda lihat, perbedaan utamanya adalah instance DataBuffer yang dialokasikan diteruskan ke panggilan LoadMemory sebagai parameter, serta disimpan di Objek LoadContext.

Fungsi semantik petunjuk turunan semuanya sangat mirip. Pertama, LoadContext diperoleh dengan memanggil metode Instruction context(), dan transmisi statis ke LoadContext *. Kedua, nilai (sesuai dengan data ) dibaca dari instance DataBuffer pemuatan. Ketiga, pendekatan Instance DataBuffer dialokasikan dari operand tujuan. Terakhir, nilai yang dimuat ditulis ke instance DataBuffer baru, dan Submit. Sekali lagi, menggunakan template fungsi bantuan adalah ide yang bagus:

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

Lanjutkan dan terapkan fungsi bantuan dan fungsi semantik terakhir ini. Bayar perhatikan jenis data yang Anda gunakan dalam {i>template<i} untuk setiap fungsi bantuan dan sesuai dengan ukuran serta sifat beban yang ditandatangani/tidak ditandatangani instruksi.

Anda dapat memeriksa pekerjaan Anda dengan rv32i_instructions.cc.


Membangun dan menjalankan simulator akhir

Sekarang kita telah menyelesaikan semua tugas yang sulit kita bisa membuat simulator akhir. Tujuan perpustakaan C++ tingkat atas yang menyatukan semua pekerjaan dalam tutorial ini adalah yang berada di other/. Tidak perlu melihat terlalu keras pada kode tersebut. Rab akan mengunjungi topik tersebut dalam tutorial lanjutan mendatang.

Ubah direktori kerja Anda menjadi other/, lalu build. Model tersebut harus dibangun tanpa yang sama.

$ cd ../other
$ bazel build :rv32i_sim

Di direktori tersebut, ada menu "{i> hello world<i}" sederhana program dalam file hello_rv32i.elf. Untuk menjalankan simulator pada file ini dan melihat hasilnya:

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

Anda akan melihat sesuatu seperti:

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
$

Simulator juga dapat dijalankan dalam mode interaktif menggunakan perintah bazel run :rv32i_sim -- -i other/hello_rv32i.elf. Ini memunculkan shell perintah. Ketikkan help pada perintah untuk melihat perintah yang tersedia.

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

Demikianlah tutorial ini. Semoga informasi ini bermanfaat.