آموزش رمزگشای دستورالعمل باینری

اهداف این آموزش عبارتند از:

  • ساختار و نحو فایل توضیحات فرمت باینری را بیاموزید.
  • بیاموزید که چگونه توضیحات فرمت باینری با توضیحات ISA مطابقت دارد.
  • توضیحات باینری را برای زیر مجموعه دستورالعمل های RiscV RV32I بنویسید.

نمای کلی

رمزگذاری دستورالعمل باینری RiscV

رمزگذاری دستورالعمل باینری روش استاندارد برای رمزگذاری دستورالعمل ها برای اجرا در یک ریزپردازنده است. آنها معمولاً در یک فایل اجرایی، معمولاً در قالب ELF ذخیره می شوند. دستورالعمل ها می توانند عرض ثابت یا عرض متغیر باشند.

به طور معمول، دستورالعمل‌ها از مجموعه کوچکی از قالب‌های رمزگذاری استفاده می‌کنند که هر قالب برای نوع دستورالعمل‌های کدگذاری شده سفارشی شده است. برای مثال، دستورالعمل‌های ثبت-ثبت ممکن است از یک قالب استفاده کنند که تعداد کدهای عملیاتی موجود را به حداکثر می‌رساند، در حالی که دستورالعمل‌های ثبت فوری از فرمتی دیگر استفاده می‌کنند که تعداد اپکدهای موجود را برای افزایش اندازه کدهای فوری که می‌توان رمزگذاری کرد، کاهش می‌دهد. دستورالعمل های انشعاب و پرش تقریباً همیشه از قالب هایی استفاده می کنند که اندازه فوری را به حداکثر می رساند تا از شاخه هایی با افست های بزرگتر پشتیبانی کند.

فرمت های دستورالعمل استفاده شده توسط دستورالعمل هایی که می خواهیم در شبیه ساز RiscV خود رمزگشایی کنیم به شرح زیر است:

فرمت R-Type که برای دستورالعمل های ثبت نام استفاده می شود:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 rs2 rs1 func3 rd opcode

فرمت I-Type، برای دستورالعمل های ثبت-فوری، دستورالعمل های بارگذاری، و دستورالعمل jalr ، 12 بیت فوری استفاده می شود.

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd opcode

فرمت تخصصی I-Type، مورد استفاده برای تغییر با دستورالعمل های فوری، 5 بیت فوری:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd opcode

فرمت U-Type، مورد استفاده برای دستورالعمل های فوری طولانی ( lui ، auipc )، 20 بیت فوری:

31..12 11..7 6..0
20 5 7
uimm20 rd opcode

فرمت B-Type، برای شاخه های شرطی، 12 بیت فوری استفاده می شود.

31 30..25 24..20 19..15 14..12 11..8 7 6..0
1 6 5 5 3 4 1 7
IM IM rs2 rs1 func3 IM IM opcode

فرمت J-Type، مورد استفاده برای دستورالعمل jal ، 20 بیت فوری.

31 30..21 20 19..12 11..7 6..0
1 10 1 8 5 7
IM IM IM IM rd opcode

فرمت S-Type، برای دستورالعمل های فروشگاه استفاده می شود، 12 بیت فوری.

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
IM rs2 rs1 func3 IM opcode

همانطور که از این فرمت ها می بینید، تمام این دستورالعمل ها 32 بیت هستند و 7 بیت پایین در هر فرمت، فیلد opcode است. همچنین توجه داشته باشید که در حالی که چندین فرمت بلافاصله اندازه یکسانی دارند، بیت های آنها از قسمت های مختلف دستورالعمل گرفته شده است. همانطور که خواهیم دید، فرمت مشخصات رمزگشای باینری قادر به بیان این است.

توضیحات کدگذاری باینری

رمزگذاری باینری دستورالعمل در فایل توضیحات فرمت باینری ( .bin_fmt ) بیان می شود. این کدگذاری باینری دستورالعمل ها را در یک ISA توصیف می کند تا یک رمزگشای دستور فرمت باینری تولید شود. رمزگشای تولید شده، کد عملیاتی را تعیین می کند، مقدار عملوند و فیلدهای فوری را استخراج می کند تا اطلاعات مورد نیاز رمزگشای رمزگذاری-آگنوستیک ISA را که در آموزش قبلی توضیح داده شد، ارائه دهد.

در این آموزش ما یک فایل توضیح کدگذاری باینری برای زیرمجموعه ای از دستورالعمل های RiscV32I می نویسیم که برای شبیه سازی دستورالعمل های مورد استفاده در یک برنامه کوچک "Hello World" ضروری است. برای جزئیات بیشتر در مورد RiscV ISA به مشخصات Risc-V {.external} مراجعه کنید.

با باز کردن فایل شروع کنید: riscv_bin_decoder/riscv32i.bin_fmt .

محتوای فایل به چند بخش تقسیم شده است.

