নির্দেশ শব্দার্থিক ফাংশন টিউটোরিয়াল

এই টিউটোরিয়ালের উদ্দেশ্য হল:

  • শিখুন কিভাবে শব্দার্থিক ফাংশন নির্দেশের শব্দার্থ প্রয়োগ করতে ব্যবহৃত হয়।
  • আইএসএ ডিকোডার বর্ণনার সাথে শব্দার্থিক ফাংশনগুলি কীভাবে সম্পর্কিত তা জানুন।
  • RiscV RV32I নির্দেশাবলীর জন্য নির্দেশনা শব্দার্থিক ফাংশন লিখুন।
  • একটি ছোট "হ্যালো ওয়ার্ল্ড" চালানোর মাধ্যমে চূড়ান্ত সিমুলেটর পরীক্ষা করুন।

শব্দার্থিক ফাংশন ওভারভিউ

MPACT-Sim-এ একটি শব্দার্থিক ফাংশন হল এমন একটি ফাংশন যা একটি নির্দেশের ক্রিয়াকলাপ বাস্তবায়ন করে যাতে এর পার্শ্ব-প্রতিক্রিয়াগুলি সিমুলেটেড অবস্থায় দৃশ্যমান হয় যেভাবে নির্দেশের পার্শ্ব-প্রতিক্রিয়াগুলি হার্ডওয়্যারে কার্যকর করার সময় দৃশ্যমান হয়। প্রতিটি ডিকোড করা নির্দেশের সিমুলেটরের অভ্যন্তরীণ উপস্থাপনায় একটি কলযোগ্য থাকে যা সেই নির্দেশের জন্য শব্দার্থিক ফাংশনকে কল করতে ব্যবহৃত হয়।

একটি শব্দার্থিক ফাংশনের একটি স্বাক্ষর void(Instruction *) থাকে, অর্থাৎ, একটি ফাংশন যা Instruction শ্রেণীর একটি উদাহরণে একটি পয়েন্টার নিয়ে যায় এবং void প্রদান করে।

Instruction শ্রেণী instruction.h এ সংজ্ঞায়িত করা হয়েছে

শব্দার্থিক ফাংশন লেখার উদ্দেশ্যে আমরা Source(int i) এবং Destination(int i) পদ্ধতি কল ব্যবহার করে অ্যাক্সেস করা উৎস এবং গন্তব্য অপারেন্ড ইন্টারফেস ভেক্টরগুলিতে বিশেষভাবে আগ্রহী।

উৎস এবং গন্তব্য অপারেন্ড ইন্টারফেস নীচে দেখানো হয়েছে:

// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
 public:
  // Methods for accessing the nth value element.
  virtual bool AsBool(int index) = 0;
  virtual int8_t AsInt8(int index) = 0;
  virtual uint8_t AsUint8(int index) = 0;
  virtual int16_t AsInt16(int index) = 0;
  virtual uint16_t AsUint16(int) = 0;
  virtual int32_t AsInt32(int index) = 0;
  virtual uint32_t AsUint32(int index) = 0;
  virtual int64_t AsInt64(int index) = 0;
  virtual uint64_t AsUint64(int index) = 0;

  // Return a pointer to the object instance that implements the state in
  // question (or nullptr) if no such object "makes sense". This is used if
  // the object requires additional manipulation - such as a fifo that needs
  // to be pop'ed. If no such manipulation is required, nullptr should be
  // returned.
  virtual std::any GetObject() const = 0;

  // Return the shape of the operand (the number of elements in each dimension).
  // For instance {1} indicates a scalar quantity, whereas {128} indicates an
  // 128 element vector quantity.
  virtual std::vector<int> shape() const = 0;

  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;

  virtual ~SourceOperandInterface() = default;
};
// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
 public:
  virtual ~DestinationOperandInterface() = default;
  // Allocates a data buffer with ownership, latency and delay line set up.
  virtual DataBuffer *AllocateDataBuffer() = 0;
  // Takes an existing data buffer, and initializes it for the destination
  // as if AllocateDataBuffer had been called.
  virtual void InitializeDataBuffer(DataBuffer *db) = 0;
  // Allocates and initializes data buffer as if AllocateDataBuffer had been
  // called, but also copies in the value from the current value of the
  // destination.
  virtual DataBuffer *CopyDataBuffer() = 0;
  // Returns the latency associated with the destination operand.
  virtual int latency() const = 0;
  // Return a pointer to the object instance that implmements the state in
  // question (or nullptr if no such object "makes sense").
  virtual std::any GetObject() const = 0;
  // Returns the order of the destination operand (size in each dimension).
  virtual std::vector<int> shape() const = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
};

একটি সাধারণ 3 অপারেন্ড নির্দেশের জন্য একটি শব্দার্থিক ফাংশন লেখার প্রাথমিক উপায় যেমন একটি 32-বিট add নির্দেশনা নিম্নরূপ:

void MyAddFunction(Instruction *inst) {
  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);
  uint32_t c = a + b;
  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
  db->Set<uint32_t>(0, c);
  db->Submit();
}

