برنامج تعليمي للدوال الدلالية للتعليم

تتمثل أهداف هذا البرنامج التعليمي في ما يلي:

  • اطّلِع على كيفية استخدام الدوال الدلالية لتنفيذ دلالات التعليمات.
  • تعرف على كيفية ارتباط الدوال الدلالية بوصف برنامج فك ترميز ISA.
  • كتابة الدوال الدلالية الإرشادية لتعليمات RiscV RV32I
  • اختبِر المحاكي النهائي من خلال تشغيل لعبة Hello World (أهلاً بالعالم) ملف تنفيذي.

نظرة عامة على الدوال الدلالية

الدالة الدلالية في 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 العادي مثل تعليمات add الإصدار 32 بت:

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 عنصرًا، و{4, 4} لمصفوفة 4×4.

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

بعد ذلك، تم تحديد القيمة a + b لعنصر uint32_t مؤقّت باسم c.

قد يتطلب السطر التالي المزيد من التوضيح:

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

DataBuffer هو كائن مرجعي مُعدَّل يُستخدم لتخزين القيم في مثل السجلات. إنها غير مكتوبة نسبيًا، على الرغم من أنها تحتوي على بناءً على الكائن الذي تم تخصيصه منه. في هذه الحالة، يكون هذا الحجم sizeof(uint32_t) تخصِّص هذه العبارة حجم مخزن بيانات احتياطي جديد وتكون هي الوجهة المستهدفة لمعامِل الوجهة هذا، وفي هذه الحالة تكون سجل 32 بت صحيح. يتم تهيئة مخزن البيانات أيضًا باستخدام وقت الاستجابة الهندسي للتعليم. يتم تحديد ذلك أثناء التعليمات. وفك ترميزه.

يتعامل السطر التالي مع مثيل المخزن المؤقت للبيانات كمصفوفة 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.

كما ترى، هناك ثلاث عمليات تنفيذ، اعتمادًا على ما إذا كان المصدر تكون أنواع المعاملات هي نفسها الوجهة، سواء كانت الوجهة أو ما إذا كانت مختلفة عن المصادر. كل إصدار من تأخذ الدالة مؤشرًا إلى مثيل التعليمات بالإضافة إلى عنصر قابل للاستدعاء (بما في ذلك دوال lambda). وهذا يعني أنه يمكننا الآن إعادة كتابة 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

لنُضف الآن الدوال الدلالية لبعض وحدات 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 تُستخدم وحدات بت من المعامل الثاني لتعليمات shift المبلغ. وبخلاف ذلك، ستكون جميع العمليات بالشكل 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

هناك فقط اثنان من التعليمات المتعلقة بـ ALU بعاملَين: lui وauipc. السابق ينسخ مُعامل المصدر الذي تم نقله مسبقًا إلى الوجهة مباشرةً. الخيار الأخير تضيف عنوان التعليمات إلى الشخص المباشر قبل كتابتها إلى الوجهة. يمكن الوصول إلى عنوان التعليمات من خلال طريقة address() لكائن Instruction.

بما أن هناك معامل مصدر واحد فقط، لا يمكننا استخدام 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 حرف ثانوي حيث إنك بحاجة إلى الوصول إلى طريقة address() في Instruction مثال. والإجابة عن هذا السؤال هي إضافة instruction إلى التقاط lambda، ما يجعله المتاحة للاستخدام في نص دالة lambda. بدلاً من [](uint32_t a) { ... } كما في السابق، يجب كتابة lambda باللغة [instruction](uint32_t a) { ... }. يمكن الآن استخدام instruction في نص lambda.

انطلق وقم بإجراء التغييرات والبناء. يمكنك التحقق من عملك مقابل rv32i_instructions.cc.


إضافة تعليمات تغيير مسار التحكّم

