مصحح أخطاء Rust: كيف تنظر تحت الغطاء خطوة بخطوة
\n\nهل تساءلت يومًا: «مصحح الأخطاء التفاعلي خطوة بخطوة لـ Rust: كيف يعمل تحت الغطاء؟» إذا كنت قد كتبت بلغة Rust وحاولت تعقب خطأ صعب، فمن المحتمل أنك صادفت GDB أو LLDB أو حتى أدوات مثل rust-gdb. ولكن كيف بالضبط يمكنها إيقاف تنفيذ البرنامج، وعرض قيم المتغيرات، والتنقل عبر الكود المصدري؟
في هذه المقالة، لن نشرح فقط كيفية استخدام مصحح الأخطاء. سننظر إلى الداخل: من الكود الآلي إلى نقاط التوقف، من إطارات المكدس إلى رموز التصحيح. إذا كنت مهتمًا ليس فقط بما يجب فعله، ولكن أيضًا لماذا يعمل هذا، فأنت في المكان الصحيح.
\n\nلماذا تعتبر Rust حالة خاصة للتصحيح؟
\n\nيتم تجميع Rust إلى كود أصلي عبر LLVM. هذا يعني أننا نستخدم نفس الأدوات المستخدمة في C/C++ للتصحيح: GDB و LLDB. لكن Rust يضيف تعقيداته الخاصة:
- \n
- الملكية والاقتراض — يمكن نقل المتغيرات، ويجب على مصحح الأخطاء تتبع ذلك. \n
- أحادية الشكل للعامات — كل
Tيتحول إلى نوع محدد، وتصبح أسماء الدوال «مشفرة». \n - التحسينات — يمكن للمترجم إعادة ترتيب الكود، ويجب على مصحح الأخطاء «فك» ذلك. \n
لهذا السبب، مصحح الأخطاء التفاعلي خطوة بخطوة لـ Rust ليس مجرد تشغيل برنامج تحت GDB. إنه نظام كامل يترجم العناوين الآلية مرة أخرى إلى أسطر من الكود المصدري.
\n\nكيف تعمل نقطة التوقف (breakpoint) تحت الغطاء؟
\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. بعد التوقف، يستعيد البايت الأصلي، وينفذ خطوة، ثم يعيد وضع الفخ — إذا كنت ترغب في المتابعة.
تجاوز الخطوة، الدخول إلى الخطوة، الخروج من الخطوة — كيف يعرف مصحح الأخطاء أين «السطر التالي»؟
\n\nهذا، ربما، هو الجزء الأكثر سحرًا. كيف يعرف مصحح الأخطاء أنه بعد let y = x + 10; يأتي println!؟
الأمر كله يتعلق بـ جداول التصحيح. يقوم مترجم Rust (عبر LLVM) بإنشاء قسم .debug_line بتنسيق DWARF. هذا جدول مضغوط يربط عناوين التعليمات بأرقام الأسطر في الكود المصدري.
على سبيل المثال، بالنسبة لكودنا، قد يبدو الجدول كالتالي:
\n\nالعنوان 0x1000 → السطر 2 (let x = 42;)\nالعنوان 0x1010 → السطر 3 (let y = x + 10;)\nالعنوان 0x1020 → السطر 4 (println!)\n\nعند الضغط على تجاوز الخطوة، يحسب مصحح الأخطاء عنوان التعليمات التالية التي لا تنتمي إلى السطر الحالي، ويضع نقطة توقف مؤقتة هناك. يستمر التنفيذ حتى هذا العنوان — وتنتقل إلى السطر التالي.
\n\nبالنسبة لـ الدخول إلى الخطوة — يتحقق مصحح الأخطاء مما إذا كان هناك استدعاء دالة في العنوان الحالي. إذا كان الأمر كذلك، فإنه يبحث عن السطر الأول لتلك الدالة.