اهداف این آموزش عبارتند از:
- با نحوه نمایش دستورالعمل ها در شبیه ساز MPACT-Sim آشنا شوید.
- ساختار و نحو فایل توضیحات ISA را بیاموزید.
- توضیحات ISA را برای زیر مجموعه دستورالعمل های RiscV RV32I بنویسید
نمای کلی
در MPACT-Sim دستورالعملهای هدف رمزگشایی و در یک نمایش داخلی ذخیره میشوند تا اطلاعات آنها بیشتر در دسترس باشد و معنایی سریعتر اجرا شود . این نمونههای دستورالعمل در حافظه پنهان دستورالعمل ذخیره میشوند تا تعداد دفعاتی که دستورالعملهای مکرر اجرا میشوند کاهش یابد.
کلاس آموزشی
قبل از شروع، مفید است که کمی به نحوه نمایش دستورالعمل ها در MPACT-Sim نگاه کنیم. کلاس Instruction
در mpact-sim/mpact/sim/generic/instruction.h تعریف شده است.
نمونه کلاس Instruction حاوی تمام اطلاعات لازم برای شبیه سازی دستورالعمل در هنگام "اجرا" است، مانند:
- آدرس دستورالعمل، اندازه دستورالعمل شبیه سازی شده، به عنوان مثال، اندازه در متن.
- کد عملیاتی
- نشانگر رابط عملوند محمول (در صورت وجود).
- وکتور نشانگرهای رابط عملوند منبع.
- وکتور نشانگرهای رابط عملوند مقصد.
- تابع معنایی قابل فراخوانی
- اشاره گر به شی دولت معماری.
- اشاره گر به شی زمینه.
- اشارهگر به فرزند و نمونههای دستورالعمل بعدی.
- رشته جداسازی قطعات.
این نمونهها عموماً در یک کش دستورالعمل (نمونه) ذخیره میشوند و هر زمان که دستورالعمل مجدداً اجرا شد، مجدداً استفاده میشوند. این باعث بهبود عملکرد در طول زمان اجرا می شود.
به جز اشاره گر به شی زمینه، همه توسط رمزگشای دستورالعملی که از توضیحات ISA تولید می شود پر می شوند. برای این آموزش نیازی به دانستن جزئیات این موارد نیست زیرا ما مستقیماً از آنها استفاده نخواهیم کرد. در عوض، درک سطح بالایی از نحوه استفاده از آنها کافی است.
تابع معنایی قابل فراخوانی شیء تابع/روش/تابع C++ (شامل لامبدا) است که معنای دستور را پیادهسازی میکند. به عنوان مثال، برای یک دستور add
، هر عملوند منبع را بارگیری می کند، دو عملوند را اضافه می کند و نتیجه را در یک عملوند مقصد می نویسد. مبحث توابع معنایی به طور عمیق در آموزش تابع معناشناسی آورده شده است.
عملگرهای دستورالعمل
کلاس دستورالعمل شامل اشاره گر به سه نوع رابط عملوند است: محمول، مبدا و مقصد. این رابطها به توابع معنایی اجازه میدهند که مستقل از نوع واقعی عملوند دستورالعمل زیربنایی نوشته شوند. به عنوان مثال، دسترسی به مقادیر رجیسترها و فوری ها از طریق یک رابط انجام می شود. این بدان معناست که دستورالعملهایی که عملیات یکسانی را انجام میدهند اما بر روی عملوندهای متفاوت (مثلاً ثباتها در مقابل فوریتها) را میتوان با استفاده از یک تابع معنایی یکسان پیادهسازی کرد.
رابط عملوند محمول، برای آن دسته از ISAهایی که از اجرای دستورات محمول پشتیبانی می کنند (برای سایر ISA ها تهی است)، برای تعیین اینکه آیا یک دستور داده شده باید بر اساس مقدار بولی گزاره اجرا شود یا خیر، استفاده می شود.
// The predicte operand interface is intended primarily as the interface to
// read the value of instruction predicates. It is separated from source
// predicates to avoid mixing it in with the source operands needed for modeling
// the instruction semantics.
class PredicateOperandInterface {
public:
virtual bool Value() = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
virtual ~PredicateOperandInterface() = default;
};
رابط عملوند منبع به تابع معنایی دستورالعمل اجازه می دهد تا مقادیر را از عملوندهای دستورالعمل ها بدون توجه به نوع عملوند زیربنایی بخواند. متدهای رابط از عملوندهای اسکالر و بردار با ارزش پشتیبانی می کنند.
// 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;
};
رابط عملوند مقصد روش هایی را برای تخصیص و مدیریت نمونه های DataBuffer
(نوع داده داخلی که برای ذخیره مقادیر ثبت استفاده می شود) ارائه می دهد. یک عملوند مقصد همچنین دارای تأخیر مرتبط با آن است، که تعداد چرخه هایی است که باید منتظر بمانند تا نمونه بافر داده اختصاص داده شده توسط تابع معنایی دستورالعمل برای به روز رسانی مقدار ثبات هدف استفاده شود. به عنوان مثال، تأخیر یک دستورالعمل add
ممکن است 1 باشد، در حالی که برای یک دستورالعمل mpy
ممکن است 4 باشد. این با جزئیات بیشتر در آموزش توابع معنایی پوشش داده شده است.
// 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;
};
توضیحات ISA
ISA (معماری مجموعه دستورالعمل) یک پردازنده مدل انتزاعی را تعریف می کند که توسط آن نرم افزار با سخت افزار تعامل می کند. مجموعه ای از دستورالعمل های موجود، انواع داده ها، رجیسترها و سایر حالت های ماشینی که دستورالعمل ها بر روی آنها کار می کنند، و همچنین رفتار آنها (معناشناسی) را تعریف می کند. برای اهداف MPACT-Sim، ISA شامل رمزگذاری واقعی دستورالعمل ها نمی شود. که به طور جداگانه درمان می شود.
ISA پردازنده در یک فایل توصیفی بیان میشود که مجموعه دستورالعملها را در سطحی انتزاعی و رمزگذاری شده توصیف میکند. فایل توضیحات مجموعه ای از دستورالعمل های موجود را برمی شمارد. برای هر دستورالعمل، فهرست کردن نام، شماره و نام عملوندهای آن، و اتصال آن به یک تابع/ فراخوانی C++ که معنای آن را پیادهسازی میکند، الزامی است. علاوه بر این، می توان یک رشته قالب بندی جداسازی و استفاده دستورالعمل از نام منابع سخت افزاری را مشخص کرد. اولی برای تولید یک نمایش متنی دستورالعمل برای اشکال زدایی، ردیابی یا استفاده تعاملی مفید است. دومی را می توان برای ایجاد دقت چرخه بیشتر در شبیه سازی استفاده کرد.
فایل توضیحات ISA توسط تجزیه کننده isa تجزیه می شود که کدی را برای رمزگشای دستورالعمل بازنمایی-آگنوستیک تولید می کند. این رمزگشا وظیفه پر کردن فیلدهای اشیاء دستورالعمل را بر عهده دارد. مقادیر خاص، مثلاً شماره ثبت مقصد، از رمزگشای دستورالعمل خاص با فرمت به دست می آیند. یکی از این رسیورها، رمزگشای باینری است که تمرکز آموزش بعدی است.
این آموزش نحوه نوشتن فایل توضیحات ISA را برای یک معماری ساده و اسکالر پوشش می دهد. ما از زیرمجموعه ای از مجموعه دستورات RiscV RV32I برای نشان دادن این موضوع استفاده خواهیم کرد و همراه با سایر آموزش ها، شبیه سازی با قابلیت شبیه سازی برنامه "Hello World" را می سازیم. برای جزئیات بیشتر در مورد RiscV ISA به مشخصات Risc-V مراجعه کنید.
با باز کردن فایل شروع کنید: riscv_isa_decoder/riscv32i.isa
محتوای فایل به چند بخش تقسیم می شود. اول اعلامیه ISA است:
isa RiscV32I {
namespace mpact::sim::codelab;
slots { riscv32; }
}
این RiscV32I
را به عنوان نام ISA اعلام میکند و مولد کد کلاسی به نام RiscV32IEncodingBase
ایجاد میکند که رابطی را تعریف میکند که رمزگشا تولید شده برای دریافت اطلاعات اپکد و عملوند استفاده میکند. نام این کلاس با تبدیل نام ISA به Pascal-case و الحاق آن با EncodingBase
ایجاد میشود. slots { riscv32; }
مشخص می کند که تنها یک اسلات دستورالعمل riscv32
در RiscV32I ISA وجود دارد (برخلاف چندین اسلات در یک دستورالعمل VLIW)، و تنها دستورالعمل های معتبر آنهایی هستند که برای اجرا در riscv32
تعریف شده اند.
// First disasm fragment is 15 char wide and left justified.
disasm widths = {-15};
این مشخص می کند که اولین قطعه جداسازی از هر مشخصات جداسازی قطعات (بیشتر را در زیر ببینید)، در یک میدان وسیع 15 کاراکتری توجیه شده باقی می ماند. هر قطعه بعدی بدون فاصله اضافی به این فیلد اضافه می شود.
در زیر این سه اعلان اسلات وجود دارد: riscv32i
، zicsr
و riscv32
. بر اساس تعریف isa
در بالا، فقط دستورالعمل های تعریف شده برای اسلات riscv32
بخشی از isa RiscV32I
خواهند بود. دو اسلات دیگر برای چیست؟
از شکافها میتوان برای دستهبندی دستورالعملها در گروههای جداگانه استفاده کرد، که سپس میتوان آنها را در یک شکاف واحد در پایان ترکیب کرد. به نماد : riscv32i, zicsr
در اعلامیه اسلات riscv32
توجه کنید. این مشخص می کند که اسلات riscv32
تمام دستورالعمل های تعریف شده در اسلات های zicsr
و riscv32i
را به ارث می برد. ISA 32 بیتی RiscV از یک ISA پایه به نام RV32I تشکیل شده است که ممکن است مجموعه ای از پسوندهای اختیاری به آن اضافه شود. مکانیسم شکاف اجازه می دهد تا دستورالعمل های موجود در این پسوندها به طور جداگانه مشخص شوند و سپس در صورت نیاز در پایان برای تعریف ISA کلی ترکیب شوند. در این مورد، دستورالعملهای گروه RiscV 'I' جدا از دستورالعملهای گروه 'zicsr' تعریف میشوند. گروه های اضافی را می توان برای "M" (ضرب/تقسیم)، "F" (نقطه شناور تک دقیق)، "D" (نقطه شناور با دقت دوگانه)، "C" (دستورالعمل های 16 بیتی فشرده) و غیره تعریف کرد. برای RiscV ISA نهایی مورد نیاز است.
// The RiscV 'I' instructions.
slot riscv32i {
...
}
// RiscV32 CSR manipulation instructions.
slot zicsr {
...
}
// The final instruction set combines riscv32i and zicsr.
slot riscv32 : riscv32i, zicsr {
...
}
تعاریف اسلات zicsr
و riscv32
نیازی به تغییر ندارند. با این حال تمرکز روی این آموزش اضافه کردن تعاریف لازم به اسلات riscv32i
است. بیایید نگاهی دقیق تر به آنچه در حال حاضر در این اسلات تعریف شده است بیندازیم:
// The RiscV 'I' instructions.
slot riscv32i {
// Include file that contains the declarations of the semantic functions for
// the 'I' instructions.
includes {
#include "learning/brain/research/mpact/sim/codelab/riscv_semantic_functions/solution/rv32i_instructions.h"
}
// These are all 32 bit instructions, so set default size to 4.
default size = 4;
// Model these with 0 latency to avoid buffering the result. Since RiscV
// instructions have sequential semantics this is fine.
default latency = 0;
// The opcodes.
opcodes {
fence{: imm12 : },
semfunc: "&RV32IFence"c
disasm: "fence";
ebreak{},
semfunc: "&RV32IEbreak",
disasm: "ebreak";
}
}
ابتدا، یک بخش includes {}
وجود دارد که فایلهای سرصفحهای را فهرست میکند که باید در کد تولید شده در هنگام ارجاع مستقیم یا غیرمستقیم به این شکاف در ISA نهایی وارد شوند. شامل فایلها همچنین میتوانند در یک بخش includes {}
با دامنه جهانی فهرست شوند، در این صورت همیشه شامل میشوند. این می تواند مفید باشد اگر همان فایل شامل در غیر این صورت باید به هر تعریف اسلات اضافه شود.
default size
و اعلانهای default latency
تعریف میکنند که، مگر اینکه غیر از این مشخص شده باشد، اندازه یک دستورالعمل 4 است، و تأخیر نوشتن یک عملوند مقصد 0 سیکل است. توجه داشته باشید، اندازه دستورالعمل مشخص شده در اینجا، اندازه افزایش شمارنده برنامه برای محاسبه آدرس دستور متوالی بعدی برای اجرا در پردازنده شبیه سازی شده است. این ممکن است با اندازه نمایش دستورالعمل در فایل اجرایی ورودی در بایت یکسان باشد یا نباشد.
بخش اصلی در تعریف اسلات، بخش Opcode است. همانطور که می بینید، تنها دو کد عملیاتی (دستورالعمل) fence
و ebreak
تا کنون در riscv32i
تعریف شده است. کد عملیاتی fence
با تعیین نام ( fence
) و مشخصات عملوند ( {: imm12 : }
) و سپس فرمت جداسازی اختیاری ( "fence"
) و قابل فراخوانی که به عنوان تابع معنایی محدود می شود، تعریف می شود ( "&RV32IFence"
).
عملوندهای دستورالعمل به صورت سه گانه مشخص می شوند، که هر جزء با یک نقطه ویرگول از هم جدا می شود . لیست های عملوند مبدا و مقصد، لیست هایی از نام عملوند هستند که با کاما از هم جدا شده اند. همانطور که می بینید، عملوندهای دستورالعمل دستور fence
شامل، هیچ عملوند محمولی، فقط یک نام عملوند مبدأ منفرد imm12
و هیچ عملوند مقصد نیست. زیر مجموعه RiscV RV32I از اجرای پیش فرض پشتیبانی نمی کند، بنابراین عملوند محمول در این آموزش همیشه خالی خواهد بود.
تابع معنایی به عنوان رشته لازم برای تعیین تابع ++C یا قابل فراخوانی برای فراخوانی تابع معنایی مشخص می شود. امضای تابع معنایی/قابل تماس void(Instruction *)
.
مشخصات جداسازی از یک لیست رشته ها جدا شده با کاما تشکیل شده است. معمولاً فقط از دو رشته استفاده می شود، یکی برای opcode و دیگری برای عملوندها. هنگامی که قالب بندی می شود (با استفاده از فراخوانی AsString()
در دستورالعمل)، هر رشته در یک فیلد با توجه به مشخصات disasm widths
که در بالا توضیح داده شد قالب بندی می شود.
تمرینهای زیر به شما کمک میکند دستورالعملهایی را به فایل riscv32i.isa
اضافه کنید تا برنامه «Hello World» را شبیهسازی کنید. برای کسانی که عجله دارند، راه حل ها را می توان در riscv32i.isa و rv32i_instructions.h پیدا کرد.
ساخت اولیه را انجام دهید
اگر دایرکتوری را به riscv_isa_decoder
تغییر نداده اید، اکنون این کار را انجام دهید. سپس پروژه را به صورت زیر بسازید - این ساخت باید موفق شود.
$ cd riscv_isa_decoder
$ bazel build :all
اکنون دایرکتوری خود را به ریشه مخزن برگردانید، سپس اجازه دهید نگاهی به منابع تولید شده بیندازیم. برای آن، دایرکتوری را به bazel-out/k8-fastbuild/bin/riscv_isa_decoder
تغییر دهید (با فرض اینکه روی یک هاست x86 هستید - برای میزبان های دیگر، k8-fastbuild یک رشته دیگر خواهد بود).
$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
در این دایرکتوری، در میان فایل های دیگر، فایل های C++ زیر تولید می شوند:
-
riscv32i_decoder.h
-
riscv32i_decoder.cc
-
riscv32i_enums.h
-
riscv32i_enums.cc
بیایید با کلیک کردن بر روی آن در مرورگر به riscv32i_enums.h
نگاه کنیم. باید ببینید که حاوی چیزی شبیه به:
#ifndef RISCV32I_ENUMS_H
#define RISCV32I_ENUMS_H
namespace mpact {
namespace sim {
namespace codelab {
enum class SlotEnum {
kNone = 0,
kRiscv32,
};
enum class PredOpEnum {
kNone = 0,
kPastMaxValue = 1,
};
enum class SourceOpEnum {
kNone = 0,
kCsr = 1,
kImm12 = 2,
kRs1 = 3,
kPastMaxValue = 4,
};
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kRd = 2,
kPastMaxValue = 3,
};
enum class OpcodeEnum {
kNone = 0,
kCsrs = 1,
kCsrsNw = 2,
kCsrwNr = 3,
kEbreak = 4,
kFence = 5,
kPastMaxValue = 6
};
constexpr char kNoneName[] = "none";
constexpr char kCsrsName[] = "Csrs";
constexpr char kCsrsNwName[] = "CsrsNw";
constexpr char kCsrwNrName[] = "CsrwNr";
constexpr char kEbreakName[] = "Ebreak";
constexpr char kFenceName[] = "Fence";
extern const char *kOpcodeNames[static_cast<int>(
OpcodeEnum::kPastMaxValue)];
enum class SimpleResourceEnum {
kNone = 0,
kPastMaxValue = 1
};
enum class ComplexResourceEnum {
kNone = 0,
kPastMaxValue = 1
};
enum class AttributeEnum {
kPastMaxValue = 0
};
} // namespace codelab
} // namespace sim
} // namespace mpact
#endif // RISCV32I_ENUMS_H
همانطور که مشاهده می کنید، هر شکاف، کد عملیاتی و عملوندی که در فایل riscv32i.isa
تعریف شده بود، در یکی از انواع enumeration تعریف شده است. علاوه بر این، یک آرایه OpcodeNames
وجود دارد که همه نامهای کدهای عملیاتی را ذخیره میکند (در riscv32i_enums.cc
تعریف شده است). فایل های دیگر حاوی رمزگشای تولید شده است که در آموزش دیگری بیشتر به آن پرداخته خواهد شد.
قانون ساخت Bazel
هدف رمزگشای ISA در Bazel با استفاده از یک ماکرو قانون سفارشی به نام mpact_isa_decoder
تعریف میشود که از mpact/sim/decoder/mpact_sim_isa.bzl
در مخزن mpact-sim
بارگیری میشود. برای این آموزش، هدف ساخت تعریف شده در riscv_isa_decoder/BUILD
عبارت است از:
mpact_isa_decoder(
name = "riscv32i_isa",
src = "riscv32i.isa",
includes = [],
isa_name = "RiscV32I",
deps = [
"//riscv_semantic_functions:riscv32i",
],
)
این قانون ابزار تجزیه کننده و مولد ISA را برای تولید کد C++ فراخوانی می کند، سپس کد تولید شده را در کتابخانه ای کامپایل می کند که سایر قوانین می توانند با استفاده از برچسب //riscv_isa_decoder:riscv32i_isa
به آن وابسته باشند. بخش includes
برای مشخص کردن فایلهای .isa
اضافی که فایل منبع ممکن است شامل شود استفاده میشود. از isa_name
برای تعیین اینکه کدام isa خاص، در صورتی که بیش از یک مشخص شده باشد، در فایل منبع برای تولید رمزگشا مورد نیاز است، استفاده میشود.
دستورالعمل های ALU ثبت نام را اضافه کنید
اکنون وقت آن است که دستورالعمل های جدیدی را به فایل riscv32i.isa
اضافه کنید. اولین گروه از دستورالعملها، دستورالعملهای ثبت-رجیستر 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 |
در حالی که فایل .isa
برای تولید رمزگشای فرمت آگنوستیک استفاده می شود، هنوز هم مفید است که فرمت باینری و طرح بندی آن را برای راهنمایی ورودی ها در نظر بگیرید. همانطور که می بینید، سه فیلد وجود دارد که مربوط به رمزگشایی است که اشیاء دستورالعمل را پر می کند: rs2
، rs1
و rd
. در این مرحله، ما انتخاب می کنیم که از این نام ها برای رجیسترهای اعداد صحیح استفاده کنیم که به یک شکل (توالی بیت)، در همان فیلدهای دستورالعمل، در تمام دستورالعمل ها کدگذاری می شوند.
دستورالعمل هایی که قرار است اضافه کنیم به شرح زیر است:
-
add
- اضافه کردن عدد صحیح. -
and
- به صورت بیتی و. -
or
- به صورت بیتی یا. -
sll
- تغییر منطقی به چپ. -
sltu
- تنظیم کمتر از، بدون علامت. -
sub
عدد صحیح -
xor
- bitwise xor.
هر یک از این دستورالعمل ها به قسمت opcodes
تعریف اسلات riscv32i
اضافه می شود. به یاد داشته باشید که برای هر دستورالعمل باید نام، کدهای عملیاتی، جداسازی و عملکرد معنایی را مشخص کنیم. نام آسان است، بیایید فقط از نام های opcode بالا استفاده کنیم. همچنین، همه آنها از یک عملوند استفاده می کنند، بنابراین می توانیم از { : rs1, rs2 : rd}
برای مشخصات عملوند استفاده کنیم. این بدان معناست که عملوند منبع ثبت مشخص شده توسط rs1 دارای اندیس 0 در بردار عملوند مبدا در شیء دستورالعمل خواهد بود، عملوند منبع ثبت مشخص شده توسط rs2 دارای اندیس 1 خواهد بود و عملوند مقصد ثبت مشخص شده توسط rd تنها عنصر در بردار عملوند مقصد (در شاخص 0).
بعد مشخصات تابع معنایی است. این کار با استفاده از کلمه کلیدی semfunc
و یک رشته C++ انجام می شود که یک قابل فراخوانی را مشخص می کند که می تواند برای اختصاص دادن به یک std::function
استفاده شود. در این آموزش ما از توابع استفاده خواهیم کرد، بنابراین رشته قابل فراخوانی "&MyFunctionName"
خواهد بود. با استفاده از طرح نامگذاری پیشنهاد شده توسط دستورالعمل fence
، اینها باید "&RV32IAdd"
، "&RV32IAnd"
و غیره باشند.
آخرین مشخصات جداسازی قطعات است. با کلمه کلیدی disasm
شروع می شود و پس از آن یک لیست رشته ها جدا شده با کاما وجود دارد که مشخص می کند دستورالعمل چگونه باید به عنوان یک رشته چاپ شود. استفاده از علامت %
در مقابل نام عملوند نشان دهنده جایگزینی رشته با استفاده از نمایش رشته آن عملوند است. برای دستور add
، این عبارت خواهد بود: disasm: "add", "%rd, %rs1,%rs2"
. این بدان معنی است که ورودی دستورالعمل add
باید به شکل زیر باشد:
add{ : rs1, rs2 : rd},
semfunc: "&RV32IAdd",
disasm: "add", "%rd, %rs1, %rs2";
پیش بروید و فایل riscv32i.isa
را ویرایش کنید و تمام این دستورالعمل ها را به توضیحات .isa
اضافه کنید. اگر به کمک نیاز دارید (یا می خواهید کار خود را بررسی کنید)، فایل توضیحات کامل اینجا است.
هنگامی که دستورالعمل ها به فایل riscv32i.isa
اضافه می شوند، لازم است برای هر یک از توابع معنایی جدید که به فایل rv32i_instructions.h
واقع در "../semantic_functions/ ارجاع داده شده اند، اعلان های تابع اضافه شود. دوباره، اگر به کمک نیاز دارید (یا می خواهید کار خود را بررسی کنید)، پاسخ اینجاست .
پس از انجام همه این کارها، به دایرکتوری riscv_isa_decoder
برگردید و دوباره بسازید. به راحتی فایل های منبع تولید شده را بررسی کنید.
دستورالعمل های ALU را با Immediates اضافه کنید
مجموعه بعدی دستوراتی که اضافه می کنیم دستورالعمل های 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 |
همانطور که می بینید، نام های عملوند rs1
و rd
به همان فیلدهای بیتی قبلی اشاره دارند و برای نشان دادن ثبات های اعداد صحیح استفاده می شوند، بنابراین این نام ها می توانند حفظ شوند. فیلدهای مقدار فوری دارای طول و مکان متفاوت هستند و دو قسمت ( uimm5
و uimm20
) بدون علامت هستند، در حالی که imm12
امضا شده است. هر کدام از اینها از نام خود استفاده می کنند.
بنابراین عملوندهای دستورات I-Type باید { : rs1, imm12 :rd }
باشند. برای دستورالعمل های تخصصی I-Type باید { : rs1, uimm5 : rd}
باشد. مشخصات عملوند دستورالعمل U-Type باید { : uimm20 : rd }
باشد.
دستورالعمل های I-Type که باید اضافه کنیم عبارتند از:
-
addi
- اضافه کردن فوری. -
andi
- بیتی و با فوری. -
ori
- به صورت بیتی یا با فوری. -
xori
- bitwise xor با فوری.
دستورالعمل های تخصصی I-Type که باید اضافه کنیم عبارتند از:
-
slli
- تغییر فوری به چپ منطقی. -
srai
- تغییر محاسبات به راست با فوری. -
srli
- تغییر منطقی به راست با فوری.
دستورالعمل های U-Type که باید اضافه کنیم عبارتند از:
-
auipc
- upper immediate را به کامپیوتر اضافه کنید. -
lui
- بارگذاری فوقانی فوری.
نام هایی که برای کدهای عملیاتی استفاده می شود به طور طبیعی از نام دستورالعمل های بالا پیروی می کنند (نیازی به ایجاد موارد جدید نیست - همه آنها منحصر به فرد هستند). وقتی نوبت به مشخص کردن توابع معنایی میرسد، به یاد بیاورید که اشیاء دستورالعمل رابطهایی را برای عملوندهای مبدأ رمزگذاری میکنند که نسبت به نوع عملوند زیربنایی آگنوستیک هستند. این بدان معنی است که برای دستورالعمل هایی که عملکرد یکسانی دارند، اما ممکن است در انواع عملوند متفاوت باشند، می توانند عملکرد معنایی یکسانی را به اشتراک بگذارند. برای مثال، دستورالعمل addi
اگر نوع عملوند را نادیده بگیرد، همان عملیاتی را انجام میدهد که دستور add
انجام میدهد، بنابراین میتوانند از مشخصات تابع معنایی یکسان "&RV32IAdd"
استفاده کنند. به طور مشابه برای andi
, ori
, xori
و slli
. دستورالعمل های دیگر از توابع معنایی جدید استفاده می کنند، اما آنها باید بر اساس عملیات نامگذاری شوند، نه عملوند، بنابراین برای srai
از "&RV32ISra"
استفاده کنید. دستورالعملهای U-Type auipc
و lui
معادل ثبت ندارند، بنابراین استفاده از "&RV32IAuipc"
و "&RV32ILui"
خوب است.
رشته های جداسازی قطعات بسیار شبیه به تمرین قبلی هستند، اما همانطور که انتظار دارید، ارجاع به %rs2
با %imm12
، %uimm5
یا %uimm20
جایگزین می شود.
ادامه دهید و تغییرات را ایجاد کنید و بسازید. خروجی تولید شده را بررسی کنید. درست مانند گذشته، میتوانید کار خود را با riscv32i.isa و rv32i_instructions.h بررسی کنید.
دستورالعمل های Branch و Jump-And-Link را اضافه کنید
دستورالعمل های شاخه و پرش و پیوند که باید اضافه کنیم، هر دو از یک عملوند مقصد استفاده می کنند که فقط در خود دستورالعمل، یعنی مقدار pc بعدی، اشاره شده است. در این مرحله ما این را به عنوان یک عملوند مناسب با نام next_pc
در نظر می گیریم. در آموزش بعدی بیشتر تعریف خواهد شد.
دستورالعمل شعبه
شاخه هایی که اضافه می کنیم همگی از رمزگذاری B-Type استفاده می کنند.
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 |
فیلدهای فوری مختلف به یک مقدار فوری امضا شده 12 بیتی متصل می شوند. از آنجایی که قالب واقعاً مرتبط نیست، ما این فوری را bimm12
، برای شاخه 12 بیتی فوری می نامیم. تکه تکه شدن در آموزش بعدی ایجاد رمزگشای باینری بررسی خواهد شد. تمام دستورالعمل های شاخه، رجیسترهای اعداد صحیح مشخص شده توسط rs1 و rs2 را با هم مقایسه می کنند، اگر شرط درست باشد، مقدار فوری به مقدار pc فعلی اضافه می شود تا آدرس دستور بعدی که باید اجرا شود را تولید می کند. بنابراین عملوندهای دستورات شاخه باید { : rs1, rs2, bimm12 : next_pc }
باشند.
دستورالعمل های شاخه ای که باید اضافه کنیم عبارتند از:
-
beq
- شعبه اگر مساوی باشد. -
bge
- شاخه اگر بزرگتر یا مساوی باشد. -
bgeu
- شاخه اگر بزرگتر یا مساوی باشد بدون علامت. -
blt
- شاخه اگر کمتر از. -
bltu
- شعبه اگر کمتر از بدون امضا باشد. -
bne
- شاخه اگر مساوی نباشد.
این نامهای اپکد همه منحصربهفرد هستند، بنابراین میتوان از آنها در توضیحات .isa
استفاده مجدد کرد. البته، نام های تابع معنایی جدید باید اضافه شود، به عنوان مثال، "&RV32IBeq"
و غیره.
مشخصات disassembly اکنون کمی پیچیدهتر است، زیرا آدرس دستورالعمل برای محاسبه مقصد استفاده میشود، بدون اینکه در واقع بخشی از عملوندهای دستورالعمل باشد. با این حال، بخشی از اطلاعات ذخیره شده در شیء دستورالعمل است، بنابراین در دسترس است. راه حل این است که از نحو عبارت در رشته disassembly استفاده کنید. به جای استفاده از «%» و به دنبال آن نام عملوند، میتوانید %( expression : print format ) را تایپ کنید. فقط عبارات بسیار ساده پشتیبانی می شوند، اما آدرس به اضافه افست یکی از آنهاست که از نماد @
برای آدرس دستورالعمل فعلی استفاده می شود. فرمت چاپ مشابه فرمتهای printf به سبک C است، اما بدون %
اصلی است. سپس فرمت جداسازی دستور beq
به صورت زیر در میآید:
disasm: "beq", "%rs1, %rs2, %(@+bimm12:08x)"
دستورالعمل های پرش و پیوند
فقط دو دستورالعمل پرش و پیوند باید اضافه شود، jal
(پرش و پیوند) و jalr
(پرش و پیوند غیرمستقیم).
دستورالعمل jal
از رمزگذاری J-Type استفاده می کند:
31 | 30..21 | 20 | 19..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
1 | 10 | 1 | 8 | 5 | 7 |
IM | IM | IM | IM | rd | opcode |
همانطور که برای دستورالعمل های شاخه، 20 بیت فوری در چندین فیلد تکه تکه شده است، بنابراین ما آن را jimm20
می نامیم. در حال حاضر تکه تکه شدن مهم نیست، اما در آموزش بعدی ایجاد رمزگشای باینری به آن پرداخته خواهد شد. سپس مشخصات عملوند به { : jimm20 : next_pc, rd }
تبدیل می شود. توجه داشته باشید که دو عملوند مقصد وجود دارد، مقدار pc بعدی و ثبت لینک مشخص شده در دستورالعمل.
مشابه دستورالعمل های شاخه بالا، قالب جداسازی به صورت زیر می شود:
disasm: "jal", "%rd, %(@+jimm20:08x)"
پرش و پیوند غیرمستقیم از فرمت I-Type با 12 بیت فوری استفاده می کند. برای تولید آدرس دستورالعمل هدف، مقدار فوری گسترش یافته با علامت را به ثبت عدد صحیح مشخص شده توسط rs1
اضافه می کند. ثبت لینک، رجیستر عدد صحیحی است که توسط rd
مشخص شده است.
31..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | opcode |
اگر الگو را دیده باشید، اکنون استنباط می کنید که مشخصات عملوند برای jalr
باید { : rs1, imm12 : next_pc, rd }
و مشخصات disassembly باشد:
disasm: "jalr", "%rd, %rs1, %imm12"
ادامه دهید و تغییرات را ایجاد کنید و سپس بسازید. خروجی تولید شده را بررسی کنید. درست مانند گذشته، میتوانید کار خود را با riscv32i.isa و rv32i_instructions.h بررسی کنید.
دستورالعمل های فروشگاه را اضافه کنید
دستورالعمل فروشگاه بسیار ساده است. همه آنها از فرمت S-Type استفاده می کنند:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
IM | rs2 | rs1 | func3 | IM | opcode |
همانطور که می بینید، این مورد دیگری از یک 12 بیت فوری تکه تکه شده است، بیایید آن را simm12
بنامیم. دستورالعملهای ذخیرهسازی همگی مقدار ثبات عدد صحیح مشخصشده توسط rs2 را به آدرس موثر در حافظه ذخیره میکنند که با افزودن مقدار ثبت عدد صحیح مشخصشده توسط rs1 به مقدار علامت بسط داده شده فوری 12 بیتی به دست میآید. فرمت عملوند باید { : rs1, simm12, rs2 }
برای همه دستورالعملهای فروشگاه باشد.
دستورالعمل های فروشگاهی که باید اجرا شوند عبارتند از:
-
sb
- ذخیره بایت. -
sh
- ذخیره نیم کلمه. -
sw
- ذخیره کلمه.
مشخصات جداسازی قطعات sb
همانطور که انتظار دارید می باشد:
disasm: "sb", "%rs2, %simm12(%rs1)"
مشخصات تابع معنایی نیز همان چیزی است که انتظار دارید: "&RV32ISb"
و غیره.
ادامه دهید و تغییرات را ایجاد کنید و سپس بسازید. خروجی تولید شده را بررسی کنید. درست مانند گذشته، میتوانید کار خود را با riscv32i.isa و rv32i_instructions.h بررسی کنید.
دستورالعمل های بارگذاری را اضافه کنید
دستورالعمل های بارگذاری کمی متفاوت از دستورالعمل های دیگر در شبیه ساز مدل سازی می شوند. برای اینکه بتوانیم مواردی را که تاخیر بار نامشخص است مدل سازی کنیم، دستورالعمل های بار به دو عمل جداگانه تقسیم می شوند: 1) محاسبه آدرس موثر و دسترسی به حافظه، و 2) بازنویسی نتیجه. در شبیه ساز این کار با تقسیم عمل معنایی بار به دو دستورالعمل مجزا انجام می شود، دستورالعمل اصلی و دستورالعمل فرزند . علاوه بر این، وقتی عملوندها را مشخص می کنیم، باید آنها را هم برای دستور اصلی و هم برای دستور فرزند مشخص کنیم. این کار با در نظر گرفتن مشخصات عملوند به عنوان لیستی از سه قلوها انجام می شود. نحو عبارت است از:
{( predicate : sources : destinations ), ( predicate : sources : destinations ), ... }
دستورالعمل های بارگذاری همگی از قالب I-Type مانند بسیاری از دستورالعمل های قبلی استفاده می کنند:
31..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | opcode |
مشخصات عملوند عملوندهای لازم برای محاسبه آدرس و شروع دسترسی حافظه از مقصد ثبت برای داده های بارگذاری را تقسیم می کند: {( : rs1, imm12 : ), ( : : rd) }
.
از آنجایی که کنش معنایی بر دو دستورالعمل تقسیم می شود، توابع معنایی به طور مشابه باید دو فراخوانی را مشخص کنند. برای lw
(بار کلمه)، این نوشته می شود:
semfunc: "&RV32ILw", "&RV32ILwChild"
مشخصات جداسازی قطعات معمولی تر است. هیچ اشاره ای به آموزش کودک نشده است. برای lw
باید باشد:
disasm: "lw", "%rd, %imm12(%rs1)"
دستورالعمل های بارگذاری که باید اجرا شوند عبارتند از:
-
lb
- بارگذاری بایت. -
lbu
- بارگذاری بایت بدون علامت. -
lh
- بارگذاری نیم کلمه. -
lhu
- بارگذاری نیم کلمه بدون امضا. -
lw
- بارگذاری کلمه.
ادامه دهید و تغییرات را ایجاد کنید و سپس بسازید. خروجی تولید شده را بررسی کنید. درست مانند گذشته، میتوانید کار خود را با riscv32i.isa و rv32i_instructions.h بررسی کنید.
ممنون که تا اینجا رفتی امیدواریم این مطلب مفید بوده باشد.