اول، تعریف decoder است.

decoder RiscV32I {
  // The namespace in which code will be generated.
  namespace mpact::sim::codelab;
  // The name (including any namespace qualifiers) of the opcode enum type.
  opcode_enum = "OpcodeEnum";
  // Include files specific to this decoder.
  includes {
    #include "riscv_isa_decoder/solution/riscv32i_decoder.h"
  }
  // Instruction groups for which to generate decode functions.
  RiscVInst32;
};

تعریف رمزگشای ما نام رسیور RiscV32I و همچنین چهار اطلاعات اضافی را مشخص می کند. اولین مورد namespace است که فضای نامی را که کد تولید شده در آن قرار می گیرد را مشخص می کند. دوم، opcode_enum ، که نام می‌برد که چگونه نوع شمارش کد opcode که توسط رمزگشا ISA تولید می‌شود باید در کد تولید شده ارجاع داده شود. سوم، includes {} شامل فایل های لازم برای کد تولید شده برای این رمزگشا را مشخص می کند. در مورد ما، این فایلی است که توسط رسیور ISA از آموزش قبلی تولید شده است. فایل های شامل اضافی را می توان در تعریف includes {} با دامنه جهانی مشخص کرد. این در صورتی مفید است که رمزگشاهای متعدد تعریف شده باشند، و همه آنها باید برخی از فایل های مشابه را شامل شوند. چهارم لیستی از نام گروه‌های دستورالعمل است که دستورالعمل‌هایی را می‌سازند که رمزگشا برای آن تولید می‌شود. در مورد ما فقط یک وجود دارد: RiscVInst32 .

در ادامه سه تعریف قالب وجود دارد. اینها فرمت‌های دستورالعمل متفاوتی را برای یک کلمه دستورالعمل 32 بیتی نشان می‌دهند که توسط دستورالعمل‌هایی که قبلاً در فایل تعریف شده‌اند استفاده می‌شوند.

// The generic RiscV 32 bit instruction format.
format Inst32Format[32] {
  fields:
    unsigned bits[25];
    unsigned opcode[7];
};

// RiscV 32 bit instruction format used by a number of instructions
// needing a 12 bit immediate, including CSR instructions.
format IType[32] : Inst32Format {
  fields:
    signed imm12[12];
    unsigned rs1[5];
    unsigned func3[3];
    unsigned rd[5];
    unsigned opcode[7];
};

// RiscV instruction format used by fence instructions.
format Fence[32] : Inst32Format {
  fields:
    unsigned fm[4];
    unsigned pred[4];
    unsigned succ[4];
    unsigned rs1[5];
    unsigned func3[3];
    unsigned rd[5];
    unsigned opcode[7];
};

اولین فرمت دستور العمل 32 بیتی به نام Inst32Format را تعریف می کند که دارای دو فیلد است: bits (عرض 25 بیت) و opcode (عرض 7 بیت). هر فیلد unsigned است، به این معنی که وقتی استخراج و در یک نوع عدد صحیح C++ قرار می‌گیرد، مقدار آن صفر می‌شود. مجموع عرض فیلدهای بیتی باید برابر با عرض قالب باشد. در صورت عدم توافق، ابزار خطا ایجاد می کند. این قالب از هیچ فرمت دیگری مشتق نشده است، بنابراین یک فرمت سطح بالا در نظر گرفته می شود.

دومی فرمت دستورالعمل 32 بیتی به نام IType را تعریف می کند که از Inst32Format مشتق شده است و این دو قالب را به هم مرتبط می کند. فرمت شامل 5 فیلد: imm12 ، rs1 ، func3 ، rd و opcode است. فیلد imm12 signed شده است، به این معنی که وقتی مقدار استخراج شده و در یک نوع عدد صحیح C++ قرار می‌گیرد، مقدار با علامت تمدید می‌شود. توجه داشته باشید که IType.opcode هر دو دارای ویژگی signed/unsigned یکسان هستند و به بیت‌های کلمه دستورالعمل یکسانی اشاره می‌کنند که Inst32Format.opcode .

فرمت سوم یک قالب سفارشی است که فقط توسط دستورالعمل fence استفاده می شود که دستورالعملی است که قبلا مشخص شده است و در این آموزش نیازی به نگرانی نیست.

نکته کلیدی: استفاده مجدد از نام فیلدها در قالب‌های مرتبط مختلف تا زمانی که بیت‌های یکسانی را نشان می‌دهند و ویژگی علامت‌دار/بدون علامت یکسانی دارند.