আসুন এই ফাংশনের টুকরোগুলি ভেঙে দেওয়া যাক। ফাংশন বডির দুটি প্রথম লাইন সোর্স অপারেন্ড 0 এবং 1 থেকে পড়ে AsUint32(0) কল অন্তর্নিহিত ডেটাকে একটি uint32_t অ্যারে হিসাবে ব্যাখ্যা করে এবং 0 তম উপাদান নিয়ে আসে। অন্তর্নিহিত রেজিস্টার বা মান অ্যারে মূল্যবান কিনা তা নির্বিশেষে এটি সত্য। সোর্স অপারেন্ডের আকার (উপাদানের মধ্যে) সোর্স অপারেন্ড পদ্ধতি shape() থেকে প্রাপ্ত করা যেতে পারে, যা প্রতিটি মাত্রায় উপাদানের সংখ্যা ধারণকারী একটি ভেক্টর প্রদান করে। এই পদ্ধতিটি একটি স্কেলারের জন্য {1} , একটি 16 উপাদান ভেক্টরের জন্য {16} এবং একটি 4x4 অ্যারের জন্য {4, 4} প্রদান করে।

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

তারপর c নামের একটি uint32_t অস্থায়ী মান a + b নির্ধারণ করা হয়।

পরবর্তী লাইনে একটু বেশি ব্যাখ্যার প্রয়োজন হতে পারে:

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

একটি ডেটাবাফার হল একটি রেফারেন্স কাউন্ট করা অবজেক্ট যা রেজিস্টারের মতো সিমুলেটেড অবস্থায় মান সংরক্ষণ করতে ব্যবহৃত হয়। এটি তুলনামূলকভাবে টাইপ করা হয়নি, যদিও এটি যে বস্তু থেকে বরাদ্দ করা হয়েছে তার উপর ভিত্তি করে এটির আকার রয়েছে। এই ক্ষেত্রে, সেই আকার হল sizeof(uint32_t) । এই বিবৃতিটি গন্তব্যের জন্য একটি নতুন ডেটা বাফার বরাদ্দ করে যা এই গন্তব্য অপারেন্ডের লক্ষ্য - এই ক্ষেত্রে একটি 32-বিট পূর্ণসংখ্যা রেজিস্টার। DataBuffer নির্দেশের জন্য আর্কিটেকচারাল লেটেন্সি সহ আরম্ভ করা হয়েছে। এটি নির্দেশ ডিকোডের সময় নির্দিষ্ট করা হয়।

পরের লাইনটি ডাটা বাফার ইনস্ট্যান্সকে uint32_t এর একটি অ্যারে হিসাবে বিবেচনা করে এবং c এ সংরক্ষিত মানটিকে 0 তম উপাদানে লেখে।

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

অবশেষে, শেষ বিবৃতি সিমুলেটরে ডেটা বাফার জমা দেয় যাতে নির্দেশের লেটেন্সি সেট করা হয় যখন নির্দেশটি ডিকোড করা হয় এবং গন্তব্য অপারেন্ড ভেক্টরকে লক্ষ্য মেশিনের অবস্থার (এই ক্ষেত্রে একটি রেজিস্টার) নতুন মান হিসাবে ব্যবহার করা হয়। জনবহুল

