מדריך למפענח הוראה בינארי

מטרות המדריך הזה הן:

  • לומדים את המבנה והתחביר של קובץ התיאור בפורמט הבינארי.
  • הסבר על האופן שבו התיאור של הפורמט הבינארי תואם לתיאור של ISA
  • כותבים את התיאורים הבינאריים לקבוצת המשנה RiscV RV32I של ההוראות.

סקירה כללית

קידוד הוראות בינאריות של RiscV

קידוד הוראה בינארית הוא הדרך הסטנדרטית לקידוד הוראות במיקרו-מעבד. בדרך כלל הם מאוחסנים בקובץ הפעלה, בדרך כלל בפורמט ELF. ההוראות יכולות להיות ברוחב קבוע או משתנה רוחב.

בדרך כלל ההוראות משתמשות בקבוצה קטנה של פורמטים של קידוד, בכל פורמט מותאם אישית לסוג ההוראות המקודדות. לדוגמה, רישום-רישום עשויים להשתמש בפורמט אחד שמגדיל את מספר קודי התפעול הזמינים, ואילו הוראות רישום מיידיות משתמשות באחרת שמחליפה את את ה-opcodes הזמינים להגדלת גודל הקובץ המיידי שאפשר לקודד. הוראות הסתעפות ודילוג כמעט תמיד משתמשות בפורמטים שמגדילים את הגודל המיידית כדי לתמוך בהסתעפויות עם קיזוזים גדולים יותר.

הפורמטים של ההוראות, שבהם נעשה שימוש בהוראות שאנחנו רוצים לפענח ב-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 שלישי קוד פעולה

פורמט I-Type המשמש להוראות מיידיות לרישום, וכן להוראות טעינה ההוראה jalr, 12 ביט מיידית.

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 שלישי קוד פעולה

פורמט 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 שלישי קוד פעולה

פורמט U-Type, משמש להוראות מיידיות למשך זמן ארוך (lui, auipc), 20 ביט מיידי:

31..12 11..7 6..0
20 5 7
uimm20 שלישי קוד פעולה

פורמט 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 קוד פעולה

פורמט J-Type, משמש להוראה בנושא jal, 20 ביט מיידי.

31 30..21 20 19..12 11..7 6..0
1 10 1 8 5 7
IM IM IM IM שלישי קוד פעולה

פורמט 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 קוד פעולה

כפי שניתן לראות בפורמטים האלה, כל ההוראות האלה הן באורך 32 סיביות, 7 הביטים הנמוכים בכל פורמט הוא שדה ה-opcode. כמו כן שימו לב שבזמן יש מספר פורמטים של מודעות בגודל זהה, והביטים שלהם נלקחים חלקים שונים בהוראה. כמו שנראה, המפענח הבינארי של המפרט את הפורמט הזה יכול לבטא זאת.

תיאור של קידוד בינארי

הקידוד הבינארי של ההוראה מבוטא בפורמט הבינארי. (.bin_fmt) קובץ תיאור. הוא מתאר את הקידוד הבינארי ב-ISA, כדי שמפענח הוראה בפורמט בינארי יוכל להיות שנוצר. המפענח שנוצר קובע את ה-opcode, מחלץ את הערך של באופרנד ובשדות מיידיים, כדי לספק את המידע שדרוש ל-ISA למקודד-מפענח שתואר במדריך הקודם.

במדריך הזה נכתוב קובץ תיאור של קידוד בינארי עבור קבוצת משנה מההוראות של RiscV32I הנדרשות כדי לדמות את ההוראות שבהן נעשה שימוש "העולם שלי" הקטן בתוכנית. לפרטים נוספים על ה-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 כולל אותו מאפיין חתום/לא חתום, והוא מתייחס לאותם ביטים של מילים להוראה בתור Inst32Format.opcode.

הפורמט השלישי הוא פורמט מותאם אישית שנמצא בשימוש רק ב-fence הוראה, שהיא הוראה שכבר צוינה ואין לנו במדריך הזה כדי לפתור את הבעיה.

נקודה חשובה: שימוש חוזר בשמות של שדות בפורמטים קשורים שונים, כל עוד שמייצגים את אותם ביטים ויש להם אותו מאפיין חתום/לא חתום.

אחרי הגדרות הפורמט ב-riscv32i.bin_fmt מגיעה קבוצת הוראות להגדרה. כל ההוראות בקבוצת הוראות צריכות להיות זהות באורך הסיביות, ולהשתמש בפורמט שגוזר (אולי בעקיפין) בפורמט של הוראה ברמה העליונה. כשב-ISA יכולות להיות הוראות עם פונקציות אורכים, קבוצת הוראות שונה לכל אורך. בנוסף, אם פענוח ה-ISA של היעד תלוי במצב ביצוע, למשל 'זרוע' לעומת 'אגודל' הוראות, צריך קבוצת הוראות נפרדת לכל מצב. המנתח 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], השם של סוג ספירת ה-opcode לשימוש ב-"OpcodeEnum", והוראה בסיסית הפורמט. סוג ספירת הקוד של הקוד צריך להיות זהה לסוג המופק על ידי בפורמט של מפענח הוראה עצמאית, מכוסה במדריך ב-ISA וממפענח.

