บทแนะนำตัวถอดรหัสคำสั่งไบนารี

วัตถุประสงค์ของบทแนะนำนี้มีดังนี้

  • ดูโครงสร้างและไวยากรณ์ของไฟล์คำอธิบายรูปแบบไบนารี
  • ดูว่าคำอธิบายรูปแบบไบนารีตรงกับคำอธิบาย ISA อย่างไร
  • เขียนคำอธิบายไบนารีสำหรับชุดคำสั่งย่อย RiscV RV32I

ภาพรวม

การเข้ารหัสคำสั่งไบนารี RiscV

การเข้ารหัสคำสั่งแบบไบนารีเป็นวิธีมาตรฐานในการเข้ารหัสคำสั่งสําหรับการดําเนินการบนไมโครโปรเซสเซอร์ โดยปกติแล้ว ไฟล์เหล่านี้จะจัดเก็บไว้ในไฟล์ที่เรียกใช้งานได้ ซึ่งมักจะอยู่ในรูปแบบ ELF วิธีการอาจเป็นได้ทั้งแบบความกว้างคงที่หรือแบบความกว้างผันแปร

โดยปกติแล้ว คำสั่งจะใช้รูปแบบการเข้ารหัสชุดเล็กๆ โดยแต่ละรูปแบบจะปรับแต่งสำหรับประเภทคำสั่งที่เข้ารหัส เช่น คำสั่งรีจิสเตอร์-รีจิสเตอร์อาจใช้รูปแบบหนึ่งที่เพิ่มจำนวนอ็อปโค้ดที่ใช้ได้สูงสุด ขณะที่คำสั่งรีจิสเตอร์-ค่าทันทีใช้รูปแบบอื่นที่ลดจำนวนอ็อปโค้ดที่ใช้ได้เพื่อเพิ่มขนาดของค่าทันทีที่สามารถเข้ารหัสได้ คำสั่ง Branch และ Jump มักจะใช้รูปแบบที่เพิ่มขนาดของค่าทันทีให้มากที่สุดเพื่อรองรับ Branch ที่มีออฟเซตขนาดใหญ่

รูปแบบคำสั่งที่ใช้โดยคำสั่งที่เราต้องการถอดรหัสในโปรแกรมจำลอง 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 รหัสดำเนินการ

รูปแบบ I-Type ใช้สำหรับคำสั่งแบบทันทีของรีจิสเตอร์ คำสั่งโหลด และคำสั่ง jalr แบบทันที 12 บิต

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd รหัสดำเนินการ

รูปแบบ I-Type เฉพาะที่ใช้สำหรับ Shift พร้อมคำสั่งทันที 5 บิต ทันที

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd รหัสดำเนินการ

รูปแบบ U-Type ที่ใช้สำหรับคำสั่งยาวๆ ทันที (lui, auipc), 20 บิตทันที ดังนี้

31..12 11..7 6..0
20 5 7
uimm20 rd รหัสดำเนินการ

รูปแบบ 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
imm imm rs2 rs1 func3 imm imm รหัสดำเนินการ

รูปแบบ J-Type ที่ใช้สำหรับคำสั่ง jal และ 20 บิตทันที

31 30..21 20 19..12 11..7 6..0
1 10 1 8 5 7
imm imm imm imm ปุ่มเลือก รหัสดำเนินการ

รูปแบบ S-Type ใช้สำหรับคำสั่งจัดเก็บ 12 บิตทันที

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
imm rs2 rs1 func3 imm รหัสดำเนินการ

ดังที่คุณเห็นจากรูปแบบเหล่านี้ คำสั่งทั้งหมดมีความยาว 32 บิต และบิตต่ำ 7 บิตในแต่ละรูปแบบคือช่อง opcode นอกจากนี้ โปรดทราบว่าแม้ว่ารูปแบบต่างๆ จะมีค่าทันทีที่มีขนาดเท่ากัน แต่บิตของค่าเหล่านั้นจะมาจากส่วนต่างๆ ของคำสั่ง อย่างที่เห็น รูปแบบการกำหนดตัวถอดรหัสไบนารี สามารถแสดงข้อมูลนี้

คำอธิบายการเข้ารหัสไบนารี