بعد از تعاریف فرمت در riscv32i.bin_fmt یک تعریف گروه دستورالعمل می آید. تمام دستورالعمل‌ها در یک گروه دستورالعمل باید طول بیت یکسانی داشته باشند و از قالبی استفاده کنند که (شاید به‌طور غیرمستقیم) از همان قالب دستورالعمل سطح بالا مشتق شده باشد. هنگامی که یک ISA می تواند دستورالعمل هایی با طول های مختلف داشته باشد، برای هر طول از یک گروه دستورالعمل متفاوت استفاده می شود. علاوه بر این، اگر رمزگشایی ISA هدف به یک حالت اجرا بستگی دارد، مانند دستورات Arm vs Thumb، یک گروه دستورالعمل جداگانه برای هر حالت مورد نیاز است. تجزیه کننده bin_fmt یک رمزگشای باینری برای هر گروه دستورالعمل تولید می کند.

instruction group RiscV32I[32] "OpcodeEnum" : Inst32Format {
  fence   : Fence  : func3 == 0b000, opcode == 0b000'1111;
  csrs    : IType  : func3 == 0b010, rs1 != 0, opcode == 0b111'0011;
  csrw_nr : IType  : func3 == 0b001, rd == 0,  opcode == 0b111'0011;
  csrs_nw : IType  : func3 == 0b010, rs1 == 0, opcode == 0b111'0011;
};

گروه دستورالعمل یک نام RiscV32I ، یک عرض [32] ، نام نوع شمارش کد برای استفاده از "OpcodeEnum" و یک فرمت دستورالعمل پایه را تعریف می کند. نوع شمارش کد عملیاتی باید همان باشد که توسط رمزگشای دستورالعمل مستقل از فرمت که در آموزش رمزگشای ISA ارائه شده است، تولید شده است.

هر توضیح کدگذاری دستورالعمل شامل 3 بخش است:

  • نام Opcode، که باید همان نامی باشد که در توضیحات رمزگشای دستورالعمل استفاده شده است تا این دو با هم کار کنند.
  • فرمت دستورالعمل مورد استفاده برای کد عملیاتی. این فرمتی است که برای برآوردن ارجاعات به فیلدهای بیتی در قسمت پایانی استفاده می شود.
  • لیستی از محدودیت های فیلد بیتی جدا شده با کاما، == ، != ، < ، <= ، > و >= که همه باید درست باشند تا کد عملیاتی با موفقیت با کلمه دستورالعمل مطابقت داشته باشد .

تجزیه کننده .bin_fmt از تمام این اطلاعات برای ساخت رمزگشا استفاده می کند که:

  • توابع استخراج (امضا/بدون علامت) را متناسب با هر فیلد بیت در هر قالب ارائه می کند. توابع استخراج کننده در فضاهای نامی قرار می گیرند که با نسخه snake-case نام فرمت نامگذاری شده اند. به عنوان مثال، توابع استخراج کننده برای قالب IType در فضای نام i_type قرار می گیرند. هر تابع استخراج کننده inline اعلام می شود، باریک ترین نوع uint_t را می گیرد که عرض قالب را نگه می دارد، و باریک ترین نوع int_t (برای علامت گذاری شده)، uint_t (برای بدون علامت)، که عرض فیلد استخراج شده را نگه می دارد، برمی گرداند. به عنوان مثال:
inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}
  • یک تابع رمزگشایی برای هر گروه دستورالعمل. مقداری از نوع OpcodeEnum را برمی‌گرداند و باریک‌ترین نوع uint_t را می‌گیرد که عرض قالب گروه دستورالعمل را نگه می‌دارد.

ساخت اولیه را انجام دهید

دایرکتوری را به riscv_bin_decoder تغییر دهید و پروژه را با استفاده از دستور زیر بسازید:

$ cd riscv_bin_decoder
$ bazel build :all

اکنون دایرکتوری خود را به ریشه مخزن برگردانید، سپس اجازه دهید نگاهی به منابع تولید شده بیندازیم. برای آن، دایرکتوری را به bazel-out/k8-fastbuild/bin/riscv_bin_decoder تغییر دهید (با فرض اینکه روی هاست x86 هستید - برای میزبان های دیگر، k8-fastbuild یک رشته دیگر خواهد بود).

$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_bin_decoder
  • riscv32i_bin_decoder.h
  • riscv32i_bin_decoder.cc

فایل هدر تولید شده (.h)

riscv32i_bin_decoder.h را باز کنید. بخش اول فایل حاوی محافظ استاندارد دیگ بخار است، شامل فایل ها، اعلان های فضای نام. پس از آن یک تابع کمکی قالب در فضای نام internal وجود دارد. این تابع برای استخراج فیلدهای بیتی از فرمت های خیلی طولانی که در یک عدد صحیح C++ 64 بیتی قرار نمی گیرد استفاده می شود.

#ifndef RISCV32I_BIN_DECODER_H
#define RISCV32I_BIN_DECODER_H

#include <iostream>
#include <cstdint>

#include "third_party/absl/functional/any_invocable.h"


#include "learning/brain/research/mpact/sim/codelab/riscv_isa_decoder/solution/riscv32i_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {


namespace internal {

template <typename T>
static inline T ExtractBits(const uint8_t *data, int data_size,
                            int bit_index, int width) {
  if (width == 0) return 0;

  int byte_pos = bit_index >> 3;
  int end_byte = (bit_index + width - 1) >> 3;
  int start_bit = bit_index & 0x7;

  // If it is only from one byte, extract and return.
  if (byte_pos == end_byte) {
    uint8_t mask = 0xff >> start_bit;
    return (mask & data[byte_pos]) >> (8 - start_bit - width);
  }

  // Extract from the first byte.
  T val = 0;
  val = data[byte_pos++] & 0xff >> start_bit;
  int remainder = width - (8 - start_bit);
  while (remainder >= 8) {
    val = (val << 8) | data[byte_pos++];
    remainder -= 8;
  }

  // Extract any remaining bits.
  if (remainder > 0) {
    val <<= remainder;
    int shift = 8 - remainder;
    uint8_t mask = 0b1111'1111 << shift;
    val |= (data[byte_pos] & mask) >> shift;
  }
  return val;
}

}  // namespace internal

بعد از بخش اولیه مجموعه ای از سه فضای نام وجود دارد، یکی برای هر یک از format موجود در فایل riscv32i.bin_fmt :


namespace fence {

...

}  // namespace fence

namespace i_type {

...

}  // namespace i_type

namespace inst32_format {

...

}  // namespace inst32_format

در هر یک از این فضاهای نام، تابع استخراج بیت فیلد inline برای هر فیلد بیت در آن فرمت تعریف شده است. بعلاوه، قالب پایه توابع استخراج را از قالب‌های فرعی کپی می‌کند که 1) نام فیلدها فقط در یک نام فیلد وجود دارد، یا 2) نام فیلدها به همان نوع فیلد (موقعیت‌های امضا شده/بدون علامت و بیت) در آن اشاره می‌کنند. در هر قالبی که در آن قرار می گیرند. این فیلدهای بیتی را که همان بیت ها را توصیف می کنند را قادر می سازد تا با استفاده از توابع موجود در فضای نام قالب سطح بالا استخراج شوند.

توابع موجود در فضای نام i_type در زیر نشان داده شده است:

namespace i_type {

inline uint8_t ExtractFunc3(uint32_t value) {
  return  (value >> 12) & 0x7;
}

inline int16_t ExtractImm12(uint32_t value) {
  int16_t result = ( (value >> 20) & 0xfff) << 4;
  result = result >> 4;
  return result;
}

inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}

inline uint8_t ExtractRd(uint32_t value) {
  return  (value >> 7) & 0x1f;
}

inline uint8_t ExtractRs1(uint32_t value) {
  return  (value >> 15) & 0x1f;
}

}  // namespace i_type

در نهایت، اعلان تابع تابع رمزگشا برای گروه دستورالعمل RiscVInst32 اعلام شده است. یک 32 بیت بدون علامت به عنوان مقدار کلمه دستورالعمل می گیرد و عضو کلاس شمارش OpcodeEnum را که مطابقت دارد، یا OpcodeEnum::kNone در صورت عدم تطابق برمی گرداند.

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

فایل منبع تولید شده (cc.)

اکنون riscv32i_bin_decoder.cc را باز کنید. قسمت اول فایل شامل اعلان های #include و namespace و به دنبال آن اعلان های تابع رمزگشا می باشد:

#include "riscv32i_bin_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {

OpcodeEnum DecodeRiscVInst32None(uint32_t);
OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word);

DecodeRiscVInst32None برای اقدامات رمزگشایی خالی استفاده می شود، به عنوان مثال، اقداماتی که OpcodeEnum::kNone برمی گرداند. سه عملکرد دیگر رمزگشای تولید شده را تشکیل می دهند. رمزگشای کلی به صورت سلسله مراتبی کار می کند. مجموعه‌ای از بیت‌ها در کلمه دستورالعمل برای تمایز بین دستورالعمل‌ها یا گروه‌هایی از دستورالعمل‌ها در سطح بالا محاسبه می‌شود. لازم نیست بیت ها به هم پیوسته باشند. تعداد بیت ها اندازه جدول جستجو را تعیین می کند که با توابع رمزگشای سطح دوم پر شده است. این در بخش بعدی فایل مشاهده می شود:

absl::AnyInvocable<OpcodeEnum(uint32_t)> parse_group_RiscVInst32_0[kParseGroupRiscVInst32_0_Size] = {
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32None, &DecodeRiscVInst32_0_3,
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,

    ...

    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32_0_3c, &DecodeRiscVInst32None,

    ...
};

در نهایت، توابع رمزگشا تعریف می شوند:

OpcodeEnum DecodeRiscVInst32None(uint32_t) {
  return OpcodeEnum::kNone;
}

OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word) {
  if ((inst_word & 0x4003) != 0x3) return OpcodeEnum::kNone;
  uint32_t index;
  index = (inst_word >> 2) & 0x1f;
  index |= (inst_word >> 7) & 0x60;
  return parse_group_RiscVInst32_0[index](inst_word);
}

OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word) {
  return OpcodeEnum::kFence;
}

OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word) {
  if ((inst_word & 0xf80) != 0x0) return OpcodeEnum::kNone;
  return OpcodeEnum::kCsrwNr;
}

OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word) {
  uint32_t rs1_value = (inst_word >> 15) & 0x1f;
  if (rs1_value != 0x0)
    return OpcodeEnum::kCsrs;
  if (rs1_value == 0x0)
    return OpcodeEnum::kCsrsNw;
  return OpcodeEnum::kNone;
}

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word) {
  OpcodeEnum opcode;
  opcode = DecodeRiscVInst32_0(inst_word);
  return opcode;
}

در این مورد، جایی که تنها 4 دستورالعمل تعریف شده است، تنها یک سطح رمزگشایی و یک جدول جستجوی بسیار پراکنده وجود دارد. با اضافه شدن دستورالعمل ها، ساختار رمزگشا تغییر می کند و ممکن است تعداد سطوح در سلسله مراتب جدول رمزگشا افزایش یابد.


دستورالعمل های ثبت نام ALU را اضافه کنید

اکنون وقت آن است که دستورالعمل های جدیدی را به فایل riscv32i.bin_fmt اضافه کنید. اولین گروه از دستورالعمل‌ها، دستورالعمل‌های ثبت-رجیستر ALU هستند، مانند add ، and و غیره. در RiscV32، همه این دستورالعمل‌ها از فرمت دستورالعمل باینری نوع R استفاده می‌کنند:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 rs2 rs1 func3 rd opcode

اولین کاری که باید انجام دهیم این است که قالب را اضافه کنیم. ادامه دهید و riscv32i.bin_fmt در ویرایشگر مورد علاقه خود باز کنید. درست بعد از Inst32Format اجازه می دهد تا فرمتی به نام RType را اضافه کنیم که از Inst32Format مشتق شده است. تمام فیلدهای بیتی در RType unsigned هستند. از نام ها، عرض بیت و ترتیب (از چپ به راست) از جدول بالا برای تعریف قالب استفاده کنید. اگر به راهنمایی نیاز دارید یا می خواهید راه حل کامل را ببینید، اینجا را کلیک کنید .

بعد باید دستورالعمل ها را اضافه کنیم. دستورالعمل ها عبارتند از:

  • add - اضافه کردن عدد صحیح.
  • and - به صورت بیتی و.
  • or - به صورت بیتی یا.
  • sll - تغییر منطقی به چپ.
  • sltu - تنظیم کمتر از، بدون علامت.
  • sub عدد صحیح
  • xor - bitwise xor.

رمزگذاری آنها عبارتند از:

31..25 24..20 19..15 14..12 11..7 6..0 نام اوپکد
000 0000 rs2 rs1 000 rd 011 0011 اضافه کردن
000 0000 rs2 rs1 111 rd 011 0011 و
000 0000 rs2 rs1 110 rd 011 0011 یا
000 0000 rs2 rs1 001 rd 011 0011 sll
000 0000 rs2 rs1 011 rd 011 0011 sltu
010 0000 rs2 rs1 000 rd 011 0011 فرعی
000 0000 rs2 rs1 100 rd 011 0011 xor
func7 func3 opcode

این تعاریف دستورالعمل را قبل از سایر دستورالعمل ها در گروه دستورالعمل RiscVInst32 اضافه کنید. رشته های باینری با پیشوند اصلی 0b (مشابه 0x برای اعداد هگزادسیمال) مشخص می شوند. برای آسان‌تر کردن خواندن رشته‌های طولانی ارقام باینری، می‌توانید در جایی که مناسب می‌دانید، نقل قول تکی ' به عنوان جداکننده رقم وارد کنید.

هر یک از این تعاریف دستورالعمل دارای سه محدودیت هستند، یعنی در func7 ، func3 ، و opcode . برای همه به جز sub ، محدودیت func7 به صورت زیر خواهد بود:

func7 == 0b000'0000

محدودیت func3 در بیشتر دستورالعمل ها متفاوت است. برای add و sub این است:

func3 == 0b000

محدودیت opcode برای هر یک از این دستورالعمل ها یکسان است:

opcode == 0b011'0011

به یاد داشته باشید که هر خط را با یک نقطه ویرگول خاتمه دهید ; .

راه حل تمام شده اینجاست .

اکنون ادامه دهید و پروژه خود را مانند قبل بسازید و فایل riscv32i_bin_decoder.cc تولید شده را باز کنید. خواهید دید که عملکردهای رمزگشای اضافی برای مدیریت دستورالعمل های جدید ایجاد شده است. در بیشتر موارد آنها شبیه به مواردی هستند که قبلاً تولید شده بودند، اما به DecodeRiscVInst32_0_c نگاه کنید که برای رمزگشایی add / sub استفاده می شود:

OpcodeEnum DecodeRiscVInst32_0_c(uint32_t inst_word) {
  static constexpr OpcodeEnum opcodes[2] = {
    OpcodeEnum::kAdd,
    OpcodeEnum::kSub,
  };
  if ((inst_word & 0xbe000000) != 0x0) return OpcodeEnum::kNone;
  uint32_t index;
  index = (inst_word >> 30) & 0x1;
  return opcodes[index];
}

در این تابع یک جدول رمزگشایی ایستا ایجاد می شود و یک مقدار جستجو از کلمه دستورالعمل برای انتخاب شاخص مناسب استخراج می شود. این یک لایه دوم را در سلسله مراتب رمزگشای دستورالعمل اضافه می کند، اما از آنجایی که کد opcode را می توان مستقیماً در یک جدول بدون مقایسه بیشتر جستجو کرد، در این تابع به جای نیاز به فراخوانی تابع دیگر، در این تابع قرار می گیرد.


دستورالعمل های ALU را با فوری اضافه کنید

مجموعه بعدی دستوراتی که اضافه می کنیم دستورالعمل های ALU هستند که به جای یکی از ثبات ها از یک مقدار فوری استفاده می کنند. سه گروه از این دستورالعمل ها (براساس زمینه فوری) وجود دارد: دستورالعمل های فوری I-Type با امضای فوری 12 بیتی، دستورالعمل های فوری I-Type تخصصی برای شیفت ها، و U-Type فوری با یک 20 بیتی. ارزش فوری بدون امضا فرمت ها در زیر نشان داده شده است:

فرمت I-Type فوری:

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd opcode

فرمت تخصصی I-Type فوری:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd opcode

فرمت فوری U-Type:

31..12 11..7 6..0
20 5 7
uimm20 rd opcode

قالب I-Type از قبل در riscv32i.bin_fmt وجود دارد، بنابراین نیازی به اضافه کردن آن قالب نیست.

اگر فرمت تخصصی I-Type را با فرمت R-Type که در تمرین قبلی تعریف کردیم مقایسه کنیم، می بینیم که تنها تفاوت این است که فیلدهای rs2 به uimm5 تغییر نام داده اند. به جای اضافه کردن یک قالب کاملا جدید، می‌توانیم قالب R-Type را تقویت کنیم. نمی‌توانیم فیلد دیگری اضافه کنیم، زیرا عرض قالب را افزایش می‌دهد، اما می‌توانیم یک همپوشانی اضافه کنیم. همپوشانی یک نام مستعار برای مجموعه‌ای از بیت‌ها در قالب است و می‌تواند برای ترکیب چند دنباله از قالب در یک موجودیت با نام جداگانه استفاده شود. عواقب جانبی این است که کد تولید شده اکنون علاوه بر موارد مربوط به فیلدها، یک تابع استخراج برای همپوشانی نیز خواهد داشت. در این مورد، وقتی هر دو rs2 و uimm5 بدون علامت هستند، تفاوت چندانی ایجاد نمی‌کند، جز اینکه مشخص می‌کند که از فیلد به عنوان یک فوری استفاده می‌شود. برای افزودن یک پوشش به نام uimm5 به فرمت R-Type، موارد زیر را بعد از آخرین فیلد اضافه کنید:

  overlays:
    unsigned uimm5[5] = rs2;

تنها فرمت جدیدی که باید اضافه کنیم فرمت U-Type است. قبل از اینکه قالب را اضافه کنیم، بیایید دو دستورالعملی که از آن قالب استفاده می‌کنند را در نظر بگیریم: auipc و lui . هر دوی اینها مقدار فوری 20 بیتی باقیمانده را با 12 تغییر می دهند قبل از استفاده از آن برای افزودن رایانه به آن ( auipc ) یا نوشتن آن مستقیماً در یک ثبات ( lui ). با استفاده از یک پوشش، می‌توانیم یک نسخه از پیش تغییر یافته از فوری ارائه دهیم، و کمی از محاسبات را از اجرای دستورالعمل به رمزگشایی دستورالعمل تغییر دهیم. ابتدا فرمت را مطابق فیلدهای مشخص شده در جدول بالا اضافه کنید. سپس می توانیم پوشش زیر را اضافه کنیم:

  overlays:
    unsigned uimm32[32] = uimm20, 0b0000'0000'0000;

نحو همپوشانی به ما اجازه می دهد که نه تنها فیلدها، بلکه حروف اللفظی را نیز به هم متصل کنیم. در این حالت ما آن را با 12 صفر به هم متصل می کنیم، در واقع آن را با 12 به سمت چپ تغییر می دهیم.

دستورالعمل های I-Type که باید اضافه کنیم عبارتند از:

  • addi - اضافه کردن فوری.
  • andi - بیتی و با فوری.
  • ori - به صورت بیتی یا با فوری.
  • xori - bitwise xor با فوری.