যদিও এটি একটি যুক্তিসঙ্গতভাবে সংক্ষিপ্ত ফাংশন, এটিতে কিছুটা বয়লারপ্লেট কোড রয়েছে যা নির্দেশের পরে নির্দেশ বাস্তবায়ন করার সময় পুনরাবৃত্তিমূলক হয়ে ওঠে। উপরন্তু, এটি নির্দেশের প্রকৃত শব্দার্থকে অস্পষ্ট করতে পারে। বেশিরভাগ নির্দেশের জন্য শব্দার্থিক ফাংশনগুলিকে আরও সহজ করার জন্য, instruction_helpers.h- এ সংজ্ঞায়িত বেশ কয়েকটি টেমপ্লেটেড হেল্পার ফাংশন রয়েছে। এই সাহায্যকারীরা এক, দুই বা তিনটি সোর্স অপারেন্ড এবং একটি একক গন্তব্য অপারেন্ড সহ নির্দেশনার জন্য বয়লারপ্লেট কোড লুকিয়ে রাখে। আসুন দুটি অপারেন্ড হেল্পার ফাংশন দেখে নেওয়া যাক:

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Argument1, Argument2)> operation) {
  Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
  Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Argument, Argument)> operation) {
  Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
  Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Result, Result)> operation) {
  Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
  Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

আপনি একটি বিবৃতি ব্যবহার করার পরিবর্তে লক্ষ্য করবেন যে:

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

সহায়ক ফাংশন ব্যবহার করে:

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

GetInstructionSource হল টেমপ্লেটেড হেল্পার ফাংশনগুলির একটি পরিবার যা নির্দেশনা উত্স অপারেন্ডগুলিতে টেমপ্লেটেড অ্যাক্সেস পদ্ধতি প্রদান করতে ব্যবহৃত হয়। এগুলি ছাড়া প্রতিটি নির্দেশ সহায়ক ফাংশনকে সঠিক As<int type>() ফাংশন সহ সোর্স অপারেন্ড অ্যাক্সেস করতে প্রতিটি ধরণের জন্য বিশেষায়িত করতে হবে। আপনি instruction.h- এ এই টেমপ্লেট ফাংশনগুলির সংজ্ঞা দেখতে পারেন।

আপনি দেখতে পাচ্ছেন যে সোর্স অপারেন্ডের ধরনগুলি গন্তব্যের মতো একই কিনা, গন্তব্যটি উত্স থেকে আলাদা কিনা বা সেগুলি আলাদা কিনা তার উপর নির্ভর করে তিনটি বাস্তবায়ন রয়েছে৷ ফাংশনের প্রতিটি সংস্করণ নির্দেশের উদাহরণের পাশাপাশি একটি কলযোগ্য (যার মধ্যে ল্যাম্বডা ফাংশন রয়েছে) একটি পয়েন্টার নেয়। এর মানে হল যে আমরা এখন উপরের add সিমেন্টিক ফাংশনটি নিম্নরূপ পুনরায় লিখতে পারি:

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

যখন বিল্ড ফাইলে bazel build -c opt এবং copts = ["-O3"] দিয়ে কম্পাইল করা হয়, তখন এটি কোনো ওভারহেড ছাড়াই সম্পূর্ণভাবে ইনলাইন করা উচিত, যা আমাদের কোনো পারফরম্যান্স পেনাল্টি ছাড়াই নোটেশনাল সংক্ষিপ্ততা দেয়।

উল্লিখিত হিসাবে ইউনারী, বাইনারি এবং ত্রিনারি স্কেলার নির্দেশাবলীর পাশাপাশি ভেক্টর সমতুল্যগুলির জন্য সহায়ক ফাংশন রয়েছে। এগুলি সাধারণ ছাঁচের সাথে খাপ খায় না এমন নির্দেশাবলীর জন্য আপনার নিজস্ব সাহায্যকারী তৈরি করার জন্য দরকারী টেমপ্লেট হিসাবেও কাজ করে।


প্রাথমিক নির্মাণ

আপনি যদি ডিরেক্টরিকে riscv_semantic_functions এ পরিবর্তন না করে থাকেন, তাহলে এখনই করুন। তারপরে নিম্নরূপ প্রকল্পটি তৈরি করুন - এই বিল্ডটি সফল হওয়া উচিত।

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

সেখানে কোনো ফাইল তৈরি হয় না, তাই সবকিছু ঠিকঠাক আছে কিনা তা নিশ্চিত করার জন্য এটি সত্যিই একটি ড্রাই রান।


তিনটি অপারেন্ড ALU নির্দেশাবলী যোগ করুন

এখন কিছু জেনেরিক, 3-অপারেন্ড ALU নির্দেশের জন্য শব্দার্থিক ফাংশন যোগ করা যাক। rv32i_instructions.cc ফাইলটি খুলুন এবং নিশ্চিত করুন যে কোনো অনুপস্থিত সংজ্ঞা rv32i_instructions.h ফাইলটিতে যোগ করা হয়েছে।

আমরা যে নির্দেশাবলী যোগ করব তা হল:

  • add - 32-বিট পূর্ণসংখ্যা যোগ করুন।
  • and - 32-বিট বিটওয়াইজ এবং।
  • or - 32-বিট বিটওয়াইজ বা।
  • sll - 32-বিট লজিক্যাল শিফট বাম।
  • sltu - 32-বিট স্বাক্ষরবিহীন সেট-এর চেয়ে কম।
  • sra - 32-বিট গাণিতিক ডান স্থানান্তর।
  • srl - 32-বিট লজিক্যাল ডান শিফট।
  • sub - 32-বিট পূর্ণসংখ্যা বিয়োগ।
  • xor - 32-বিট বিটওয়াইজ xor।

আপনি যদি আগের টিউটোরিয়ালগুলি করে থাকেন তবে আপনি মনে করতে পারেন যে আমরা ডিকোডারে রেজিস্টার-রেজিস্টার নির্দেশাবলী এবং রেজিস্টার-তাত্ক্ষণিক নির্দেশাবলীর মধ্যে পার্থক্য করেছি। এটি শব্দার্থিক ফাংশন আসে, আমাদের আর এটি করতে হবে না. অপারেন্ড ইন্টারফেসগুলি অপারেন্ডটি যেটি থেকে অপারেন্ডের মান পড়বে, নিবন্ধন বা অবিলম্বে, শব্দার্থিক ফাংশনটি সম্পূর্ণরূপে অজ্ঞেয় সহকারে অন্তর্নিহিত উৎস অপারেন্ডটি আসলে কী।

sra ব্যতীত, উপরের সমস্ত নির্দেশাবলী 32-বিট স্বাক্ষরবিহীন মানগুলিতে কাজ করে বলে গণ্য করা যেতে পারে, তাই এর জন্য আমরা BinaryOp টেমপ্লেট ফাংশনটি ব্যবহার করতে পারি যা আমরা আগে শুধুমাত্র একক টেমপ্লেট টাইপ আর্গুমেন্ট দিয়ে দেখেছিলাম। সেই অনুযায়ী rv32i_instructions.cc এ ফাংশন বডি পূরণ করুন। উল্লেখ্য যে দ্বিতীয় অপারেন্ড থেকে শিফ্ট নির্দেশাবলীর শুধুমাত্র নিম্ন 5 বিট শিফট পরিমাণের জন্য ব্যবহৃত হয়। অন্যথায়, সমস্ত অপারেশন ফর্ম src0 op src1 :

  • add : a + b
  • and : a & b
  • or : a | b
  • sll : a << (b & 0x1f)
  • sltu : (a < b) ? 1 : 0
  • srl : a >> (b & 0x1f)
  • sub : a - b
  • xor : a ^ b

sra জন্য আমরা তিনটি আর্গুমেন্ট BinaryOp টেমপ্লেট ব্যবহার করব। টেমপ্লেটের দিকে তাকিয়ে, প্রথম ধরনের আর্গুমেন্ট হল ফলাফলের প্রকার uint32_t । দ্বিতীয়টি হল সোর্স অপারেন্ড 0 এর ধরন, এক্ষেত্রে int32_t , এবং শেষটি হল সোর্স অপারেন্ড 1 এর ধরন, এক্ষেত্রে uint32_t । এটি sra শব্দার্থিক ফাংশনের শরীরকে তৈরি করে:

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

এগিয়ে যান এবং পরিবর্তন করুন এবং নির্মাণ করুন. আপনি rv32i_instructions.cc এর বিরুদ্ধে আপনার কাজ পরীক্ষা করতে পারেন।


দুটি অপারেন্ড ALU নির্দেশাবলী যোগ করুন

শুধুমাত্র দুটি 2-অপারেন্ড ALU নির্দেশাবলী রয়েছে: lui এবং auipc । প্রাক্তনটি প্রি-শিফ্ট করা সোর্স অপারেন্ডকে সরাসরি গন্তব্যে কপি করে। পরবর্তীটি গন্তব্যে লেখার আগে অবিলম্বে নির্দেশের ঠিকানা যোগ করে। নির্দেশের ঠিকানা Instruction বস্তুর address() পদ্ধতি থেকে অ্যাক্সেসযোগ্য।

যেহেতু শুধুমাত্র একটি সোর্স অপারেন্ড আছে, তাই আমরা BinaryOp ব্যবহার করতে পারি না, পরিবর্তে আমাদের UnaryOp ব্যবহার করতে হবে। যেহেতু আমরা উৎস এবং গন্তব্য অপারেন্ড উভয়কেই uint32_t হিসাবে বিবেচনা করতে পারি আমরা একক আর্গুমেন্ট টেমপ্লেট সংস্করণ ব্যবহার করতে পারি।

// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
                    std::function<Result(Argument)> operation) {
  Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
  Result dest_value = operation(lhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
                    std::function<Result(Result)> operation) {
  Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
  Result dest_value = operation(lhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

lui এর শব্দার্থিক ফাংশনের মূল অংশটি যতটা তুচ্ছ হতে পারে, শুধু উৎসটি ফেরত দিন। auipc এর জন্য শব্দার্থিক ফাংশন একটি ছোটখাটো সমস্যা প্রবর্তন করে, যেহেতু আপনাকে Instruction উদাহরণে address() পদ্ধতি অ্যাক্সেস করতে হবে। উত্তর হল ল্যাম্বডা ক্যাপচারে instruction যোগ করা, এটি ল্যাম্বডা ফাংশন বডিতে ব্যবহারের জন্য উপলব্ধ করে। আগের মতো [](uint32_t a) { ... } এর পরিবর্তে, ল্যাম্বডা লিখতে হবে [instruction](uint32_t a) { ... } এখন ল্যাম্বডা শরীরে instruction ব্যবহার করা যেতে পারে।

এগিয়ে যান এবং পরিবর্তন করুন এবং নির্মাণ করুন. আপনি rv32i_instructions.cc এর বিরুদ্ধে আপনার কাজ পরীক্ষা করতে পারেন।


নিয়ন্ত্রণ প্রবাহ পরিবর্তন নির্দেশাবলী যোগ করুন

কন্ট্রোল ফ্লো পরিবর্তনের নির্দেশাবলী যেগুলি আপনাকে প্রয়োগ করতে হবে সেগুলি শর্তসাপেক্ষ শাখা নির্দেশাবলীতে বিভক্ত করা হয়েছে (একটি তুলনা সত্য হলে সঞ্চালিত করা হয় ছোট শাখা), এবং জাম্প-এন্ড-লিঙ্ক নির্দেশাবলী, যা ফাংশন কলগুলি (-এন্ড-লিঙ্ক) প্রয়োগ করতে ব্যবহৃত হয় লিংক রেজিস্টারকে শূন্যে সেট করে অপসারণ করা হয়, যেগুলোকে নো-অপস করে)।

শর্তসাপেক্ষ শাখা নির্দেশাবলী যোগ করুন

শাখা নির্দেশের জন্য কোন সহায়ক ফাংশন নেই, তাই দুটি বিকল্প আছে। স্ক্র্যাচ থেকে শব্দার্থিক ফাংশন লিখুন, বা একটি স্থানীয় সহায়ক ফাংশন লিখুন। যেহেতু আমাদের 6টি শাখা নির্দেশাবলী বাস্তবায়ন করতে হবে, তাই পরেরটি প্রচেষ্টার মূল্য বলে মনে হয়। আমরা এটি করার আগে, আসুন শুরু থেকে একটি শাখা নির্দেশনা শব্দার্থিক ফাংশন বাস্তবায়ন তাকান.

void MyConditionalBranchGreaterEqual(Instruction *instruction) {
  int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
  int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
  if (a >= b) {
    uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
    uint32_t target = offset + instruction->address();
    DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
    db->Set<uint32_t>(0,m target);
    db->Submit();
  }
}

শাখা নির্দেশাবলী জুড়ে শুধুমাত্র যে জিনিসটি পরিবর্তিত হয় তা হল শাখার অবস্থা, এবং দুটি উৎস অপারেন্ডের ডেটা প্রকার, স্বাক্ষরিত বনাম স্বাক্ষরবিহীন 32 বিট int। তার মানে সোর্স অপারেন্ডের জন্য আমাদের একটি টেমপ্লেট প্যারামিটার থাকতে হবে। সহায়ক ফাংশনকে নিজেই Instruction উদাহরণ এবং একটি কলযোগ্য বস্তু যেমন std::function নিতে হবে যা পরামিতি হিসাবে bool প্রদান করে। সহায়ক ফাংশন দেখতে হবে:

template <typename OperandType>
static inline void BranchConditional(
    Instruction *instruction,
    std::function<bool(OperandType, OperandType)> cond) {
  OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
  OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
  if (cond(a, b)) {
    uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
    uint32_t target = offset + instruction->address();
    DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
    db->Set<uint32_t>(0, target);
    db->Submit();
  }
}

এখন আমরা bge (স্বাক্ষরিত শাখা বড় বা সমান) শব্দার্থিক ফাংশন লিখতে পারি:

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

অবশিষ্ট শাখা নির্দেশাবলী নিম্নরূপ:

  • Beq - শাখা সমান।
  • Bgeu - শাখা বড় বা সমান (স্বাক্ষরবিহীন)।
  • Blt - এর চেয়ে কম শাখা (স্বাক্ষরিত)।
  • Bltu - এর চেয়ে কম শাখা (স্বাক্ষরবিহীন)।
  • Bne - শাখা সমান নয়।

এগিয়ে যান এবং এই শব্দার্থিক ফাংশনগুলি বাস্তবায়নের জন্য পরিবর্তনগুলি করুন এবং পুনরায় তৈরি করুন৷ আপনি rv32i_instructions.cc এর বিরুদ্ধে আপনার কাজ পরীক্ষা করতে পারেন।

জাম্প এবং লিঙ্ক নির্দেশাবলীর জন্য একটি সহায়ক ফাংশন লেখার কোন মানে নেই, তাই আমাদের স্ক্র্যাচ থেকে লিখতে হবে। চলুন শুরু করা যাক তাদের নির্দেশের শব্দার্থবিদ্যা দেখে।

jal নির্দেশনাটি সোর্স অপারেন্ড 0 থেকে একটি অফসেট নেয় এবং জাম্প টার্গেট গণনা করতে বর্তমান পিসিতে (নির্দেশনা ঠিকানা) যোগ করে। জাম্প টার্গেট গন্তব্য অপারেন্ড 0 এ লেখা হয়। রিটার্ন ঠিকানা হল পরবর্তী ক্রমিক নির্দেশের ঠিকানা। এটির ঠিকানায় বর্তমান নির্দেশের আকার যোগ করে এটি গণনা করা যেতে পারে। ফিরতি ঠিকানা গন্তব্য অপারেন্ডে লেখা হয় 1. ল্যাম্বডা ক্যাপচারে নির্দেশ অবজেক্ট পয়েন্টার অন্তর্ভুক্ত করতে মনে রাখবেন।

jalr নির্দেশনা সোর্স অপারেন্ড 0 হিসাবে একটি বেস রেজিস্টার এবং সোর্স অপারেন্ড 1 হিসাবে একটি অফসেট নেয় এবং জাম্প টার্গেট গণনা করার জন্য তাদের একসাথে যুক্ত করে। অন্যথায় এটি jal নির্দেশের সাথে অভিন্ন।

নির্দেশের শব্দার্থবিদ্যার এই বর্ণনার উপর ভিত্তি করে, দুটি শব্দার্থিক ফাংশন লিখুন এবং তৈরি করুন। আপনি rv32i_instructions.cc এর বিরুদ্ধে আপনার কাজ পরীক্ষা করতে পারেন।


মেমরি স্টোর নির্দেশাবলী যোগ করুন

তিনটি স্টোর নির্দেশাবলী রয়েছে যা আমাদের বাস্তবায়ন করতে হবে: স্টোর বাইট ( sb ), স্টোর হাফওয়ার্ড ( sh ) এবং স্টোর ওয়ার্ড ( sw )। স্টোরের নির্দেশাবলী আমরা এখন পর্যন্ত যে নির্দেশাবলী প্রয়োগ করেছি তার থেকে আলাদা যে তারা স্থানীয় প্রসেসর স্টেটে লেখে না। পরিবর্তে তারা একটি সিস্টেম সম্পদ - প্রধান মেমরি লিখতে. MPACT-Sim মেমরিকে একটি নির্দেশ অপারেন্ড হিসাবে বিবেচনা করে না, তাই মেমরি অ্যাক্সেস অন্য পদ্ধতি ব্যবহার করে সম্পাদন করতে হবে।

উত্তর হল MPACT-Sim ArchState অবজেক্টে মেমরি অ্যাক্সেস পদ্ধতি যোগ করা, অথবা আরও সঠিকভাবে, একটি নতুন RiscV স্টেট অবজেক্ট তৈরি করুন যা ArchState থেকে প্রাপ্ত হয় যেখানে এটি যোগ করা যেতে পারে। ArchState অবজেক্ট মূল সম্পদ পরিচালনা করে, যেমন রেজিস্টার এবং অন্যান্য রাষ্ট্রীয় বস্তু। এটি গন্তব্য অপারেন্ড ডেটা বাফারগুলিকে বাফার করার জন্য ব্যবহৃত বিলম্ব লাইনগুলি পরিচালনা করে যতক্ষণ না সেগুলি নিবন্ধিত বস্তুগুলিতে লেখা না হয়। বেশিরভাগ নির্দেশনা এই শ্রেণীর জ্ঞান ছাড়াই প্রয়োগ করা যেতে পারে, কিন্তু কিছু, যেমন মেমরি অপারেশন এবং অন্যান্য নির্দিষ্ট সিস্টেম নির্দেশাবলীর জন্য এই স্টেট অবজেক্টে থাকার জন্য কার্যকারিতা প্রয়োজন।

আসুন একটি উদাহরণ হিসাবে rv32i_instructions.cc এ ইতিমধ্যেই বাস্তবায়িত হওয়া fence নির্দেশনার শব্দার্থিক ফাংশনটি একবার দেখে নেওয়া যাক। নির্দিষ্ট মেমরি ক্রিয়াকলাপ সম্পূর্ণ না হওয়া পর্যন্ত fence নির্দেশে নির্দেশের সমস্যা রয়েছে। এটি নির্দেশের আগে নির্বাহিত নির্দেশাবলী এবং পরে কার্যকর করা নির্দেশাবলীর মধ্যে মেমরি অর্ডারিং গ্যারান্টি দিতে ব্যবহৃত হয়।

// Fence.
void RV32IFence(Instruction *instruction) {
  uint32_t bits = instruction->Source(0)->AsUint32(0);
  int fm = (bits >> 8) & 0xf;
  int predecessor = (bits >> 4) & 0xf;
  int successor = bits & 0xf;
  auto *state = static_cast<RiscVState *>(instruction->state());
  state->Fence(instruction, fm, predecessor, successor);
}

fence নির্দেশের শব্দার্থিক ফাংশনের মূল অংশ হল শেষ দুটি লাইন। প্রথমে স্টেট অবজেক্ট Instruction ক্লাসে একটি পদ্ধতি ব্যবহার করে আনা হয় এবং downcast<> RiscV নির্দিষ্ট প্রাপ্ত ক্লাসে। তারপর RiscVState ক্লাসের Fence পদ্ধতিটিকে বেড়া অপারেশন করতে বলা হয়।

স্টোর নির্দেশাবলী একইভাবে কাজ করবে। প্রথমে মেমরি অ্যাক্সেসের কার্যকর ঠিকানাটি বেস এবং অফসেট নির্দেশনা উত্স অপারেন্ড থেকে গণনা করা হয়, তারপরে সংরক্ষণ করা মানটি পরবর্তী উত্স অপারেন্ড থেকে আনা হয়। এরপর, RiscV স্টেট অবজেক্টটি state() মেথড কল এবং static_cast<> মাধ্যমে প্রাপ্ত হয় এবং উপযুক্ত পদ্ধতি বলা হয়।

RiscVState অবজেক্ট StoreMemory পদ্ধতিটি তুলনামূলকভাবে সহজ, তবে এর কয়েকটি প্রভাব রয়েছে যা আমাদের সচেতন হওয়া দরকার:

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

আমরা দেখতে পাচ্ছি, পদ্ধতিটি তিনটি পরামিতি নেয়, স্টোরের নির্দেশের নির্দেশক, দোকানের ঠিকানা, এবং স্টোর ডেটা ধারণ করে এমন একটি DataBuffer উদাহরণের একটি পয়েন্টার। লক্ষ্য করুন, কোন আকারের প্রয়োজন নেই, DataBuffer উদাহরণে নিজেই একটি size() পদ্ধতি রয়েছে। যাইহোক, নির্দেশে অ্যাক্সেসযোগ্য কোনও গন্তব্য অপারেন্ড নেই যা উপযুক্ত আকারের DataBuffer উদাহরণ বরাদ্দ করতে ব্যবহার করা যেতে পারে। পরিবর্তে আমাদের একটি DataBuffer ফ্যাক্টরি ব্যবহার করতে হবে যা Instruction উদাহরণে db_factory() পদ্ধতি থেকে প্রাপ্ত হয়। কারখানায় একটি পদ্ধতি Allocate(int size) রয়েছে যা প্রয়োজনীয় আকারের একটি DataBuffer উদাহরণ প্রদান করে। একটি অর্ধ-শব্দ স্টোরের জন্য একটি DataBuffer উদাহরণ বরাদ্দ করতে কীভাবে এটি ব্যবহার করতে হয় তার একটি উদাহরণ এখানে দেওয়া হল (মনে রাখবেন auto হল একটি C++ বৈশিষ্ট্য যা অ্যাসাইনমেন্টের ডান দিক থেকে ধরণ নির্ণয় করে):

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

একবার আমাদের কাছে DataBuffer দৃষ্টান্ত থাকলে আমরা এটিকে যথারীতি লিখতে পারি:

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

তারপর এটি মেমরি স্টোর ইন্টারফেসে পাস করুন:

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

আমরা এখনও পুরোপুরি সম্পন্ন না. DataBuffer দৃষ্টান্ত রেফারেন্স গণনা করা হয়. এটি সাধারণত Submit পদ্ধতি দ্বারা বোঝা যায় এবং পরিচালনা করা হয়, যাতে সর্বাধিক ঘন ঘন ব্যবহারের ক্ষেত্রে যতটা সম্ভব সহজ রাখা যায়। যাইহোক, StoreMemory সেভাবে লেখা হয় না। এটি DataBuffer ইনস্ট্যান্সকে IncRef করবে যখন এটি এটিতে কাজ করে এবং তারপর সম্পন্ন হলে DecRef । যাইহোক, যদি শব্দার্থিক ফাংশনটি তার নিজস্ব রেফারেন্স DecRef না করে তবে এটি কখনই পুনরুদ্ধার করা হবে না। সুতরাং, শেষ লাইনটি হতে হবে:

  db->DecRef();

তিনটি স্টোর ফাংশন আছে, এবং মেমরি অ্যাক্সেসের আকার পার্থক্য শুধুমাত্র জিনিস. এটি অন্য স্থানীয় টেমপ্লেটেড হেল্পার ফাংশনের জন্য একটি দুর্দান্ত সুযোগ বলে মনে হচ্ছে। স্টোর ফাংশন জুড়ে একমাত্র জিনিসটি হল স্টোর মানের প্রকার, তাই টেমপ্লেটটিতে এটি একটি যুক্তি হিসাবে থাকতে হবে। তা ছাড়া, শুধুমাত্র Instruction উদাহরণটি পাস করতে হবে:

template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
  auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
  auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
  uint32_t address = base + offset;
  auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(ValueType));
  db->Set<ValueType>(0, value);
  state->StoreMemory(instruction, address, db);
  db->DecRef();
}