การเข้ารหัสไบนารีของคำสั่งจะแสดงในรูปแบบไบนารีในไฟล์คำอธิบาย (.bin_fmt) ซึ่งอธิบายการเข้ารหัสไบนารีของคำสั่งใน ISA เพื่อให้สร้างโปรแกรมถอดรหัสคำสั่งรูปแบบไบนารีได้ ตัวถอดรหัสที่สร้างขึ้นจะระบุ opcode และแยกค่าตัวถูกดำเนินการและช่องแบบทันที เพื่อให้ข้อมูลที่จำเป็นสำหรับตัวถอดรหัสที่ไม่เข้ารหัสของ 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 รวมถึงข้อมูลอีก 4 รายการ รายการแรกคือ namespace ซึ่งจะกําหนดเนมสเปซที่จะวางโค้ดที่สร้างขึ้น ประการที่ 2 คือ opcode_enum ซึ่งระบุวิธีอ้างอิงประเภทการแจกแจงคำสั่งที่ตัวถอดรหัส ISA สร้างขึ้นภายในโค้ดที่สร้างขึ้น ประการที่สาม includes {} จะระบุไฟล์รวมที่จําเป็นสําหรับโค้ดที่สร้างขึ้นสําหรับโปรแกรมถอดรหัสนี้ ในกรณีของเรา นี่คือไฟล์ที่ตัวถอดรหัส ISA สร้างขึ้นจากบทแนะนำก่อนหน้านี้ คุณสามารถระบุไฟล์อื่นๆ ที่มีได้ในคำจำกัดความ includes {} ที่มีขอบเขตรวมทั้งหมด ซึ่งมีประโยชน์ในกรณีที่มีการกำหนดตัวถอดรหัสหลายรายการ และตัวถอดรหัสทั้งหมดต้องรวมไฟล์เดียวกันบางไฟล์ ที่ 4 คือรายการชื่อกลุ่มคำสั่งที่ประกอบกันเป็นคำสั่งสำหรับการสร้างตัวถอดรหัส ในกรณีของเรา มีเพียงรายการเดียวคือ RiscVInst32

ต่อไปคือคำจำกัดความรูปแบบ 3 รูปแบบ ซึ่งแสดงรูปแบบคำสั่งที่แตกต่างกันสำหรับคำคำสั่ง 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 ซึ่งมี 2 ช่อง ได้แก่ bits (กว้าง 25 บิต) และ opcode (กว้าง 7 บิต) แต่ละช่องเป็น unsigned ซึ่งหมายความว่าระบบจะเพิ่มค่า 0 เมื่อดึงข้อมูลและวางไว้ในประเภทจำนวนเต็ม C++ ผลรวมของความกว้างของช่องข้อมูลไบต์ต้องเท่ากับความกว้างของรูปแบบ เครื่องมือจะแสดงข้อผิดพลาดหากข้อมูลไม่ตรงกัน รูปแบบนี้ไม่ได้มาจากรูปแบบอื่น จึงถือว่าเป็นรูปแบบระดับบนสุด

ส่วนรายการที่ 2 จะกำหนดรูปแบบคำสั่งแบบ 32 บิตที่มีชื่อว่า IType ซึ่งมาจาก Inst32Format ทำให้รูปแบบทั้ง 2 นี้เกี่ยวข้องกัน รูปแบบนี้มี 5 ช่อง ได้แก่ imm12, rs1, func3, rd และ opcode ช่อง imm12 คือ signed ซึ่งหมายความว่าค่าจะขยายออกเมื่อดึงค่าออกมาและวางอยู่ในประเภทจำนวนเต็ม C++ โปรดทราบว่า IType.opcode ทั้ง 2 อย่างมีแอตทริบิวต์ที่มีการรับรอง/ไม่มีลายเซ็นเหมือนกัน และอ้างถึงบิตคำศัพท์ในคำสั่งเหมือนกับ Inst32Format.opcode

รูปแบบที่ 3 คือรูปแบบที่กำหนดเองซึ่งใช้โดยfenceคำสั่งเท่านั้น ซึ่งเป็นคำสั่งที่ระบุไว้แล้วและเราไม่ต้องกังวลในบทแนะนำนี้

ประเด็นสำคัญ: ใช้ชื่อช่องซ้ำในรูปแบบที่เกี่ยวข้องที่แตกต่างกันตราบใดที่ชื่อนั้นแทนบิตเดียวกันและมีแอตทริบิวต์ที่มีลายเซ็น/ไม่มีลายเซ็นเดียวกัน

