इस ट्यूटोरियल का मकसद ये है:
- जानें कि जनरेट किए गए आईएसए और बाइनरी डिकोडर एक साथ कैसे फ़िट होते हैं.
- RiscV के लिए पूरा निर्देश डिकोडर बनाने के लिए, ज़रूरी C++ कोड लिखें RV32I, जो आईएसए और बाइनरी डिकोडर को जोड़ता है.
निर्देश डिकोडर को समझना
यह निर्देश डिकोडर, निर्देश के पते के आधार पर, इन कामों के लिए ज़िम्मेदार होता है:
निर्देश शब्द को मेमोरी से हटा देता है और
Instruction
, जो उस निर्देश को दिखाता है.
टॉप लेवल डिकोडर, यहां दिखाए गए generic::DecoderInterface
को लागू करता है:
// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
public:
// Return a decoded instruction for the given address. If there are errors
// in the instruciton decoding, the decoder should still produce an
// instruction that can be executed, but its semantic action function should
// set an error condition in the simulation when executed.
virtual Instruction *DecodeInstruction(uint64_t address) = 0;
virtual ~DecoderInterface() = default;
};
जैसा कि आपको दिख रहा है, यहां सिर्फ़ एक तरीका लागू करने की ज़रूरत है: cpp
virtual Instruction *DecodeInstruction(uint64_t address);
अब देखते हैं कि जनरेट किए गए कोड के लिए क्या उपलब्ध कराया गया है और उसकी क्या ज़रूरत है.
सबसे पहले, फ़ाइल में टॉप लेवल क्लास RiscV32IInstructionSet
पर विचार करें
riscv32i_decoder.h
, जो कि
आईएसए डिकोडर. कॉन्टेंट को फिर से देखने के लिए, इसकी सलूशन डायरेक्ट्री पर जाएं
उस ट्यूटोरियल को दोबारा बनाया और सभी को फिर से बनाया.
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
अब अपनी डायरेक्ट्री को वापस डेटा स्टोर करने की जगह के रूट में बदलें. इसके बाद, आइए एक नज़र डालते हैं
जनरेट किए गए सोर्स पर दिखते हैं. इसके लिए, डायरेक्ट्री को इसमें बदलें
bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(यह मानते हुए कि आप x86 पर हैं
होस्ट - अन्य होस्ट के लिए, k8-फ़ास्टबिल्ड कोई दूसरी स्ट्रिंग होगी).
$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
आपको चार सोर्स फ़ाइलें दिखेंगी, जिनमें जनरेट किया गया C++ कोड मौजूद होगा:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
पहली फ़ाइल riscv32i_decoder.h
खोलें. हमारे पास तीन क्लास हैं,
इस पर एक नज़र डालें:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
क्लास के नाम नोट करें. सभी क्लास के नाम
"isa" में दिए गए नाम का पास्कल-केस वर्शन उस फ़ाइल में एलान के लिए:
isa RiscV32I { ... }
अभी तक किसी भी व्यक्ति ने चेक इन नहीं किया है
चलिए, पहले RiscVIInstructionSet
क्लास से शुरुआत करते हैं. यह नीचे दिखाया गया है:
class RiscV32IInstructionSet {
public:
RiscV32IInstructionSet(ArchState *arch_state,
RiscV32IInstructionSetFactory *factory);
Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);
private:
std::unique_ptr<Riscv32Slot> riscv32_decoder_;
ArchState *arch_state_;
};
इस क्लास में वर्चुअल तरीके से कोई तरीका नहीं है. इसलिए, यह अपने-आप में पूरी होने वाली क्लास है, लेकिन
दो चीज़ों पर ध्यान दें. सबसे पहले, कंस्ट्रक्टर,
RiscV32IInstructionSetFactory
क्लास. इस क्लास ने जनरेट की है
डिकोडर, RiscV32Slot
क्लास का इंस्टेंस बनाने के लिए इस्तेमाल करता है. इसका इस्तेमाल इन कामों के लिए किया जाता है
slot RiscV32
के लिए बताए गए सभी निर्देशों को डिकोड करें
riscv32i.isa
फ़ाइल. दूसरा, Decode
तरीका एक अतिरिक्त पैरामीटर लेता है
RiscV32IEncodingBase
के लिए पॉइंटर टाइप करें, यह एक ऐसी क्लास है जो
पहले ट्यूटोरियल और बाइनरी में जनरेट किए गए isa डिकोडर के बीच का इंटरफ़ेस
डिकोडर को दूसरे लैब में जनरेट किया गया.
RiscV32IInstructionSetFactory
क्लास एक ऐब्स्ट्रैक्ट क्लास है, जिससे हम
पूरी डिकोडर के लिए, अपना खुद का तरीका लागू करना होगा. ज़्यादातर मामलों में यह
क्लास आसान है: हर एक के लिए कंस्ट्रक्टर को कॉल करने का तरीका बताएं
हमारी .isa
फ़ाइल में स्लॉट क्लास के बारे में बताया गया है. हमारे मामले में, यह बहुत आसान है
ऐसी केवल एक ही श्रेणी है: Riscv32Slot
(riscv32
नाम का पास्कल-केस
Slot
के साथ जोड़ा गया है). यह तरीका आपके लिए जनरेट नहीं किया जाता, क्योंकि
इस्तेमाल के कुछ ऐडवांस उदाहरण, जहां सब-क्लास बनाने में मदद मिल सकती है
और इसके कंस्ट्रक्टर को कॉल करना न भूलें.
हम इसमें बाद में आखिरी क्लास RiscV32IEncodingBase
से रूबरू होंगे
ट्यूटोरियल शुरू करना चाहते हैं, क्योंकि यह किसी दूसरे एक्सरसाइज़ का विषय है.
टॉप लेवल का निर्देश डिकोडर तय करें
फैक्ट्री क्लास परिभाषित करें
अगर आपने पहले ट्यूटोरियल के लिए प्रोजेक्ट को फिर से बनाया है, तो पक्का करें कि आपने
riscv_full_decoder
डायरेक्ट्री.
riscv32_decoder.h
फ़ाइल खोलें. सभी आवश्यक फ़ाइलों में
को पहले ही जोड़ा जा चुका है और नेमस्पेस सेट अप किए जा चुके हैं.
//Exercise 1 - step 1
के तौर पर मार्क की गई टिप्पणी के बाद, क्लास तय करें
RiscV32IsaFactory
, RiscV32IInstructionSetFactory
से इनहेरिट किया जा रहा है.
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
इसके बाद, CreateRiscv32Slot
के लिए बदलाव की जानकारी दें. चूंकि हम किसी
Riscv32Slot
की व्युत्पन्न क्लास, हम बस इसका उपयोग करके एक नया इंस्टेंस आवंटित करते हैं
std::make_unique
.
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
अगर आपको मदद चाहिए (या अपने काम की जांच करनी है), तो इसका पूरा जवाब यह है यहां पढ़ें.
डिकोडर क्लास को परिभाषित करें
कंस्ट्रक्टर, डिस्ट्रक्टर, और तरीके की जानकारी
इसके बाद, डिकोडर क्लास को परिभाषित किया जाता है. ऊपर जैसी फ़ाइल में, इस लिंक पर जाएं
RiscV32Decoder
का एलान. क्लास की परिभाषा के हिसाब से एलान वाले फ़ॉर्म को बड़ा करें
जहां RiscV32Decoder
को generic::DecoderInterface
से इनहेरिट किया जाता है.
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
अब कंस्ट्रक्टर लिखने से पहले, आइए कोड पर नज़र डालते हैं
इसे बाइनरी डिकोडर पर हमारे दूसरे ट्यूटोरियल में जनरेट किया गया है. इसके अलावा, सभी
Extract
फ़ंक्शन, यहां DecodeRiscVInst32
फ़ंक्शन है:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
यह फ़ंक्शन, निर्देश वाले शब्द को डिकोड करके, उसे दिखाता है
उस निर्देश से मेल खाने वाला ऑपकोड. दूसरी ओर,
DecodeInterface
क्लास जिसे RiscV32Decoder
लागू करता है वह सिर्फ़
इससे पहले ही अपने कारोबार के हिसाब से name@yourcompany.com जैसा कोई ईमेल पता बनाएं. इस तरह, RiscV32Decoder
क्लास के पास मेमोरी को ऐक्सेस करने की अनुमति होनी चाहिए
DecodeRiscVInst32()
को भेजने के लिए निर्देश शब्द पढ़ें. इस प्रोजेक्ट में
इसका इस्तेमाल करके, मेमोरी को ऐक्सेस किया जा सकता है.
.../mpact/sim/util/memory
का सही नाम util::MemoryInterface
है. इसकी जानकारी यहां दी गई है:
// Load data from address into the DataBuffer, then schedule the Instruction
// inst (if not nullptr) to be executed (using the function delay line) with
// context. The size of the data access is based on size of the data buffer.
virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
ReferenceCount *context) = 0;
इसके अलावा हमें state
क्लास इंस्टेंस को
कंस्ट्रक्टर हैं. सही राज्य वर्ग है
riscv::RiscVState
क्लास, जो generic::ArchState
से ली गई. साथ ही, इसमें जोड़ी गई
RiscV की सुविधा. इसका मतलब है कि हमें कंस्ट्रक्टर का एलान करना होगा, ताकि यह
state
और memory
पर पॉइंटर ले जा सकता है:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
डिफ़ॉल्ट कंस्ट्रक्टर को मिटाएं और डिस्ट्रक्टर को ओवरराइड करें:
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
इसके बाद, बताएं कि हमें किस DecodeInstruction
तरीके को बदलना है
generic::DecoderInterface
.
generic::Instruction *DecodeInstruction(uint64_t address) override;
अगर आपको मदद चाहिए (या अपने काम की जांच करनी है), तो इसका पूरा जवाब यह है यहां पढ़ें.
डेटा सदस्य की परिभाषाएं
RiscV32Decoder
क्लास को सेव करने के लिए, निजी डेटा वाले सदस्यों की ज़रूरत होगी
कंस्ट्रक्टर पैरामीटर और फ़ैक्ट्री क्लास का पॉइंटर.
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
इसे कोड में बदलने वाली क्लास के लिए पॉइंटर की भी ज़रूरत होती है, जो यहां से लिया गया है
RiscV32IEncodingBase
, चलिए कॉल करते हैं कि RiscV32IEncoding
(हम लागू करेंगे
में बदलाव नहीं किया है). इसके अलावा, इसे
RiscV32IInstructionSet
, इसलिए यह जोड़ें:
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
आखिर में, हमें अपने मेमोरी इंटरफ़ेस के साथ इस्तेमाल करने के लिए, डेटा सदस्य तय करना होगा:
generic::DataBuffer *inst_db_;
अगर आपको मदद चाहिए (या अपने काम की जांच करनी है), तो इसका पूरा जवाब यह है यहां पढ़ें.
डिकोडर क्लास की विधियां तय करें
इसके बाद, कंस्ट्रक्टर, डिस्ट्रक्टर, और
DecodeInstruction
तरीका. riscv32_decoder.cc
फ़ाइल खोलें. खाली
मेथड पहले से फ़ाइल में हैं, साथ ही नेमस्पेस घोषणाएँ और कुछ
एलानों में से using
एलान शामिल हैं.
कंस्ट्रक्टर की परिभाषा
कंस्ट्रक्टर को सिर्फ़ डेटा मेंबर शुरू करने की ज़रूरत है. सबसे पहले,
state_
और memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
इसके बाद, डिकोडर से जुड़ी हर क्लास के इंस्टेंस को उस क्लास में पास करें जो सही पैरामीटर का इस्तेमाल करें.
// Allocate the isa factory class, the top level isa decoder instance, and
// the encoding parser.
riscv_isa_factory_ = new RiscV32IsaFactory();
riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
riscv_encoding_ = new RiscV32IEncoding(state);
आखिर में, DataBuffer
इंस्टेंस तय करें. इसे फ़ैक्ट्री का इस्तेमाल करके बांटा गया हो
state_
सदस्य के ज़रिए ऐक्सेस किया जा सकता है. हम स्टोर करने के लिए डेटा बफ़र का साइज़ तय करते हैं
सिंगल uint32_t
होगा, क्योंकि यह निर्देश शब्द का आकार होता है.
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
डिस्ट्रक्टर की परिभाषा
डिस्ट्रक्टर आसान है, बस उन ऑब्जेक्ट को फ़्री है जो हमने कंस्ट्रक्टर में असाइन किए हैं,
लेकिन एक ट्विस्ट के साथ. डेटा बफ़र इंस्टेंस को रेफ़रंस के तौर पर गिना जाता है, इसलिए इसके बजाय
उस पॉइंटर पर delete
को कॉल करने पर, हम ऑब्जेक्ट को DecRef()
कर देते हैं:
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
तरीके की परिभाषा
हमारे मामले में, यह तरीका बहुत आसान है. हम मान लेंगे कि पता सही तरीके से अलाइन है और गड़बड़ी की कोई भी अन्य जांच नहीं की गई है आवश्यक.
सबसे पहले, निर्देश शब्द को मेमोरी का इस्तेमाल करके मेमोरी से फ़ेच करना होगा
इंटरफ़ेस और DataBuffer
इंस्टेंस.
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
इसके बाद, हम निर्देश के शब्द को पार्स करने के लिए, RiscVIEncoding
इंस्टेंस में कॉल करते हैं,
यह कार्रवाई, आईएसए डिकोडर को कॉल करने से पहले की जानी चाहिए. याद रखें कि आईएसए
ऑपकोड पाने के लिए डिकोडर सीधे RiscVIEncoding
इंस्टेंस में कॉल करता है
और ऑरेंड को निर्देश वाले शब्द से तय किया जाता है. हमने उसे लागू नहीं किया है
क्लास का इस्तेमाल किया है, लेकिन आइए void ParseInstruction(uint32_t)
को इस तरीके के तौर पर इस्तेमाल करते हैं.
riscv_encoding_->ParseInstruction(iword);
अंत में हम पता और एन्कोडिंग क्लास में पास करते हुए ISA डिकोडर को कॉल करते हैं.
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
अगर आपको मदद चाहिए (या अपने काम की जांच करनी है), तो इसका पूरा जवाब यह है यहां पढ़ें.
एन्कोडिंग क्लास
एन्कोडिंग क्लास एक इंटरफ़ेस लागू करती है, जिसका इस्तेमाल डिकोडर क्लास करता है निर्देश ऑपकोड, उसके सोर्स और डेस्टिनेशन ऑपरेंड को हासिल करना, और संसाधन ऑपरेंड. ये सभी ऑब्जेक्ट, बाइनरी से मिली जानकारी पर निर्भर करते हैं फ़ॉर्मैट डिकोडर, जैसे कि opcode, निर्देश शब्द वगैरह. इसे रखने के लिए डिकोडर क्लास से अलग किया जाता है कोड में बदलने के तरीके के बारे में बताना और कोड में बदलने के कई अलग-अलग तरीकों के लिए सहायता चालू करना आने वाले समय में.
RiscV32IEncodingBase
एक ऐब्स्ट्रैक्ट क्लास है. ऐसे तरीकों का सेट जिन्हें
नीचे दी गई टेबल में लागू किया गया है.
class RiscV32IEncodingBase {
public:
virtual ~RiscV32IEncodingBase() = default;
virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;
virtual ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end) = 0;
virtual ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end) = 0;
virtual PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op) = 0;
virtual SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no) = 0;
virtual DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency) = 0;
virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no) = 0;
};
पहली नज़र में यह कुछ जटिल लगता है, विशेष रूप से इतने सारे कि पैरामीटर में गणना कर सकते हैं, लेकिन RiscV जैसे आसान आर्किटेक्चर के लिए हम असल में ज़्यादातर पैरामीटर, क्योंकि उनकी वैल्यू साफ़ तौर पर बताई जाएंगी.
चलिए, दोनों तरीकों के बारे में जानते हैं.
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
GetOpcode
तरीका वर्तमान के लिए OpcodeEnum
सदस्य लौटाता है
निर्देश की पहचान करना. यह OpcodeEnum
क्लास है
जनरेट की गई isa डिकोडर फ़ाइल riscv32i_enums.h
में बताया गया है. इस तरीके को अपनाने के लिए
दो पैरामीटर हैं, जिनमें से दोनों को हमारे उद्देश्यों के लिए अनदेखा किया जा सकता है. इसमें से पहला
ये स्लॉट टाइप है (एक enum क्लास की जानकारी भी riscv32i_enums.h
में दी गई है),
क्योंकि RiscV में सिर्फ़ एक ही स्लॉट है, इसलिए सिर्फ़ एक ही संभावित वैल्यू हो सकती है:
SlotEnum::kRiscv32
. दूसरा, स्लॉट की इंस्टेंस संख्या है (अगर
स्लॉट के एक से ज़्यादा इंस्टेंस हैं, जो कुछ VLIW में हो सकते हैं
आर्किटेक्चर).
ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end)
ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end);
प्रोसेसर में हार्डवेयर संसाधनों की मॉडलिंग के लिए, अगले दो तरीकों का इस्तेमाल किया जाता है
ताकि साइकल को ज़्यादा सटीक बनाया जा सके. अपने ट्यूटोरियल अभ्यासों के लिए, हम
इसलिए, इन्हें लागू करने के दौरान इनका एक हिस्सा निकाल दिया जाएगा और ये nullptr
के बाद वापस आएंगे.
PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op);
SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no);
DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency);
ये तीन तरीके, कोड में इस्तेमाल होने वाले ऑपरेंड ऑब्जेक्ट के लिए पॉइंटर दिखाते हैं
निर्देश सिमैंटिक फ़ंक्शन, ताकि किसी भी निर्देश की वैल्यू ऐक्सेस की जा सके
ऑपरेंड, हर निर्देश सोर्स ऑपरेंड को पहले से तैयार करना, और नया
वैल्यू को निर्देश डेस्टिनेशन ऑपरेंड के तौर पर डालें. क्योंकि RiscV इस्तेमाल नहीं किया जाता
निर्देश विधेय है, उस विधि को केवल nullptr
वापस लौटाने की आवश्यकता होती है.
इन फ़ंक्शन में पैरामीटर का पैटर्न एक जैसा होता है. सबसे पहले, बस
GetOpcode
स्लॉट और एंट्री पास कर दी गई है. इसके बाद,
वह निर्देश जिसके लिए ऑपरेंड बनाना है. इसका इस्तेमाल सिर्फ़ तब किया जाता है, जब
अलग-अलग ऑपकोड को एक ही ऑपरेंड के लिए अलग-अलग ऑपरेंड ऑब्जेक्ट लौटाने होंगे
प्रकारों के बारे में नहीं है, जो इस RiscV सिम्युलेटर के मामले में नहीं है.
अगला विधेय, स्रोत, और गंतव्य, ऑपरेंड गणना प्रविष्टि है जो
बनाए जाने वाले ऑपरेंड की पहचान करता है. ये तीनों,
जैसा कि नीचे दिखाया गया है, riscv32i_enums.h
में OpEnums:
enum class PredOpEnum {
kNone = 0,
kPastMaxValue = 1,
};
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
अगर आप
riscv32.isa
तो आप ध्यान देंगे कि ये स्रोत और गंतव्य के सेट से संबंधित होते हैं
हर निर्देश के बारे में एलान में इस्तेमाल किए गए ऑपरेंड नाम. अलग-अलग सोर्स का इस्तेमाल करके
ऑपरेंड के ऐसे नाम जो अलग-अलग बिटफ़ील्ड और ऑपरेंड को दिखाते हैं
टाइप के साथ-साथ, यह एन्कोडिंग क्लास को enum सदस्य के रूप में खास तौर पर लिखना आसान बनाता है
लौटाने के लिए सटीक ऑपरेंड टाइप तय करता है. साथ ही, यह ज़रूरी नहीं है कि
स्लॉट, एंट्री या ऑपकोड पैरामीटर की वैल्यू पर विचार करें.
आखिर में, सोर्स और डेस्टिनेशन ऑपरेंड के लिए, ऑपरेंड को इस डेस्टिनेशन के लिए (फिर से, हम इसे अनदेखा कर सकते हैं) पास किया जाता है ऑपरेंड, इंतज़ार का समय (चक्र में) जो निर्देश देने के समय के बीच खत्म हो जाता है जारी की जाती है और गंतव्य परिणाम बाद के निर्देशों के लिए उपलब्ध होता है. हमारे सिम्युलेटर में, यह इंतज़ार का समय 0 होगा. इसका मतलब है कि निर्देश में लिखा गया है रजिस्टर करने के लिए तुरंत रजिस्टर कर दें.
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
फ़ाइनल फ़ंक्शन का इस्तेमाल किसी खास डेस्टिनेशन के इंतज़ार का समय जानने के लिए किया जाता है
ऑपरेंड अगर इसे .isa
फ़ाइल में *
के रूप में तय किया गया है. ऐसा आम तौर पर नहीं होता,
और इसका इस्तेमाल इस RiscV सिम्युलेटर के लिए नहीं किया जाता है, इसलिए इस फ़ंक्शन को लागू करने का हमारा तरीका
सिर्फ़ 0 दिखाएगा.
एन्कोडिंग क्लास तय करें
हेडर फ़ाइल (.h)
तरीके
riscv32i_encoding.h
फ़ाइल खोलें. सभी आवश्यक फ़ाइलों में
को पहले ही जोड़ा जा चुका है और नेमस्पेस सेट अप किए जा चुके हैं. सभी कोड जोड़ने की सुविधा
टिप्पणी // Exercise 2.
को फ़ॉलो किया गया
चलिए, क्लास RiscV32IEncoding
के बारे में बताकर शुरुआत करते हैं जो
जनरेट किया गया इंटरफ़ेस है.
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
इसके बाद, इस मामले में कंस्ट्रक्टर को स्टेट इंस्टेंस पर पॉइंटर लेना चाहिए
riscv::RiscVState
के लिए पॉइंटर. डिफ़ॉल्ट डिस्ट्रक्टर का इस्तेमाल किया जाना चाहिए.
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
इंटरफ़ेस में इस्तेमाल किए गए सभी तरीकों को जोड़ने से पहले, चलिए
निर्देश को पार्स करने के लिए RiscV32Decoder
:
void ParseInstruction(uint32_t inst_word);
अब हम आपको उन तरीकों के बारे में बताते हैं जिन्हें इस्तेमाल करने के दौरान, मामूली बदलाव इस्तेमाल नहीं किए जाने वाले पैरामीटर के नाम:
// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
SimpleResourceVector &,
int) override {
return nullptr;
}
ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
ComplexResourceEnum ,
int, int) override {
return nullptr;
}
PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
PredOpEnum) override {
return nullptr;
}
int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }
अंत में सार्वजनिक इंटरफ़ेस की शेष विधि ओवरराइड जोड़ें लेकिन लागू करने की प्रोसेस .cc फ़ाइल तक टाल दी गई है.
OpcodeEnum GetOpcode(SlotEnum, int) override;
SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
SourceOpEnum source_op, int) override;
DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
DestOpEnum dest_op, int,
int latency) override;
हर ऑपरेंड गैटर तरीकों को लागू करने को आसान बनाने के लिए
हम कॉल किए जा सकने वाले (फ़ंक्शन ऑब्जेक्ट) के दो अरे बनाएंगे, ताकि
SourceOpEnum
और DestOpEnum
सदस्यों की संख्या वाली वैल्यू.
इस प्रकार इन विधियों का मुख्य हिस्सा कम होकर
उस enum मान के लिए फ़ंक्शन ऑब्जेक्ट जो पास किया गया है और अपना वापस लौट रहा है
वैल्यू.
इन दो सरणियों के इनिशलाइज़ेशन को व्यवस्थित करने के लिए, हम दो प्राइवेट रेंज को तय करते हैं इन तरीकों को कंस्ट्रक्टर से इस तरह कॉल किया जाएगा:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
डेटा सदस्य
सदस्यों के लिए ज़रूरी डेटा इस तरह है:
riscv::RiscVState *
वैल्यू होल्ड करने के लिए,state_
.uint32_t
टाइप काinst_word_
, जिसमें मौजूदा वैल्यू ही रखी गई है निर्देश शब्द का इस्तेमाल करें.- मौजूदा निर्देश का ऑपकोड होल्ड करने के लिए
opcode_
ParseInstruction
तरीका. इसका टाइपOpcodeEnum
है. - सोर्स पाने के लिए इस्तेमाल किए गए callables को स्टोर करने के लिए,
source_op_getters_
कलेक्शन ऑपरेंड ऑब्जेक्ट. अरे एलिमेंट का टाइप यह हैabsl::AnyInvocable<SourceOperandInterface *>()>
. dest_op_getters_
callables को स्टोर करने के लिए इस्तेमाल होने वाला अरे डेस्टिनेशन ऑपरेंड ऑब्जेक्ट. अरे एलिमेंट का टाइप यह हैabsl::AnyInvocable<DestinationOperandInterface *>()>
.xreg_alias
RiscV पूर्णांक रजिस्टर एबीआई के नामों की कैटगरी, जैसे कि "शून्य" और "ra" "x0" के बजाय और "x1" शामिल हैं.
riscv::RiscVState *state_;
uint32_t inst_word_;
OpcodeEnum opcode_;
absl::AnyInvocable<SourceOperandInterface *()>
source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
absl::AnyInvocable<DestinationOperandInterface *(int)>
dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];
const std::string xreg_alias_[32] = {
"zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0",
"a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5",
"s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};
अगर आपको मदद चाहिए (या अपने काम की जांच करनी है), तो इसका पूरा जवाब यह है यहां पढ़ें.
सोर्स फ़ाइल (.cc).
riscv32i_encoding.cc
फ़ाइल खोलें. सभी आवश्यक फ़ाइलों में
को पहले ही जोड़ा जा चुका है और नेमस्पेस सेट अप किए जा चुके हैं. सभी कोड जोड़ने की सुविधा
टिप्पणी // Exercise 2.
को फ़ॉलो किया गया
हेल्पर फ़ंक्शन
हम सबसे पहले कुछ हेल्पर फ़ंक्शन लिखेंगे. इनका इस्तेमाल इन कामों के लिए किया जाएगा
सोर्स और डेस्टिनेशन रजिस्टर ऑपरेंड के बारे में जानकारी. इन्हें टेंप्लेट में
रजिस्टर करें और RiscVState
ऑब्जेक्ट में कॉल करेगा, ताकि
ऑब्जेक्ट को रजिस्टर करें और फिर रजिस्टर ऑब्जेक्ट में ऑपरेंड फ़ैक्ट्री तरीके को कॉल करें.
चलिए, डेस्टिनेशन ऑपरेंड हेल्पर से शुरू करते हैं:
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency);
}
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency, op_name);
}
यहां दो हेल्पर फ़ंक्शन दिखाए गए हैं. दूसरा, एक अतिरिक्त
पैरामीटर op_name
, जिसकी मदद से ऑपरेंड को एक अलग नाम या स्ट्रिंग का इस्तेमाल किया जा सकता है
का इस्तेमाल नहीं किया जा सकता.
इसी तरह, सोर्स ऑपरेंड हेल्पर के लिए:
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand();
return op;
}
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand(op_name);
return op;
}
कंस्ट्रक्टर और इंटरफ़ेस फ़ंक्शन
कंस्ट्रक्टर और इंटरफ़ेस के फ़ंक्शन बहुत आसान हैं. कंस्ट्रक्टर इसकी मदद से, सिर्फ़ callables अरे को शुरू करने के लिए, दो शुरुआती तरीकों को कॉल किया जाता है ऑपरेंड गैटर.
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
, निर्देश के लिए इस्तेमाल होने वाले शब्द को सेव करता है. इसके बाद, वह इस ऑपकोड को स्टोर करता है
बाइनरी डिकोडर से जनरेट किए गए कोड में कॉल करने से मिलता है.
// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
inst_word_ = inst_word;
opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}
आखिर में, ऑपरेंड गैटर उस गैटर फ़ंक्शन से वैल्यू देते हैं जिसे वह कॉल करता है गंतव्य/स्रोत संकार्य एनम मान का उपयोग करके अरे लुकअप के आधार पर.
DestinationOperandInterface *RiscV32IEncoding::GetDestination(
SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
return dest_op_getters_[static_cast<int>(dest_op)](latency);
}
SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
SourceOpEnum source_op, int) {
return source_op_getters_[static_cast<int>(source_op)]();
}
कलेक्शन को शुरू करने के तरीके
जैसा कि आपने अंदाज़ा लगाया होगा कि ज़्यादातर काम गेटर की शुरुआत करने में होता है
तो चिंता न करें. चिंता न करें. इसके लिए आसान और दोहराए जाने वाले पैटर्न का इस्तेमाल किया जाता है. आइए
पहले InitializeDestinationOpGetters()
से शुरू करें, क्योंकि
कुछ गंतव्य ऑपरेंड.
riscv32i_enums.h
से जनरेट हुई DestOpEnum
क्लास को वापस लाएं:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
dest_op_getters_
के लिए हमें चार एंट्री शुरू करनी होंगी, हर kNone
के लिए एक,
kCsr
, kNextPc
, और kRd
. सुविधा के लिए, प्रत्येक प्रविष्टि को
Lambda फ़ंक्शन के साथ, कॉल करने लायक किसी दूसरी सुविधा का भी इस्तेमाल किया जा सकता है. हस्ताक्षर
Lambda फ़ंक्शन, void(int latency)
है.
अब तक हमने अलग-अलग मंज़िलों के बारे में ज़्यादा बात नहीं की है
MPACT-Sim में बताए गए ऑपरेंड. इस अभ्यास के लिए हम केवल दो
प्रकार: generic::RegisterDestinationOperand
में परिभाषित
register.h
,
और generic::DevNullOperand
में परिभाषित किया गया है
devnull_operand.h
.
इन ऑपरेंड के विवरण अभी बहुत महत्वपूर्ण नहीं हैं, बस
पुराना का इस्तेमाल रजिस्टर करने के लिए लिखने के लिए किया जाता है और दूसरा वाला सभी राइट को अनदेखा करता है.
kNone
के लिए पहली एंट्री छोटी है - बस एक nullptr दें और वैकल्पिक रूप से
कोई गड़बड़ी लॉग करें.
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
आगे है kCsr
. यहां हम थोड़ी धोखा करने जा रहे हैं. "हैलो वर्ल्ड" प्रोग्राम
हालांकि, यह किसी असल सीएसआर अपडेट पर निर्भर नहीं होता, लेकिन कुछ बॉयलरप्लेट कोड जो
सीएसआर निर्देशों को एक्ज़ीक्यूट करें. इसका समाधान यह है कि
"सीएसआर" नाम का सामान्य रजिस्टर और चैनल को इस तरह लिखा जाना चाहिए.
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
अगला नंबर kNextPc
है, जो "पीसी" के बारे में है रजिस्टर करें. इसका इस्तेमाल टारगेट के तौर पर किया जाता है
और सीधे निर्देशों का पालन करें. RiscVState
में यह नाम इस तरह से बताया गया है
kPcName
.
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
आखिर में kRd
डेस्टिनेशन ऑपरेंड है. riscv32i.isa
में ऑपरेंड
rd
का इस्तेमाल सिर्फ़ "rd" में एन्कोड किए गए पूर्णांक रजिस्टर को देखने के लिए किया जाता है फ़ील्ड
साफ़ तौर पर बताया गया है, इसलिए इसका मतलब साफ़ तौर पर नहीं बताया गया है. यह लीजिए
सिर्फ़ एक Android घड़ी का विजेट है. x0
(abi नाम zero
) को रजिस्टर करें. यह 0 से हार्डवायर किया गया है,
रजिस्टर करने के लिए, हम DevNullOperand
का इस्तेमाल करते हैं.
इसलिए इस गेटर में हम पहले rd
फ़ील्ड में
.bin_fmt फ़ाइल से Extract
तरीका जनरेट किया गया. मान 0 होने पर, हम
"DevNull" वापस करो ऑपरेंड नहीं है, तो सही रजिस्टर ऑपरेंड लौटाया जा सकता है,
संकार्य नाम के रूप में उचित पंजीकृत अन्य नाम का उपयोग करने पर ध्यान देते हैं.
dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
// First extract register number from rd field.
int num = inst32_format::ExtractRd(inst_word_);
// For register x0, return the DevNull operand.
if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
// Return the proper register operand.
return GetRegisterDestinationOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
xreg_alias_[num]);
)
}
}
अब InitializeSourceOperandGetters()
तरीके पर जाएं, जहां पैटर्न
ज़्यादा समान नहीं है, लेकिन जानकारी थोड़ी अलग है.
चलिए, सबसे पहले उस SourceOpEnum
पर नज़र डालते हैं जिसे इनसे जनरेट किया गया था
पहले ट्यूटोरियल में riscv32i.isa
:
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
सदस्यों की जांच करने पर, kNone
के अलावा, वे दो ग्रुप में आते हैं. एक
तुरंत ऑपरेंड है: kBimm12
, kImm12
, kJimm20
, kSimm12
, kUimm20
,
और kUimm5
. अन्य रजिस्टर ऑपरेंड हैं: kCsr
, kRs1
, और kRs2
.
kNone
ऑपरेंड को गंतव्य ऑपरेंड की तरह ही हैंडल किया जाता है - एक
शून्य.
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
अब रजिस्टर ऑपरेंड के बारे में बात करते हैं. हम इसके जैसी kCsr
को हैंडल करेंगे
हम संबंधित गंतव्य ऑपरेंड को कैसे हैंडल करते हैं - बस
"सीएसआर" का इस्तेमाल करने वाला हेल्पर फ़ंक्शन डालें.
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
ऑपरेट kRs1
और kRs2
को kRd
की तरह ही हैंडल किया जाता है. हालांकि, इनमें से कोई एक लागू नहीं होता
हम x0
(या zero
) को अपडेट नहीं करना चाहते थे. हालांकि, हम यह पक्का करना चाहते हैं कि
हम हमेशा उस ऑपरेंड से 0 पढ़ते हैं. इसके लिए हम
generic::IntLiteralOperand<>
क्लास इसमें तय की गई है
literal_operand.h
.
इस ऑपरेंड का इस्तेमाल लिटरल वैल्यू को स्टोर करने के लिए किया जाता है, न कि किसी सिम्युलेटेड वैल्यू को स्टोर करने के लिए
आस-पास की वैल्यू). अगर ऐसा नहीं है, तो पैटर्न एक जैसा रहेगा: सबसे पहले,
निर्देश वाले शब्द से rs1/rs2 वैल्यू, अगर यह शून्य है, तो लिटरल वैल्यू होती है
0 टेंप्लेट पैरामीटर वाला ऑपरेंड, ऐसा नहीं होने पर सामान्य रजिस्टर मिलता है
सहायक फ़ंक्शन का इस्तेमाल करके सोर्स ऑपरेंड, जिसमें संकार्य के रूप में abi उपनाम का इस्तेमाल किया गया है
नाम.
source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs1(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs2(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
आखिर में, हम अलग-अलग तुरंत काम करने वाले ऑपरेंड को हैंडल करते हैं. तुरंत मान यह हैं
generic::ImmediateOperand<>
में तय की गई क्लास के इंस्टेंस में सेव किया गया
immediate_operand.h
.
मौजूदा ऑपरेंड के लिए अलग-अलग गेटर के बीच सिर्फ़ अंतर
एक्सट्रैक्टर फ़ंक्शन का इस्तेमाल करता है. साथ ही, यह बताता है कि स्टोरेज का टाइप साइन किया हुआ है या नहीं या
बिटफ़ील्ड के अनुसार साइन नहीं किया गया है.
// Immediates.
source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractBImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractImm12(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm5(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractJImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractSImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm32(inst_word_));
};
}
अगर आपको मदद चाहिए (या अपने काम की जांच करनी है), तो इसका पूरा जवाब यह है यहां पढ़ें.
इसी के साथ यह ट्यूटोरियल खत्म होता है. हमें उम्मीद है कि यह उपयोगी रहा होगा.