يتم تقسيم التعليمات التي تحتاج إلى تنفيذها لتغيير تدفق التحكم إلى تعليمات فرع شرطية (فروع أقصر يتم إجراؤها إذا أن تكون المقارنة صحيحة)، وإرشادات الانتقال السريع والربط، والتي تُستخدم تنفيذ استدعاءات الدوال (تتم إزالة -and-link عن طريق تعيين الرابط والتسجيل إلى الصفر، مما يجعل هذه تكتب عمليات عدم التنفيذ).

إضافة تعليمات الفرع الشرطي

لا توجد دالة مساعدة لتعليمات الفرع، لذا هناك خياران. يمكنك كتابة الدوال الدلالية من البداية أو كتابة دالة مساعدة محلية. ونظرًا لأننا نحتاج إلى تنفيذ 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; });
}

وفيما يلي تعليمات الفرع المتبقية:

  • باقي الفرع - الفرع يساوي.
  • Bgeu - الفرع أكبر أو يساوي (غير موقع).
  • Blt - الفرع أصغر من (موقع).
  • Bltu - الفرع أقل من (غير موقَّع).
  • Bne - الفرع غير متساوي.

انطلق وأجرِ التغييرات لتنفيذ هذه الدوال الدلالية، لإعادة البناء. يمكنك التحقق من عملك مقابل rv32i_instructions.cc.

لا جدوى من كتابة دالة مساعدة للانتقال والربط. التعليمات، لذلك سنحتاج إلى كتابتها من البداية. لنبدأ النظر في دلالات تعليماتهم.

تأخذ تعليمات jal إزاحة من معامل المصدر 0 وتضيفها إلى جهاز الكمبيوتر الحالي (عنوان التعليمات) لحساب هدف الانتقال. هدف الانتقال تتم كتابتها في معامل الوجهة 0. عنوان الإرجاع هو عنوان بالتعليمات التسلسلية التالية. ويمكن حسابه بإضافة حجم التعليمات إلى عنوانها. تتم كتابة عنوان الإرجاع إلى معامل الوجهة 1. تذكر تضمين مؤشر كائن التعليمات في التقاط صورة لمدا.

تأخذ تعليمات jalr سجلاً أساسيًا كمعامل مصدر 0 وإزاحة. باعتباره معامل المصدر 1، ويجمعهما معًا لحساب هدف الانتقال. بخلاف ذلك، فإنّها تتطابق مع تعليمات jal.

واستنادًا إلى هذه الأوصاف لتعليمات الدلالة، اكتب الدوال والبناء. يمكنك التحقق من عملك مقابل rv32i_instructions.cc.


إضافة تعليمات تخزين الذاكرة

هناك ثلاث تعليمات للمتجر نحتاج إلى تنفيذها: Store بايت (sb)، وتخزين نصف كلمة (sh)، وتخزين كلمة (sw). تعليمات حول المتجر عن الإرشادات التي اتبعناها حتى الآن بشأن عدم الكتابة إلى حالة المعالج المحلي. وبدلاً من ذلك يكتبون إلى مورد النظام - الذاكرة الرئيسية. لا يتعامل MPACT-Sim مع الذاكرة كمعامل تعليمات، لذا، يجب الوصول إلى الذاكرة باستخدام منهجية أخرى.

الإجابة هي إضافة طرق الوصول إلى الذاكرة إلى كائن MPACT-Sim ArchState، أو بشكل أكثر صحيحة، أنشِئ كائن حالة RiscV جديدًا مشتقًا من ArchState. حيث يمكن إضافة ذلك. يدير كائن ArchState الموارد الأساسية، مثل السجلات وكائنات الدولة الأخرى. كما أنه يدير خطوط التأخير المستخدمة التخزين المؤقت لبيانات معامل الوجهة حتى يمكن كتابتها مرة أخرى كائنات السجل. يمكن تنفيذ معظم التعليمات دون معرفة هذه الفئة، لكن بعضها، مثل عمليات الذاكرة وغيرها من أنواع تعليمات تتطلّب وظيفة للإبقاء على عنصر الحالة هذا.