หลังจากคำจำกัดความของรูปแบบใน riscv32i.bin_fmt จะมีคำจำกัดความกลุ่มคำสั่ง คำสั่งทั้งหมดในกลุ่มคำสั่งต้องมีความยาวบิตเท่ากัน และใช้รูปแบบที่มาจาก (อาจโดยอ้อม) รูปแบบคำสั่งระดับบนสุดเดียวกัน เมื่อ ISA มีคำสั่งที่มีความยาวต่างกัน ระบบจะใช้กลุ่มคำสั่งที่แตกต่างกันสำหรับความยาวแต่ละรายการ นอกจากนี้ หากการถอดรหัส ISA เป้าหมายขึ้นอยู่กับโหมดการดําเนินการ เช่น คำสั่ง Arm กับ 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 ส่วนดังนี้

  • ชื่ออ็อปโค้ด ซึ่งต้องเหมือนกับที่ใช้ในคำอธิบายโปรแกรมถอดรหัสเพื่อให้ทั้ง 2 อย่างทำงานร่วมกันได้
  • รูปแบบคำสั่งที่จะใช้สำหรับอ็อบเจ็กต์โค้ด รูปแบบนี้เป็นรูปแบบที่ใช้เพื่อตอบสนองการอ้างอิงถึงบิตฟิลด์ในส่วนสุดท้าย
  • รายการค่าจำกัดของช่องบิต ==, !=, <, <=, > และ >= ที่คั่นด้วยคอมมา ซึ่งทั้งหมดจะต้องเป็นจริงเพื่อให้ 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 ส่วนแรกของไฟล์มีการป้องกันแบบ 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

ถัดจากส่วนแรกจะมีชุดเนมสเปซ 3 ชุด โดยแต่ละชุดมีไว้สําหรับประกาศ 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 ฟังก์ชันอีก 3 รายการที่เหลือประกอบกันเป็นโปรแกรมถอดรหัสที่สร้างขึ้น ตัวถอดรหัสโดยรวมทำงานแบบลําดับชั้น ระบบจะคำนวณชุดบิตในข้อความระบุคำสั่งเพื่อแยกความแตกต่างระหว่างคำสั่งหรือกลุ่มคำสั่งที่ระดับบนสุด ข้อมูลเล็กๆ น้อยๆ ไม่จำเป็นต้องอยู่ติดกัน จํานวนบิตจะกําหนดขนาดของตารางการค้นหาที่สร้างขึ้นด้วยฟังก์ชันตัวถอดรหัสระดับที่ 2 ซึ่งจะแสดงในส่วนถัดไปของไฟล์

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 รหัสดำเนินการ

สิ่งแรกที่ต้องทำคือการเพิ่มรูปแบบ เปิดไฟล์ riscv32i.bin_fmt ในเครื่องมือแก้ไขที่ต้องการ ถัดจาก Inst32Format ให้ใส่รูปแบบชื่อ RType ซึ่งมาจาก Inst32Format บิตฟิลด์ทั้งหมดใน RType เป็น unsigned ใช้ชื่อ ความกว้างของบิต และลําดับ (จากซ้ายไปขวา) จากตารางด้านบนเพื่อกําหนดรูปแบบ หากต้องการคำแนะนำหรือดูวิธีแก้ปัญหาแบบเต็ม ให้คลิกที่นี่

ต่อไปเราต้องเพิ่มวิธีการ วิธีการมีดังนี้

  • add - การเพิ่มจำนวนเต็ม
  • and - bitwise and
  • or - การดำเนินการ 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,000 rs2 rs1 110 ปุ่มเลือก 011 0011 หรือ
000,000 rs2 rs1 001 rd 011 0011 sll
000,000 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 รหัสดำเนินการ

เพิ่มคําจํากัดความวิธีการเหล่านี้ก่อนวิธีการอื่นๆ ในกลุ่มวิธีการ RiscVInst32 สตริงฐาน 2 จะระบุด้วยคำนำหน้า 0b (คล้ายกับ 0x สำหรับตัวเลขฐาน 16) คุณอาจแทรกเครื่องหมายคำพูดเดี่ยว ' เป็นตัวคั่นหลักในจุดที่เหมาะสมเพื่อให้อ่านสตริงยาวของเลขฐานสองได้ง่ายขึ้น

คําจํากัดความของคำสั่งแต่ละรายการจะมีข้อจำกัด 3 ข้อ ได้แก่ on 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];
}