এগিয়ে যান এবং স্টোর শব্দার্থিক ফাংশন শেষ করুন এবং তৈরি করুন। আপনি rv32i_instructions.cc এর বিরুদ্ধে আপনার কাজ পরীক্ষা করতে পারেন।


মেমরি লোড নির্দেশাবলী যোগ করুন

লোড নির্দেশাবলী যা বাস্তবায়ন করা প্রয়োজন তা হল:

  • lb - লোড বাইট, একটি শব্দে সাইন-এক্সটেন্ড করুন।
  • lbu - লোড বাইট স্বাক্ষরবিহীন, শূন্য-একটি শব্দে প্রসারিত করুন।
  • lh - অর্ধ-শব্দ লোড করুন, একটি শব্দে সাইন-প্রসারিত করুন।
  • lhu - অর্ধ-শব্দ স্বাক্ষরবিহীন লোড করুন, একটি শব্দে শূন্য-প্রসারিত করুন।
  • lw - লোড শব্দ।

লোড নির্দেশাবলী হল সবচেয়ে জটিল নির্দেশাবলী যা আমাদের এই টিউটোরিয়ালে মডেল করতে হবে। এগুলি স্টোরের নির্দেশাবলীর অনুরূপ, যাতে তাদের RiscVState অবজেক্ট অ্যাক্সেস করতে হয়, কিন্তু জটিলতা যোগ করে যে প্রতিটি লোড নির্দেশাবলী দুটি পৃথক শব্দার্থিক ফাংশনে বিভক্ত। প্রথমটি দোকান নির্দেশের অনুরূপ, এতে এটি কার্যকর ঠিকানা গণনা করে এবং মেমরি অ্যাক্সেস শুরু করে। দ্বিতীয়টি কার্যকর করা হয় যখন মেমরি অ্যাক্সেস সম্পূর্ণ হয়, এবং মেমরি ডেটা রেজিস্টার গন্তব্য অপারেন্ডে লেখে।