رمزگذاری آنها عبارتند از:

31..20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 rd 001 0011 افزودن
imm12 rs1 111 rd 001 0011 اندی
imm12 rs1 110 rd 001 0011 یا
imm12 rs1 100 rd 001 0011 xori
func3 opcode

دستورالعمل های R-Type (I-Type تخصصی) که باید اضافه کنیم عبارتند از:

  • slli - تغییر فوری به چپ منطقی.
  • srai - تغییر محاسبات به راست با فوری.
  • srli - تغییر منطقی به راست با فوری.

رمزگذاری آنها عبارتند از:

31..25 24..20 19..15 14..12 11..7 6..0 نام اوپکد
000 0000 uimm5 rs1 001 rd 001 0011 slli
010 0000 uimm5 rs1 101 rd 001 0011 سری
000 0000 uimm5 rs1 101 rd 001 0011 srli
func7 func3 opcode

دستورالعمل های U-Type که باید اضافه کنیم عبارتند از:

  • auipc - upper immediate را به کامپیوتر اضافه کنید.
  • lui - بارگذاری فوقانی فوری.

رمزگذاری آنها عبارتند از:

31..12 11..7 6..0 نام اوپکد
uimm20 rd 001 0111 auipc
uimm20 rd 011 0111 لویی
opcode

ادامه دهید و تغییرات را ایجاد کنید و سپس بسازید. خروجی تولید شده را بررسی کنید. همانطور که قبلاً، می توانید کار خود را با riscv32i.bin_fmt بررسی کنید.


مجموعه بعدی دستوراتی که باید تعریف شوند عبارتند از دستورالعمل های شاخه شرطی، دستور jump-and-link و دستور jump-and-link register.

شاخه های شرطی که اضافه می کنیم همگی از رمزگذاری B-Type استفاده می کنند.

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
imm7 rs2 rs1 func3 imm5 opcode

در حالی که رمزگذاری B-Type از نظر طرح با رمزگذاری R-Type یکسان است، ما انتخاب می کنیم از یک نوع قالب جدید برای تراز با اسناد RiscV استفاده کنیم. اما می‌توانید با استفاده از فیلدهای func7 و rd رمزگذاری R-Type، یک همپوشانی اضافه کنید تا جابجایی شاخه مناسب فوراً بیرون بیاید.

افزودن فرمت BType با فیلدهای مشخص شده در بالا ضروری است، اما کافی نیست. همانطور که می بینید، فوری به دو فیلد دستورالعمل تقسیم می شود. علاوه بر این، دستورالعمل های شاخه این را به عنوان یک الحاق ساده از دو فیلد در نظر نمی گیرند. در عوض، هر فیلد بیشتر پارتیشن بندی می شود و این پارتیشن ها به ترتیب متفاوتی به هم متصل می شوند. در نهایت، آن مقدار یک به چپ جابه‌جا می‌شود تا یک افست تراز 16 بیتی به دست آید.

دنباله بیت ها در کلمه دستورالعمل استفاده شده برای تشکیل فوری عبارتند از: 31، 7، 30..25، 11..8. این مربوط به ارجاعات زیر فیلد زیر است، جایی که ایندکس یا محدوده بیت‌های فیلد را مشخص می‌کند، با شماره‌گذاری از راست به چپ، یعنی imm7[6] به msb imm7 اشاره دارد و imm5[0] به lsb از imm5

imm7[6], imm5[0], imm7[5..0], imm5[4..1]

ساختن این دستکاری بیت بخشی از دستورالعمل های شاخه خود دو اشکال بزرگ دارد. اول، اجرای تابع معنایی را با جزئیات در نمایش دستورالعمل باینری مرتبط می کند. دوم، سربار زمان اجرا بیشتری را اضافه می کند. پاسخ این است که یک همپوشانی به فرمت BType اضافه کنید، از جمله یک '0' انتهایی برای محاسبه تغییر سمت چپ.

  overlays:
    signed b_imm[13] = imm7[6], imm5[0], imm7[5..0], imm5[4..1], 0b0;

توجه داشته باشید که همپوشانی امضا شده است، بنابراین هنگامی که از کلمه دستورالعمل استخراج می شود، به طور خودکار امضا می شود.

دستورالعمل jump-and-link (فوری) از رمزگذاری J-Type استفاده می کند:

31..12 11..7 6..0
20 5 7
imm20 rd opcode

این نیز یک فرمت آسان برای افزودن است، اما باز هم، فرمت فوری که توسط دستورالعمل استفاده می‌شود آنقدر که به نظر می‌رسد ساده نیست. دنباله‌های بیتی که برای تشکیل فوری کامل استفاده می‌شوند عبارتند از: 31، 19..12، 20، 30..21، و بی‌واسطه نهایی با یک به چپ برای تراز نیم کلمه جابه‌جا می‌شود. راه حل این است که یک همپوشانی دیگر (21 بیت برای محاسبه تغییر سمت چپ) به قالب اضافه کنید:

  overlays:
    signed j_imm[21] = imm20[19, 7..0, 8, 18..9], 0b0;