ฟังก์ชันนี้จะสร้างตารางถอดรหัสแบบคงที่ขึ้นมา และดึงค่าที่ค้นหาออกจากคำคำแนะนำเพื่อเลือกดัชนีที่เหมาะสม ซึ่งจะเพิ่มชั้นที่ 2 ไว้ในลําดับชั้นของตัวถอดรหัสคําสั่ง แต่เนื่องจากสามารถค้นหาอ็อปโค้ดในตารางได้โดยตรงโดยไม่ต้องเปรียบเทียบเพิ่มเติม จึงมีการแทรกไว้ในฟังก์ชันนี้แทนที่จะต้องเรียกใช้ฟังก์ชันอื่น


เพิ่มคำสั่ง ALU ที่มีค่าทันที

ชุดคำสั่งถัดไปที่เราจะเพิ่มคือคำสั่ง ALU ที่ใช้ค่าทันทีแทนรีจิสเตอร์ คำสั่งเหล่านี้มี 3 กลุ่ม (อิงตามช่อง Immediate) ได้แก่ คำสั่ง Immediate ประเภท I ที่มี Immediate แบบลงนาม 12 บิต คำสั่ง Immediate ประเภท I เฉพาะสำหรับการเปลี่ยน และ Immediate ประเภท U ที่มี Immediate แบบไม่ลงนาม 20 บิต รูปแบบต่างๆ มีดังนี้

รูปแบบทันทีประเภท I

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd รหัสดำเนินการ

รูปแบบทันทีประเภท I เฉพาะ

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd รหัสดำเนินการ

รูปแบบทันทีแบบตัว U

31..12 11..7 6..0
20 5 7
uimm20 rd รหัสดำเนินการ

มีรูปแบบ I-Type อยู่แล้วใน riscv32i.bin_fmt จึงไม่จำเป็นต้องเพิ่มรูปแบบดังกล่าว

หากเปรียบเทียบรูปแบบ I-Type เฉพาะกับรูปแบบ R-Type ที่เรากําหนดไว้ในแบบฝึกหัดก่อนหน้า เราจะเห็นว่าความแตกต่างเพียงอย่างเดียวคือช่อง rs2 เปลี่ยนชื่อเป็น uimm5 เราอาจเพิ่มรูปแบบ R-Type แทนการเพิ่มรูปแบบใหม่ทั้งหมด เราเพิ่มช่องอื่นไม่ได้เนื่องจากจะทำให้รูปแบบกว้างขึ้น แต่เราเพิ่มการวางซ้อนได้ การวางซ้อนคือชื่อแทนสําหรับชุดบิตในรูปแบบ และสามารถใช้เพื่อรวมลําดับย่อยหลายรายการของรูปแบบเข้าด้วยกันเป็นเอนทิตีที่มีชื่อแยกต่างหาก ผลข้างเคียงคือตอนนี้โค้ดที่สร้างขึ้นจะมีฟังก์ชันการดึงข้อมูลสำหรับการวางซ้อนด้วย นอกเหนือจากฟังก์ชันสำหรับช่อง ในกรณีนี้ เมื่อไม่ได้ลงชื่อทั้ง rs2 และ uimm5 ก็ไม่ได้มีความแตกต่างมากนัก ยกเว้นการระบุอย่างชัดเจนว่าจะใช้ช่องนี้ทันที หากต้องการเพิ่มการวางซ้อนชื่อ uimm5 ลงในรูปแบบ R ให้เพิ่มข้อมูลต่อไปนี้ต่อจากช่องสุดท้าย

  overlays:
    unsigned uimm5[5] = rs2;

รูปแบบใหม่เพียงรูปแบบเดียวที่เราต้องการเพิ่มคือรูปแบบ U ก่อนที่จะเพิ่มรูปแบบนั้น ลองพิจารณาวิธีการ 2 รายการที่ใช้รูปแบบดังกล่าว ซึ่งได้แก่ auipc และ lui ทั้ง 2 คำสั่งนี้จะเลื่อนค่าทันที 20 บิตไปทางซ้าย 12 ตำแหน่งก่อนนำไปใช้เพื่อเพิ่ม PC เข้าไป (auipc) หรือเขียนค่าดังกล่าวลงในรีจิสเตอร์โดยตรง (lui) การใช้การวางซ้อนช่วยให้เราสามารถระบุค่าทันทีเวอร์ชันที่เลื่อนไว้ล่วงหน้าได้ ซึ่งจะเปลี่ยนการคำนวณเล็กน้อยจากการดำเนินการคำสั่งเป็นการถอดรหัสคำสั่ง ก่อนอื่นให้เพิ่มรูปแบบตามช่องที่ระบุไว้ในตารางด้านบน จากนั้นเราจะเพิ่มการวางซ้อนต่อไปนี้ได้

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

