रस्ट डिबगर: step by step के हुड के नीचे कैसे झाँकें
\n\nक्या आपने कभी सोचा है: «Step by step interactive debugger for Rust: यह हुड के नीचे कैसे काम करता है?» यदि आपने Rust में लिखा है और किसी मुश्किल बग को पकड़ने की कोशिश की है, तो आपने निश्चित रूप से GDB, LLDB या rust-gdb जैसे टूल का सामना किया होगा। लेकिन वे प्रोग्राम के निष्पादन को कैसे रोकते हैं, वेरिएबल के मान कैसे दिखाते हैं और सोर्स कोड पर कदम कैसे बढ़ाते हैं?
इस लेख में, हम केवल यह नहीं बताएंगे कि डिबगर का उपयोग कैसे करें। हम अंदर झाँकेंगे: मशीन कोड से लेकर ब्रेकपॉइंट तक, स्टैक फ्रेम से लेकर डिबग सिंबल तक। यदि आप न केवल क्या करना है, बल्कि क्यों यह काम करता है, इसमें रुचि रखते हैं — तो आप सही जगह पर हैं।
\n\nडिबगिंग के लिए Rust एक विशेष मामला क्यों है?
\n\nRust LLVM के माध्यम से नेटिव कोड में कंपाइल होता है। इसका मतलब है कि डिबगिंग के लिए हम वही टूल का उपयोग करते हैं जो C/C++ के लिए करते हैं: GDB और LLDB। लेकिन Rust अपनी जटिलताएँ जोड़ता है:
- \n
- स्वामित्व और उधार — वेरिएबल को स्थानांतरित किया जा सकता है, और डिबगर को इस पर नज़र रखनी चाहिए। \n
- जेनेरिक का मोनोमॉर्फ़िज़ेशन — प्रत्येक
Tएक विशिष्ट प्रकार में बदल जाता है, और फ़ंक्शन नाम «एन्क्रिप्टेड» हो जाते हैं। \n - ऑप्टिमाइज़ेशन — कंपाइलर कोड को पुनर्व्यवस्थित कर सकता है, और डिबगर को इसे वापस «अनरोल» करना होगा। \n
यही कारण है कि step by step interactive debugger for Rust केवल GDB के तहत प्रोग्राम चलाना नहीं है। यह एक पूरी प्रणाली है जो मशीन पतों को वापस सोर्स कोड की पंक्तियों में अनुवादित करती है।
\n\nब्रेकपॉइंट हुड के नीचे कैसे काम करता है?
\n\nकल्पना करें: आप println!("Hello"); पंक्ति पर एक ब्रेकपॉइंट सेट करते हैं। प्रोसेसर में क्या होता है?
जब आप डिबगर के तहत प्रोग्राम चलाते हैं, तो यह निम्नलिखित करता है:
\n\n- \n
- डिबग जानकारी पढ़ता है (DWARF प्रारूप) निष्पादन योग्य फ़ाइल से। वहाँ पत्राचार संग्रहीत होता है: मेमोरी में पता → कोड में पंक्ति। \n
- निर्देश को बदलता है उस पते पर एक विशेष कमांड
INT3(x86 पर —0xCC) से। यह प्रोसेसर के लिए एक «जाल» है। \n - जब निष्पादन इस बिंदु पर पहुँचता है, तो प्रोसेसर सिग्नल SIGTRAP उत्पन्न करता है। \n
- डिबगर सिग्नल को इंटरसेप्ट करता है, रजिस्टरों की स्थिति को सहेजता है और आपको वर्तमान पंक्ति दिखाता है। \n
यहाँ Rust में एक सरल प्रोग्राम का उदाहरण है:
\n\nfn main() {\n let x = 42;\n let y = x + 10; // <-- ब्रेकपॉइंट\n println!("y = {}", y);\n}\n\nजब आप let y = x + 10; पर ब्रेकपॉइंट सेट करते हैं, तो डिबगर बाइनरी में इस निर्देश का पता ढूंढता है और पहले बाइट को 0xCC से बदल देता है। रुकने के बाद, यह मूल बाइट को पुनर्स्थापित करता है, एक कदम निष्पादित करता है, और फिर से जाल सेट करता है — यदि आप जारी रखना चाहते हैं।
Step over, step into, step out — डिबगर कैसे जानता है कि «अगली पंक्ति» कहाँ है?
\n\nयह शायद सबसे जादुई हिस्सा है। डिबगर कैसे जानता है कि let y = x + 10; के बाद println! आता है?
यह सब डिबग टेबल के बारे में है। Rust कंपाइलर (LLVM के माध्यम से) DWARF प्रारूप में एक .debug_line सेक्शन उत्पन्न करता है। यह एक संपीड़ित तालिका है जो निर्देश पतों को सोर्स कोड में पंक्ति संख्याओं से मैप करती है।
उदाहरण के लिए, हमारे कोड के लिए तालिका इस तरह दिख सकती है:
\n\nपता 0x1000 → पंक्ति 2 (let x = 42;)\nपता 0x1010 → पंक्ति 3 (let y = x + 10;)\nपता 0x1020 → पंक्ति 4 (println!)\n\nजब आप step over दबाते हैं, तो डिबगर अगले निर्देश के पते की गणना करता है जो वर्तमान पंक्ति से संबंधित नहीं है, और वहाँ एक अस्थायी ब्रेकपॉइंट सेट करता है। निष्पादन इस पते तक जारी रहता है — और आप अगली पंक्ति पर चले जाते हैं।
\n\nStep into के लिए — डिबगर जाँचता है कि क्या वर्तमान पते पर कोई फ़ंक्शन कॉल है। यदि हाँ, तो यह उस फ़ंक्शन की पहली पंक्ति की तलाश करता है।