همانطور که می بینید، نحو برای همپوشانی ها از تعیین محدوده های متعدد در یک فیلد به صورت مختصر پشتیبانی می کند. بعلاوه، اگر از نام فیلد استفاده نشود، اعداد بیت به خود کلمه دستورالعمل اشاره می‌کنند، بنابراین موارد بالا می‌توانند به خوبی مانند:

    signed j_imm[21] = [31, 19..12, 20, 30..21], 0b0;

در نهایت، Jump-and-link (رجیستر) از فرمت I-type همانطور که قبلا استفاده می شد استفاده می کند.

فرمت I-Type فوری:

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd opcode

این بار، هیچ تغییری در قالب وجود ندارد.

دستورالعمل های شاخه ای که باید اضافه کنیم عبارتند از:

  • beq - شعبه اگر مساوی باشد.
  • bge - شاخه اگر بزرگتر یا مساوی باشد.
  • bgeu - شاخه اگر بزرگتر یا مساوی باشد بدون علامت.
  • blt - شاخه اگر کمتر از.
  • bltu - شعبه اگر کمتر از بدون امضا باشد.
  • bne - شاخه اگر مساوی نباشد.

آنها به صورت زیر کدگذاری می شوند:

31..25 24..20 19..15 14..12 11..7 6..0 نام اوپکد
imm7 rs2 rs1 000 imm5 110 0011 beq
imm7 rs2 rs1 101 imm5 110 0011 bge
imm7 rs2 rs1 111 imm5 110 0011 bgeu
imm7 rs2 rs1 100 imm5 110 0011 blt
imm7 rs2 rs1 110 imm5 110 0011 bltu
imm7 rs2 rs1 001 imm5 110 0011 bne
func3 opcode

دستور jal به صورت زیر کدگذاری می شود:

31..12 11..7 6..0 نام اوپکد
imm20 rd 110 1111 جال
opcode

دستورالعمل jalr به صورت زیر رمزگذاری شده است:

31..20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 rd 110 0111 jarr
func3 opcode

ادامه دهید و تغییرات را ایجاد کنید و سپس بسازید. خروجی تولید شده را بررسی کنید. همانطور که قبلاً، می توانید کار خود را با riscv32i.bin_fmt بررسی کنید.


دستورالعمل های فروشگاه را اضافه کنید

دستورالعمل‌های فروشگاه از رمزگذاری S-Type استفاده می‌کنند که با کدگذاری B-Type که توسط دستورالعمل‌های شاخه استفاده می‌شود، یکسان است، به جز ترکیب فایل فوری. ما انتخاب می کنیم که قالب SType را اضافه کنیم تا با مستندات RiscV تراز بماند.

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
imm7 rs2 rs1 func3 imm5 opcode

در مورد فرمت SType ، خوشبختانه فوری یک ترکیب مستقیم از دو فیلد فوری است، بنابراین مشخصات همپوشانی به سادگی به شرح زیر است:

  overlays:
    signed s_imm[12] = imm7, imm5;

توجه داشته باشید که هنگام به هم پیوستن فیلدهای کامل، هیچ مشخص کننده محدوده بیتی لازم نیست.

دستورالعمل های فروشگاه به صورت زیر کدگذاری می شود:

31..25 24..20 19..15 14..12 11..7 6..0 نام اوپکد
imm7 rs2 rs1 000 imm5 010 0011 sb
imm7 rs2 rs1 001 imm5 010 0011 ش
imm7 rs2 rs1 010 imm5 010 0011 سوئیچ
func3 opcode

ادامه دهید و تغییرات را ایجاد کنید و سپس بسازید. خروجی تولید شده را بررسی کنید. همانطور که قبلاً، می توانید کار خود را با riscv32i.bin_fmt بررسی کنید.


دستورالعمل های بارگذاری را اضافه کنید

دستورالعمل های بارگذاری از فرمت I-Type استفاده می کنند. هیچ تغییری نباید در آنجا ایجاد شود.

رمزگذاری ها عبارتند از:

31..20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 rd 000 0011 پوند
imm12 rs1 100 rd 000 0011 lbu
imm12 rs1 001 rd 000 0011 lh
imm12 rs1 101 rd 000 0011 lhu
imm12 rs1 010 rd 000 0011 lw
func3 opcode

ادامه دهید و تغییرات را ایجاد کنید و سپس بسازید. خروجی تولید شده را بررسی کنید. همانطور که قبلاً، می توانید کار خود را با riscv32i.bin_fmt بررسی کنید.

این آموزش به پایان می رسد، امیدواریم مفید بوده باشد.