כל תיאור של קידוד הוראות מורכב מ-3 חלקים:

  • שם ה-opcode, שחייב להיות זהה לשם שבו נעשה שימוש בהוראה בתיאור של המפענח, כדי ששניהם יפעלו יחד.
  • פורמט ההוראה שבו צריך להשתמש ל-opcode. זה הפורמט ששימשו למתן אזכורים לשדות ביטים בחלק האחרון.
  • רשימה מופרדת בפסיקים של אילוצים בשדות ביט, ==, !=, <, <=, >, ו->= חייבים להיות TRUE כדי שה-opcode יוכל להתאים בהצלחה מילת הוראות.

המנתח של .bin_fmt משתמש בכל המידע הזה כדי לבנות מפענח, ש:

  • מספקת פונקציות חילוץ (חתומות/לא חתומות) בהתאם לכל ביט בכל פורמט. פונקציות החילוץ ממוקמות במרחבי שמות שקיבלה את השם שלו בגרסת תרחיש הנחש של שם הפורמט. לדוגמה, פונקציות חילוץ עבור הפורמט IType ממוקמות במרחב השמות i_type. כל פונקציית חילוץ מוצהרת כ-inline, והיא מקבלת את הערך הצר ביותר uint_t סוג שמחזיר את הרוחב של הפורמט, ומחזיר את הערך הצר ביותר int_t (לסוג חתום), uint_t (לסוג לא חתום) שמכיל את השדה שחולץ רוחב. E.g.:
inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}
  • פונקציית פענוח לכל קבוצת הוראות. היא מחזירה ערך מסוג OpcodeEnum, והיא משתמשת בסוג ה-uint_t הצר ביותר עם הרוחב של את הפורמט של קבוצת ההוראות.

ביצוע build ראשוני

משנים את הספרייה ל-riscv_bin_decoder ויוצרים את הפרויקט באמצעות הפקודה הבאה:

$ cd riscv_bin_decoder
$ bazel build :all

עכשיו החזרנו את הספרייה שלכם לשורש של המאגר, ועכשיו בואו נראה במקורות שנוצרו. בשביל זה, צריך לשנות את הספרייה ל bazel-out/k8-fastbuild/bin/riscv_bin_decoder (בהנחה שאתם משתמשים ב-x86) Host (מארח) - עבור מארחים אחרים, ה-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. החלק הראשון של הקובץ מכיל תקן הגנה על בסיס סטנדרטי (boilerplate), כולל קבצים והצהרות על מרחב שמות. לאחר מכן יש פונקציית עזר בתבנית במרחב השמות 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 ומרחב השמות ואחריו פונקציית המפענח הצהרות:

#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 שלישי קוד פעולה

הדבר הראשון שאנחנו צריכים לעשות הוא להוסיף את הפורמט. פותחים את האפליקציה riscv32i.bin_fmt בעורך המועדף עליך. מיד אחרי שה-Inst32Format מאפשר מוסיפים פורמט בשם RType שנובע מ-Inst32Format. כל שדות הביטים בRType הם unsigned. צריך לציין את השמות, את רוחב הסיביות ואת הסדר שלהן (מימין לשמאל) מהטבלה שלמעלה כדי להגדיר את הפורמט. אם אתם צריכים רמז או אם אתם רוצים לראות את הפתרון המלא, יש ללחוץ כאן.

בשלב הבא צריך להוסיף את ההוראות. ההוראות הן:

  • add – הוספת מספר שלם.
  • and - ברמת הסיביות ו-.
  • or – ברמת הסיביות או,
  • sll – הזזת לוגיקה שמאלה.
  • sltu - הגדרה של 'פחות מ-', ללא חתימה.
  • sub – החסרה של מספרים שלמים.
  • xor – xor ברמת הסיביות.

הקידודים שלהם:

31..25 24..20 19..15 14..12 11..7 6..0 שם opcode
000 0000 rs2 rs1 000 שלישי 011 0011 add
000 0000 rs2 rs1 111 שלישי 011 0011 וגם
000 0000 rs2 rs1 110 שלישי 011 0011 או
000 0000 rs2 rs1 001 שלישי 011 0011 sll
000 0000 rs2 rs1 011 שלישי 011 0011 סלטו
010 0000 rs2 rs1 000 שלישי 011 0011 sub
000 0000 rs2 rs1 100 שלישי 011 0011 Xor
func7 func3 קוד פעולה

מוסיפים את הגדרות ההוראות האלה לפני ההוראות האחרות ב קבוצת הוראות מסוג 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 שלישי קוד פעולה

הפורמט המיידי המיוחד I-Type:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 שלישי קוד פעולה

הפורמט המיידי מסוג U:

31..12 11..7 6..0
20 5 7
uimm20 שלישי קוד פעולה

הפורמט 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 – ערך Xor של כלשהו באופן מיידי.

הקידודים שלהם:

31..20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 שלישי 001 0011 Addi
imm12 rs1 111 שלישי 001 0011 אנדי
imm12 rs1 110 שלישי 001 0011 אורי
imm12 rs1 100 שלישי 001 0011 Xori
func3 קוד פעולה

ההוראות מסוג R (סוג I-Type מיוחד) שעלינו להוסיף הן:

  • slli – שינוי היגיון שמאלה באופן מיידי.
  • srai - שינוי שיטת החשבון הימנית באופן מיידי.
  • srli – שינוי הלוגיקה הימנית מייד.

הקידודים שלהם:

31..25 24..20 19..15 14..12 11..7 6..0 שם opcode
000 0000 uimm5 rs1 001 שלישי 001 0011 slli
010 0000 uimm5 rs1 101 שלישי 001 0011 סראי
000 0000 uimm5 rs1 101 שלישי 001 0011 Srli
func7 func3 קוד פעולה

ההוראות מסוג U שעלינו להוסיף הן:

  • auipc - הוספה של למעלה מיידית למחשב PC.
  • lui – טעינה מיידית למעלה.

הקידודים שלהם:

31..12 11..7 6..0 שם opcode
uimm20 שלישי 001 0111 auipc
uimm20 שלישי 011 0111 Lui
קוד פעולה

ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול riscv32i.bin_fmt.


הקבוצה הבאה של ההוראות שצריך להגדיר היא ההסתעפות המותנית הוראות הדילוג והקישור, והרישום של 'קפיצה וקישור' הוראות להתאמה אישית.

ההסתעפויות המותנות שאנחנו מוסיפים משתמשות בקידוד מסוג 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 קוד פעולה

אמנם הקידוד מסוג B-Type זהה בפריסה לקידוד R-Type, לבחור להשתמש בסוג פורמט חדש כדי שיתאים למסמכי התיעוד של RiscV. אבל אפשר גם פשוט להוסיף שכבת-על כדי לקבל את ההסתעפות המתאימה העברה מיידית, באמצעות השדות func7 ו-rd של סוג R באמצעות הקידוד.

הוספת פורמט 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;

שימו לב ששכבת-העל חתומה, כך שהיא תורחב אוטומטית כאשר הוא נשלף ממילת ההוראה.

ההוראה 'דילוג וקישור (מיידי)' משתמשת בקידוד J-Type:

31..12 11..7 6..0
20 5 7
imm20 שלישי קוד פעולה

זהו גם פורמט שקל להוסיף, אבל שוב, הפורמט המיידי שבו משתמשים ההוראה לא פשוטה כמו שנדמה. רצפי הביטים שמשמשים האנשים המיידיים המלאים הם: 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;

לבסוף, הדילוג והקישור (רישום) משתמש בפורמט I-type כפי שנעשה בו שימוש. בעבר.

הפורמט המיידי I-Type:

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 שלישי קוד פעולה

הפעם לא צריך לבצע שינוי בפורמט.

הוראות ההסתעפות שעלינו להוסיף הן:

  • beq – הסתעפות אם שווה.
  • bge – הסתעפות אם גדול מ- או שווה.
  • bgeu - הסתעפות אם היא גדולה או שווה ללא חתימה.
  • blt – הסתעפות אם פחות מ-.
  • bltu - הסתעפות אם פחות מאשר ללא חתימה.
  • bne – הסתעפות אם לא שווה ל-.

הם מקודדים כך:

31..25 24..20 19..15 14..12 11..7 6..0 שם opcode
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 קוד פעולה

ההוראה jal מקודדת כך:

31..12 11..7 6..0 שם opcode
imm20 שלישי 110 1111 חלול
קוד פעולה

ההוראה jalr מקודדת כך:

31..20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 שלישי 110 0111 Jalr
func3 קוד פעולה

ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול 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 קוד פעולה

במקרה של הפורמט SType, למרבה המזל, המיידית היא ישרה. משרשור קדימה של שני השדות המיידיים, כך שמפרט שכבת-העל היא פשוט:

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

שימו לב שאם משרשרים שדות שלמים, לא צריך להשתמש במזהי טווח ביטים.

הוראות החנות מקודדות כך:

31..25 24..20 19..15 14..12 11..7 6..0 שם opcode
imm7 rs2 rs1 000 imm5 010 0011 sb
imm7 rs2 rs1 001 imm5 010 0011 ד'
imm7 rs2 rs1 010 imm5 010 0011 sw
func3 קוד פעולה

ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול riscv32i.bin_fmt.


הוספת הוראות טעינה

הוראות הטעינה משתמשות בפורמט I-Type. לא צריך לבצע שם שינויים.

הקידודים הם:

31..20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 שלישי 000 0011 lb
imm12 rs1 100 שלישי 000 0011 ליבו
imm12 rs1 001 שלישי 000 0011 לה
imm12 rs1 101 שלישי 000 0011 להו
imm12 rs1 010 שלישי 000 0011 lw
func3 קוד פעולה

ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול riscv32i.bin_fmt.

סיימנו את המדריך הזה, אנחנו מקווים שהוא עזר.