আসুন RiscVStateLoadMemory পদ্ধতির ঘোষণা দেখে শুরু করি:

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

StoreMemory পদ্ধতির তুলনায়, LoadMemory দুটি অতিরিক্ত প্যারামিটার নেয়: একটি Instruction উদাহরণের একটি পয়েন্টার এবং একটি রেফারেন্স গণনা করা context বস্তুর একটি পয়েন্টার। প্রথমটি হল শিশু নির্দেশনা যা রেজিস্টার রাইট-ব্যাক প্রয়োগ করে (আইএসএ ডিকোডার টিউটোরিয়ালে বর্ণিত)। বর্তমান Instruction উদাহরণে child() পদ্ধতি ব্যবহার করে এটি অ্যাক্সেস করা হয়। পরেরটি একটি ক্লাসের একটি উদাহরণের একটি নির্দেশক যা ReferenceCount থেকে প্রাপ্ত হয় যা এই ক্ষেত্রে একটি DataBuffer উদাহরণ সঞ্চয় করে যাতে লোড করা ডেটা থাকবে। প্রসঙ্গ অবজেক্টটি Instruction অবজেক্টে context() পদ্ধতির মাধ্যমে উপলব্ধ (যদিও বেশিরভাগ নির্দেশের জন্য এটি nullptr এ সেট করা হয়)।

