जैसा कि compute units tutorial में चर्चा की गई है, किसी Solana प्रोग्राम कॉल द्वारा खपत किए गए compute units निष्पादित (executed) SBF (Solana Bytecode Format) निर्देशों की संख्या और किसी भी syscalls के रनटाइम लागत के योग के बराबर होते हैं। यह लेख SBF instruction set में गहराई से जाता है और यह प्रदर्शित करता है कि execution traces और agave-ledger-tool का उपयोग करके उन निर्देशों का विश्लेषण कैसे किया जाए।
Rust to SBF ट्यूटोरियल से, हम जानते हैं कि Solana प्रोग्राम SBF (Solana Bytecode Format) में संकलित (compile) होते हैं, जो sBPF virtual machine, पर चलता है, यह eBPF से लिया गया एक Solana-विशिष्ट VM है। SBF निर्देश x86 या ARM जैसी पारंपरिक assembly भाषाओं के समान होते हैं और इस तरह दिखते हैं:
mov64 r0, 1 ; move 1 (64 bit padded) to register 0
mov64 r1, 2 ; move 2 (64 bit padded) to register 1
add64 r0, r1 ; add register 1 to register 0, store result in register 0
पूर्वापेक्षाएँ (Prerequisites)
यह लेख मानकर चलता है कि आपने निम्नलिखित पूरा कर लिया है:
- Compute Units tutorial - Solana compute units कैसे काम करते हैं
- Rust to SBF Compilation tutorial - Solana प्रोग्राम compilation पाइपलाइन को समझने के लिए
- असेंबली कॉन्सेप्ट्स (registers, memory addressing, jumps) की बुनियादी जानकारी होना भी आवश्यक है।
Rust to SBF compilation ट्यूटोरियल में, हमने समझाया था कि Solana प्रोग्राम तीन मुख्य चरणों से कैसे गुजरते हैं: Rust से LLVM IR, फिर SBF bytecode, और अंत में native code। यह लेख Solana के VM आर्किटेक्चर को कवर करता है और दिखाता है कि व्यवहार में SBF bytecode का विश्लेषण कैसे किया जाए।
Solana Virtual Machine आर्किटेक्चर
Solana VM register-based है, जबकि Ethereum VM stack-based है। एक register-based VM में, निर्देश (instructions) निश्चित आकार के स्टोरेज स्लॉट के एक निश्चित सेट पर काम करते हैं जिन्हें registers कहा जाता है, और प्रत्येक निर्देश स्पष्ट रूप से बताता है कि वह किन registers से पढ़ता है और किन में लिखता है। एक stack-based VM में, निर्देश अंतर्निहित (implicitly) रूप से एक स्टैक डेटा संरचना के शीर्ष (top) पर काम करते हैं, इसलिए उपयोग किए जाने वाले ऑपरेंड (operands) को स्टैक पर पुश (push) किया जाना चाहिए और उससे पॉप (pop) किया जाना चाहिए।
eBPF 11 registers (R0–R10) को परिभाषित करता है, जो सभी 64 बिट चौड़े हैं। Solana का sBPF VM इन्हीं 11 registers को लागू करता है, लेकिन आंतरिक रूप से एक छिपा हुआ बारहवां रजिस्टर, R11 बनाए रखता है, जिसका उपयोग प्रोग्राम काउंटर ट्रैकिंग के लिए किया जाता है। चूंकि निष्पादन (execution) के दौरान प्रोग्राम द्वारा R11 को न तो पढ़ा जा सकता है और न ही लिखा जा सकता है, इसलिए प्रोग्राम कोड के लिए केवल मूल 11 eBPF registers ही दृश्यमान (visible) होते हैं।
eBPF विनिर्देश (specification) के अनुसार, registers के निम्नलिखित उपयोग के मामले (use cases) हैं:
R0: यह रजिस्टर फ़ंक्शन कॉल से रिटर्न वैल्यू और प्रोग्राम की एक्जिट वैल्यू को होल्ड करता हैR1-R5: ये registers फ़ंक्शन कॉल के आर्गुमेंट्स (arguments) को होल्ड करते हैंR6-R9: ये callee-saved registers हैं, जिसका अर्थ है कि इन्हें फ़ंक्शन कॉल के दौरान संरक्षित (preserved) रखा जाना चाहिएR10: यह एक read-only फ्रेम पॉइंटर है जो वर्तमान स्टैक फ्रेम को इंगित (point) करता है
R0-R5 स्क्रैच (scratch) registers हैं जिन्हें फ़ंक्शंस बिना सेव किए ओवरराइट (overwrite) कर सकते हैं। R6-R9 callee-saved registers हैं जिन्हें फ़ंक्शन कॉल के दौरान संरक्षित किया जाना चाहिए। इसका मतलब यह है कि जब कोई फ़ंक्शन foo() bar() को कॉल करता है, तो कॉल के बाद foo() को जिन भी मानों (values) की आवश्यकता होती है, उन्हें R6–R9 में रखा जाना चाहिए। यदि bar() को उन registers का उपयोग करने की आवश्यकता है, तो यह प्रवेश (entry) करते समय उनके कंटेंट को अपने स्वयं के स्टैक फ्रेम में सेव करता है और foo() पर लौटने से पहले उन्हें पुनर्स्थापित (restore) करता है। इस प्रक्रिया को spilling (स्टैक पर सेव करना) और filling (स्टैक से पुनर्स्थापित करना) कहा जाता है।
SBF Instruction Set (Opcodes)
जैसा कि हम जानते हैं, SBF eBPF पर आधारित है, इसलिए वे समान instruction set का उपयोग करते हैं। Solana VM द्वारा उपयोग किए जाने वाले सभी निर्देश (opcodes) here परिभाषित हैं, और आप eBPF specification में विवरण के साथ पूरा सेट भी पा सकते हैं।
इन opcodes में शामिल हैं:
Arithmetic और Logic Operations:
- Arithmetic opcodes:
add,sub,mul,div,mod,neg(negate),sdiv(signed division) औरsmod(signed modulo) - Logical opcodes:
and,or,xor,lsh(left shift),rsh(right shift),arsh(arithmetic right shift) - प्रत्येक opcode का एक 64-बिट संस्करण (डिफ़ॉल्ट) और एक 32-बिट संस्करण होता है
- प्रत्येक opcode के दो रूप होते हैं: एक जो दो registers (destination और source) लेता है, और दूसरा जो एक रजिस्टर और एक immediate value (प्रोग्राम बायटेकोड में हार्डकोड किया गया एक स्थिरांक) लेता है। उदाहरण के लिए,
add64 r0, r1रजिस्टर r1 को r0 में जोड़ता है, जबकिadd64 r0, 42स्थिरांक 42 को r0 में जोड़ता है।
Data Movement:
- एक
movopcode भी है जो registers के बीच या immediates से registers में मान (values) कॉपी करता है
Control Flow:
Solana VM में unconditional और conditional jumps के लिए opcodes होते हैं।
jaएक unconditional jump करता है। यह बिना कुछ जाँचे निष्पादन (execution) को दूसरे निर्देश ऑफ़सेट पर ले जाता है।- फिर conditional jumps आते हैं।
jeqजंप करता है यदि दो मान बराबर हैं।jneजंप करता है यदि वे बराबर नहीं हैं।jltऔरjgtless than या greater than की जाँच करते हैं।jleऔरjgeless than or equal, या greater than or equal की जाँच करते हैं। - इनके signed वर्ज़न भी हैं।
jslt,jsgt,jsle, औरjsgeसमान तुलनाओं को संभालते हैं लेकिन ऑपरेंड्स को signed integers के रूप में मानते हैं। callनिष्पादन को बायटेकोड के एक लेबल किए गए हिस्से (जैसे संकलित फ़ंक्शन) पर ले जाता है। syscalls के लिए, प्रोग्रामsyscallनिर्देश (न किcall) का उपयोग करते हैं। syscall निर्देश के बाद एक पहचानकर्ता (identifier) होता है जो यह निर्दिष्ट करता है कि किस syscall को लागू करना है, उदाहरण के लिएsol_log_याsol_log_64_।exitकॉलर पर वापस लौटता है, या यदि कॉल स्टैक खाली है तो प्रोग्राम निष्पादन को समाप्त कर देता है।
Memory Operations:
मेमोरी रीड और राइट ऑपरेशंस करने के लिए भी opcodes मौजूद हैं।
ldxमेमोरी से रजिस्टर में पढ़ता (read) है, औरstxरजिस्टर से मेमोरी में लिखता (write) है।- load और store निर्देशों में प्रत्यय (suffixes) के साथ आकार के संस्करण होते हैं जो दिखाते हैं कि प्रत्येक संस्करण कितने बाइट्स पढ़ता या लिखता है। इसलिए load के लिए, हमारे पास हैं:
ldxdw,ldxw,ldxh,ldxb। यहाँldxdw8 बाइट्स (double word) लोड करता है,ldxw4 बाइट्स (a word) लोड करता है,ldxh2 बाइट्स (half word) लोड करता है, औरldxb1 बाइट लोड करता है। store संस्करण भी इसी पैटर्न का पालन करते हैं। चूँकि सभी registers 64 बिट्स चौड़े होते हैं, छोटे ऑपरेशंस (32-बिट, 16-बिट, 8-बिट) रजिस्टर के निचले बिट्स पर लिखते हैं और ऊपरी बिट्स को ज़ीरो (zero out) कर देते हैं। - Load दो ऑपरेंड लेता है: एक destination रजिस्टर और एक मेमोरी एड्रेस। उदाहरण के लिए,
ldxdw r0, [r1+0x08]r1+0x08एड्रेस पर मेमोरी से 8 बाइट्स रजिस्टरr0में लोड करता है। सिंटैक्स[r1+0x08]का अर्थ है:r1में स्टोर किए गए एड्रेस को लें, 0x08-बाइट ऑफ़सेट जोड़ें, फिर उस अंतिम एड्रेस से पढ़ें। - Store भी दो ऑपरेंड लेता है: एक मेमोरी एड्रेस और एक source रजिस्टर। उदाहरण के लिए,
stxdw [r1+0x08], r0रजिस्टरr0से 8 बाइट्स मेमोरी में एड्रेसr1+0x08पर स्टोर करता है। - मेमोरी एड्रेस की गणना ऊपर दिखाए गए अनुसार एक बेस रजिस्टर प्लस एक ऑफ़सेट (जैसे,
[r1+0x08]) के रूप में की जाती है।
अगले कदम
अब जब आप sBPF VM आर्किटेक्चर, रजिस्टर कन्वेंशन और instruction set को समझ गए हैं, तो अगला लेख यह प्रदर्शित करता है कि traces का उपयोग करके प्रोग्राम निष्पादन (execution) का विश्लेषण कैसे किया जाए और वास्तविक बायटेकोड निष्पादन से compute units की गणना कैसे की जाए।
यह लेख Solana development पर एक ट्यूटोरियल श्रृंखला का हिस्सा है