لنلقِ نظرة على الدالة الدلالية لتعليمات fence. التي تم تنفيذها بالفعل في rv32i_instructions.cc كمثال. 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. بعد ذلك، يمكنك استخدام Fence. يتم استدعاء طريقة لفئة RiscVState لإجراء عملية وضع السياج.

ستعمل تعليمات المتجر بالطريقة نفسها. أولاً، العنوان الفعلي يُحسب الوصول إلى الذاكرة من معاملات مصدر التعليمات الأساسية وإزاحة، فمن ثم يتم استرجاع القيمة المراد تخزينها من معامل المصدر التالي. بعد ذلك، يتم الحصول على كائن حالة RiscV من خلال استدعاء طريقة state() static_cast<>، ويتم استدعاء الطريقة المناسبة.

طريقة الكائن RiscVState في StoreMemory بسيطة نسبيًا، ولكنها تشمل ينبغي أن نكون على دراية بالآثار المترتبة عليها:

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

كما نرى، تأخذ الطريقة ثلاث معلمات، يشير المؤشر إلى متجر التعليمات نفسها وعنوان المتجر ومؤشر الماوس إلى DataBuffer مثيل يحتوي على بيانات المتجر. لاحظ أنه لا يوجد حجم مطلوب، يحتوي مثيل DataBuffer نفسه على طريقة size(). ومع ذلك، لا توجد مُعامِل الوجهة الذي يمكن الوصول إليه من خلال التعليمات ويمكن استخدامه تخصيص مثيل DataBuffer بالحجم المناسب. بدلاً من ذلك نحتاج إلى استخدام مصنع DataBuffer تم الحصول عليه من طريقة db_factory() في المثيل Instruction. يستخدم المصنع طريقة 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 كتابتها بهذه الطريقة. سيؤدي هذا الإجراء إلى IncRef المثيل DataBuffer أثناء تشغيله. عليه ثم 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، إلا أنّه يضيف تعقيدًا في كل عملية تحميل التعليمات إلى دالتين دلاليتين منفصلتين. الأول هو على غرار تعليمات المتجر، من حيث إنه يحتسب العنوان الفعال في الوصول إلى الذاكرة. ويتم تنفيذ الثانية عندما يتم حذف الوصول الكامل، وتكتب بيانات الذاكرة في وجهة السجل المُعامِل.

لنبدأ بإلقاء نظرة على تعريف طريقة LoadMemory في اللغة RiscVState:

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

مقارنةً بطريقة StoreMemory، يتطلّب LoadMemory طريقتين إضافيتين. المعلمات: مؤشر إلى مثيل Instruction ومؤشر إلى مرجع عدد العناصر context. الطريقة الأولى هي تعليمات التابعة التي ينفذ كتابة التسجيل (الموضح في البرنامج التعليمي لفك ترميز ISA). أُنشأها جون هنتر، الذي كان متخصصًا يتم الوصول إليه باستخدام الطريقة child() في المثيل Instruction الحالي. وهذه العلامة الأخيرة هي مؤشر على مثيل لفئة مشتقة من ReferenceCount التي تخزِّن في هذه الحالة مثيلاً DataBuffer من هذا النوع. يحتوي على البيانات المحملة. يتوفر كائن السياق من خلال context() في الكائن Instruction (ولكن بالنسبة إلى معظم التعليمات يتم تعيينها على 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.

تتشابه الدوال الدلالية الخاصة بالتعليمات التابعة إلى حد كبير. أولاً، يتم الحصول على LoadContext من خلال استدعاء طريقة Instruction context()، التحويل الثابت إلى LoadContext *. ثانيًا، القيمة (وفقًا للبيانات type) من مثيل البيانات المحمَّلة DataBuffer. ثالثًا، نهج جديد يتم تخصيص مثيل DataBuffer من معامل الوجهة. أخيرًا، تتم كتابة القيمة التي تم تحميلها إلى مثيل DataBuffer الجديد، وتتم إضافة 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 world" بسيطة برنامج في الملف 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] >

وبهذا نصل إلى ختام هذا البرنامج التعليمي. نأمل أن تكون هذه المعلومات مفيدة.