RiscV মেমরি লোডের জন্য প্রসঙ্গ অবজেক্ট নিম্নলিখিত কাঠামো হিসাবে সংজ্ঞায়িত করা হয়েছে:

// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
  explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
  ~LoadContext() override {
    if (value_db != nullptr) value_db->DecRef();
  }

  // Override the base class method so that the data buffer can be DecRef'ed
  // when the context object is recycled.
  void OnRefCountIsZero() override {
    if (value_db != nullptr) value_db->DecRef();
    value_db = nullptr;
    // Call the base class method.
    generic::ReferenceCount::OnRefCountIsZero();
  }
  // Data buffers for the value loaded from memory (byte, half, word, etc.).
  DataBuffer *value_db = nullptr;
};

ডেটা সাইজ (বাইট, অর্ধ-শব্দ এবং শব্দ) এবং লোড করা মান সাইন-বর্ধিত কিনা তা ছাড়া লোড নির্দেশাবলী সব একই। পরেরটি শুধুমাত্র শিশুর নির্দেশনার কারণ। প্রধান লোড নির্দেশাবলীর জন্য একটি টেমপ্লেটেড হেল্পার ফাংশন তৈরি করা যাক। এটি স্টোর নির্দেশের অনুরূপ হবে, ব্যতীত এটি একটি মান পেতে উত্স অপারেন্ড অ্যাক্সেস করবে না এবং এটি একটি প্রসঙ্গ অবজেক্ট তৈরি করবে।