ไวยากรณ์การวางซ้อนช่วยให้เราต่อเชื่อมได้ไม่เพียงช่องข้อมูลเท่านั้น แต่ยังต่อเชื่อมค่าคงที่ได้ด้วย ในกรณีนี้ เราจะต่อสตริงด้วย 0 12 ตัว ซึ่งจะส่งผลให้มีการเลื่อนไปทางซ้าย 12 ตำแหน่ง

คำสั่ง I-Type ที่เราต้องเพิ่ม ได้แก่

  • addi - เพิ่มทันที
  • andi - Bitwise and with immediate
  • ori - Bitwise or พร้อม immediate
  • xori - Bitwise xor with immediate

การเข้ารหัสมีดังนี้

31..20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 rd 001 0011 addi
imm12 rs1 111 ปุ่มเลือก 001 0011 andi
imm12 rs1 110 rd 001 0011 ori
imm12 rs1 100 rd 001 0011 xori
func3 รหัสดำเนินการ

คำสั่งประเภท R (ประเภท I เฉพาะ) ที่เราจำเป็นต้องเพิ่มมีดังนี้

  • 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 srai
000 0000 uimm5 rs1 101 rd 001 0011 Srli
func7 func3 รหัสดำเนินการ

คำสั่ง U-Type ที่เราต้องเพิ่ม ได้แก่

  • auipc - เพิ่ม immediate บนไปยัง PC
  • lui - โหลดส่วนบนทันที

การเข้ารหัสมีดังนี้

31..12 11..7 6..0 ชื่อ opcode
uimm20 rd 001 0111 auipc
uimm20 rd 011 0111 lui
รหัสดำเนินการ

โปรดทําการเปลี่ยนแปลงแล้วสร้าง ตรวจสอบเอาต์พุตที่สร้างขึ้น คุณตรวจสอบงานกับ riscv32i.bin_fmt ได้เช่นเดิม


วิธีการชุดต่อไปที่จำเป็นต้องกำหนด ได้แก่ วิธีการสำหรับ Branch แบบมีเงื่อนไข วิธีการข้ามและลิงก์ และวิธีการลงทะเบียนแบบข้ามและลิงก์

สาขาแบบมีเงื่อนไขที่เราเพิ่มทั้งหมดจะใช้การเข้ารหัส 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 จะมีเลย์เอาต์เหมือนกับการเข้ารหัสประเภท R ทุกประการ แต่เราเลือกใช้ประเภทรูปแบบใหม่เพื่อให้สอดคล้องกับเอกสารประกอบสำหรับ RiscV แต่คุณยังเพิ่มการวางซ้อนเพื่อให้ Branch ที่เหมาะสมทราบได้ทันทีโดยใช้ช่อง func7 และ rd ของการเข้ารหัส R-Type

การเพิ่มรูปแบบ BType ที่มีช่องตามที่ระบุไว้ข้างต้นเป็นสิ่งจําเป็น แต่ยังไม่เพียงพอ ดังที่คุณเห็น คำสั่งแบบทันทีจะแบ่งออกเป็น 2 ช่องคำสั่ง นอกจากนี้ คำสั่งสาขาจะไม่ถือว่าการดำเนินการนี้เป็นการเชื่อมต่อช่อง 2 ช่องเข้าด้วยกัน แต่ระบบจะแบ่งแต่ละช่องออกเป็นหลายส่วน และต่อเชื่อมแต่ละส่วนเหล่านี้ตามลําดับอื่น สุดท้าย ระบบจะเลื่อนค่านั้นไปทางซ้าย 1 ตำแหน่งเพื่อให้ได้ออฟเซตที่สอดคล้องกับ 16 บิต

ลําดับของบิตในคําสั่งที่ใช้สร้างค่าทันทีคือ 31, 7, 30..25, 11..8 ซึ่งสอดคล้องกับการอ้างอิงฟิลด์ย่อยต่อไปนี้ โดยที่ดัชนีหรือช่วงจะระบุบิตในฟิลด์ โดยนับจากขวาไปซ้าย ดังนี้ imm7[6] หมายถึง MSB ของ imm7 และ imm5[0] หมายถึง MSB ของ imm5

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