template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
  auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
  auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
  uint32_t address = base + offset;
  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(ValueType));
  db->set_latency(0);
  auto *context = new riscv::LoadContext(db);
  state->LoadMemory(instruction, address, db, instruction->child(), context);
  context->DecRef();
}

আপনি দেখতে পাচ্ছেন, প্রধান পার্থক্য হল যে বরাদ্দকৃত DataBuffer দৃষ্টান্ত উভয়ই একটি প্যারামিটার হিসাবে LoadMemory কলে পাস করা হয়, সেইসাথে LoadContext অবজেক্টে সংরক্ষণ করা হয়।

শিশু নির্দেশনা শব্দার্থিক ফাংশন সব খুব অনুরূপ. প্রথমত, Instruction method context() কে কল করে LoadContext প্রাপ্ত করা হয় এবং LoadContext * এ স্ট্যাটিক-কাস্ট করা হয়। দ্বিতীয়ত, মান (ডেটা টাইপ অনুযায়ী) লোড-ডেটা DataBuffer উদাহরণ থেকে পড়া হয়। তৃতীয়ত, গন্তব্য অপারেন্ড থেকে একটি নতুন DataBuffer উদাহরণ বরাদ্দ করা হয়েছে। অবশেষে, লোড করা মানটি নতুন DataBuffer ইনস্ট্যান্সে লেখা হয় এবং 'ed' Submit । আবার, একটি টেমপ্লেটেড হেল্পার ফাংশন একটি ভাল ধারণা:

template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
  auto *context = down_cast<riscv::LoadContext *>(instruction->context());
  uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->Set<uint32_t>(0, value);
  db->Submit();
}

এগিয়ে যান এবং এই শেষ সহায়ক ফাংশন এবং শব্দার্থিক ফাংশনগুলি বাস্তবায়ন করুন। প্রতিটি হেল্পার ফাংশন কলের জন্য আপনি টেমপ্লেটে যে ডেটা টাইপ ব্যবহার করেন তার প্রতি মনোযোগ দিন এবং এটি লোড নির্দেশনার আকার এবং স্বাক্ষরিত/স্বাক্ষরবিহীন প্রকৃতির সাথে সামঞ্জস্যপূর্ণ।

আপনি rv32i_instructions.cc এর বিরুদ্ধে আপনার কাজ পরীক্ষা করতে পারেন।


চূড়ান্ত সিমুলেটর তৈরি করুন এবং চালান

এখন যেহেতু আমরা সমস্ত কঠিন শব্দ করেছি আমরা চূড়ান্ত সিমুলেটর তৈরি করতে পারি। শীর্ষ স্তরের C++ লাইব্রেরিগুলি যা এই টিউটোরিয়ালগুলির সমস্ত কাজকে একত্রিত করে other/ তে অবস্থিত। এই কোডটি খুব কঠিন দেখতে হবে না। আমরা ভবিষ্যতের একটি উন্নত টিউটোরিয়ালে সেই বিষয়টি পরিদর্শন করব।

আপনার ওয়ার্কিং ডাইরেক্টরি other/ এ পরিবর্তন করুন এবং বিল্ড করুন। এটা ত্রুটি ছাড়া নির্মাণ করা উচিত.

$ cd ../other
$ bazel build :rv32i_sim

সেই ডিরেক্টরিতে hello_rv32i.elf ফাইলে একটি সাধারণ "হ্যালো ওয়ার্ল্ড" প্রোগ্রাম রয়েছে। এই ফাইলটিতে সিমুলেটর চালাতে এবং ফলাফলগুলি দেখতে:

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

আপনি এর লাইন বরাবর কিছু দেখতে হবে:

INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
  bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$

bazel run :rv32i_sim -- -i other/hello_rv32i.elf কমান্ডটি ব্যবহার করে সিমুলেটরটি একটি ইন্টারেক্টিভ মোডেও চালানো যেতে পারে। এটি একটি সাধারণ কমান্ড শেল নিয়ে আসে। উপলব্ধ কমান্ডগুলি দেখতে প্রম্পটে help টাইপ করুন।

$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
  bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000   addi           ra, 0, 0
[0] > help


    quit                           - exit command shell.
    core [N]                       - direct subsequent commands to core N
                                     (default: 0).
    run                            - run program from current pc until a
                                     breakpoint or exit. Wait until halted.
    run free                       - run program in background from current pc
                                     until breakpoint or exit.
    wait                           - wait for any free run to complete.
    step [N]                       - step [N] instructions (default: 1).
    halt                           - halt a running program.
    reg get NAME [FORMAT]          - get the value or register NAME.
    reg NAME [FORMAT]              - get the value of register NAME.
    reg set NAME VALUE             - set register NAME to VALUE.
    reg set NAME SYMBOL            - set register NAME to value of SYMBOL.
    mem get VALUE [FORMAT]         - get memory from location VALUE according to
                                     format. The format is a letter (o, d, u, x,
                                     or X) followed by width (8, 16, 32, 64).
                                     The default format is x32.
    mem get SYMBOL [FORMAT]        - get memory from location SYMBOL and format
                                     according to FORMAT (see above).
    mem SYMBOL [FORMAT]            - get memory from location SYMBOL and format
                                     according to FORMAT (see above).
    mem set VALUE [FORMAT] VALUE   - set memory at location VALUE(1) to VALUE(2)
                                     according to FORMAT. Default format is x32.
    mem set SYMBOL [FORMAT] VALUE  - set memory at location SYMBOL to VALUE
                                     according to FORMAT. Default format is x32.
    break set VALUE                - set breakpoint at address VALUE.
    break set SYMBOL               - set breakpoint at value of SYMBOL.
    break VALUE                    - set breakpoint at address VALUE.
    break SYMBOL                   - set breakpoint at value of SYMBOL.
    break clear VALUE              - clear breakpoint at address VALUE.
    break clear SYMBOL             - clear breakpoint at value of SYMBOL.
    break clear all                - remove all breakpoints.
    help                           - display this message.

_start:
80000000   addi           ra, 0, 0
[0] >

এটি এই টিউটোরিয়ালটি শেষ করে। আমরা আশা করি এটি সহায়ক হয়েছে।