การทำให้การจัดการบิตนี้เป็นส่วนหนึ่งของคำสั่งสาขามีข้อเสียที่สำคัญ 2 ข้อ ประการแรก เชื่อมโยงการใช้งานฟังก์ชันเชิงความหมายกับรายละเอียดในการนําเสนอคําสั่งแบบไบนารี ประการที่ 2 คือการเพิ่มค่าใช้จ่ายเพิ่มเติมของรันไทม์ คำตอบคือการเพิ่มการวางซ้อนลงในรูปแบบ BType รวมถึง "0" ต่อท้ายเพื่อพิจารณาการเลื่อนไปทางซ้าย

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

โปรดทราบว่าการวางซ้อนมีการเซ็นชื่อ ดังนั้นระบบจะขยายค่าลงท้ายโดยอัตโนมัติเมื่อดึงข้อมูลออกจากคำคำสั่ง

คำสั่ง Jump-and-Link (ทันที) ใช้การเข้ารหัสประเภท J ดังนี้

31..12 11..7 6..0
20 5 7
imm20 rd รหัสดำเนินการ

รูปแบบนี้ยังเพิ่มได้ง่ายอีกด้วย แต่คําสั่ง immediate ที่ใช้นั้นไม่ได้ตรงไปตรงมาอย่างที่คิด ลำดับบิตที่ใช้สร้างค่าทันทีแบบเต็มคือ 31, 19..12, 20, 30..21 และค่าทันทีสุดท้ายจะเลื่อนไปทางซ้าย 1 บิตเพื่อการจัดแนวแบบครึ่งคำ วิธีแก้ไขคือเพิ่มการวางซ้อน (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 (Register) ใช้รูปแบบ I-Type ตามที่ใช้ในก่อนหน้านี้

รูปแบบทันทีประเภท I

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd รหัสดำเนินการ

ครั้งนี้คุณไม่จําเป็นต้องเปลี่ยนแปลงรูปแบบ

คำสั่งสาขาที่เราจำเป็นต้องเพิ่มมีดังนี้

  • beq - Branch if equal
  • bge - Branch if greater than or equal.
  • bgeu - สาขา หากมีมากกว่าหรือเท่ากับ ไม่มีเครื่องหมาย
  • blt - สาขา หากน้อยกว่า
  • bltu - สาขา หากมีน้อยกว่าที่ไม่ได้ลงชื่อ
  • bne - Branch if not equal

โดยมีการโค้ดดังนี้

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 ชื่อรหัสดำเนินการ
imm20 rd 110 1111 jal
รหัสดำเนินการ

คำสั่ง jalr ได้รับการเข้ารหัสดังนี้

31..20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 rd 110 0111 Jalr
func3 รหัสดำเนินการ

ทำการเปลี่ยนแปลง แล้วสร้างได้เลย ตรวจสอบเอาต์พุตที่สร้างขึ้น คุณตรวจสอบงานกับ riscv32i.bin_fmt ได้เช่นเดิม


เพิ่มวิธีการในร้านค้า

คำสั่งจัดเก็บใช้การเข้ารหัสประเภท S ซึ่งเหมือนกับการเข้ารหัสประเภท B ที่คำสั่งสาขาใช้ ยกเว้นองค์ประกอบของค่าทันที เราเลือกที่จะเพิ่มรูปแบบ 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 ข้อมูลทันทีจะต่อท้ายกันโดยตรงจากช่องข้อมูลทันที 2 ช่อง ดังนั้นข้อกําหนดเฉพาะของการวางซ้อนจึงมีดังนี้

  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 sh
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 rd 000 0011 lbu
imm12 rs1 001 ปุ่มเลือก 000 0011 lh
imm12 rs1 101 rd 000 0011 lhu
imm12 rs1 010 rd 000 0011 lw
func3 รหัสดำเนินการ

ทำการเปลี่ยนแปลง แล้วสร้างได้เลย ตรวจสอบเอาต์พุตที่สร้างขึ้น คุณสามารถตรวจสอบงานของคุณเทียบกับ riscv32i.bin_fmt ได้เช่นเดียวกับก่อนหน้านี้

บทแนะนำนี้จบแล้ว เราหวังว่าจะเป็นประโยชน์