GDB
معمارية تطبيقات المصادر المفتوحة (المجلد الثاني)GDB
معمارية تطبيقات المصادر المفتوحة (المجلد الثاني) GDB
Stan Shebs
إذا استمتعت بهذه الكتب، فقد يعجبك أيضًا التصميم البرمجي بالأمثلة في Python والتصميم البرمجي بالأمثلة في JavaScript.
كان GDB، أي مصحِّح أخطاء GNU (GNU Debugger)، من أوائل البرمجيات التي كُتبت لمؤسسة البرمجيات الحرة (Free Software Foundation)، وظلّ منذ ذلك الحين عمادًا لأنظمة البرمجيات الحرة والمفتوحة المصدر. صُمِّم في الأصل بوصفه مصحِّح أخطاء عاديًا على مستوى الشيفرة المصدرية لنظام Unix، ثم توسّع منذ ذلك الحين ليغطي طيفًا واسعًا من الاستخدامات، بما في ذلك استخدامه مع العديد من الأنظمة المُدمجة (embedded)، ونما من بضعة آلاف من أسطر لغة C إلى أكثر من نصف مليون سطر.
سيتناول هذا الفصل البنية الداخلية العامة لـ GDB، مبيِّنًا كيف تطوّر تدريجيًا مع ظهور احتياجات مستخدمين جديدة وميزات جديدة بمرور الزمن.
4.1. الهدف
صُمِّم GDB ليكون مصحِّح أخطاء رمزيًا (symbolic debugger) للبرمجيات المكتوبة بلغات أمرية مُترجَمة (compiled imperative languages) مثل C وC++ وAda وFortran. وفي واجهة سطر الأوامر الأصلية، يبدو الاستخدام النموذجي على النحو الآتي:
% gdb myprog
[...]
(gdb) break buggy_function
Breakpoint 1 at 0x12345678: file myprog.c, line 232.
(gdb) run 45 92
Starting program: myprog
Breakpoint 1, buggy_function (arg1=45, arg2=92) at myprog.c:232
232 result = positive_variable * arg1 + arg2;
(gdb) print positive_variable
$$1 = -34
(gdb)
يعرض GDB أمرًا ما غير صحيح، فيقول المطوّر «آها» أو «همم»، ثم عليه أن يقرّر معًا ما هو الخطأ وكيف يُصلحه.
النقطة المهمة بالنسبة إلى التصميم هي أن أداة مثل GDB هي في جوهرها صندوق أدوات تفاعلي للتنقّل داخل برنامج، وبالتالي فإنها تحتاج إلى الاستجابة لسلسلة غير متوقّعة من الطلبات. كما أنها ستُستخدم مع برامج حُسِّنت بالمُصرِّف (compiler)، وبرامج تستغل كل خيار عتادي من أجل الأداء، لذا فإنها تحتاج إلى معرفة تفصيلية حتى في أدنى مستويات النظام.
يحتاج GDB كذلك إلى القدرة على تصحيح أخطاء برامج صُرِّفت بمُصرِّفات مختلفة (لا بمُصرِّف C الخاص بـ GNU فحسب)، وإلى تصحيح أخطاء برامج صُرِّفت قبل سنوات بإصدارات قديمة إلى حدّ بعيد من المُصرِّفات، وإلى تصحيح أخطاء برامج تكون معلوماتها الرمزية مفقودة أو قديمة أو خاطئة ببساطة؛ ومن ثمّ فإن اعتبارًا آخر في التصميم هو أن يظلّ GDB عاملًا ومفيدًا حتى حين تكون بيانات البرنامج مفقودة أو تالفة أو غير مفهومة ببساطة.
تفترض الأقسام التالية إلمامًا عابرًا باستخدام GDB من سطر الأوامر. فإن كنت جديدًا على GDB، فجرّبه وتصفّح الدليل.SPS+00
4.2. نشأة GDB
GDB برنامج عتيق. لقد ظهر إلى الوجود نحو عام 1985، وكتبه ريتشارد ستولمان (Richard Stallman) إلى جانب GCC وGNU Emacs وسائر مكوّنات GNU الأولى. (في تلك الأيام لم تكن هناك مستودعات عامة للتحكّم في الشيفرة المصدرية، وقد فُقد اليوم كثير من تاريخ التطوير التفصيلي.)
أقدم الإصدارات المتاحة بسهولة تعود إلى عام 1988، والمقارنة مع الشيفرة الحالية تُظهر أن عددًا ضئيلًا فقط من الأسطر لا يزال يشبه الأصل؛ فكل ما تقريبًا من GDB أُعيدت كتابته مرة واحدة على الأقل. وثمة أمر آخر لافت في الإصدارات المبكرة من GDB، هو أن الأهداف الأصلية كانت متواضعة إلى حدّ ما، وأن كثيرًا من العمل الذي أُنجز منذ ذلك الحين كان توسعةً لـ GDB نحو بيئات واستخدامات لم تكن جزءًا من الخطة الأصلية.
4.3. المخطط الكتلي
الشكل 4.1: البنية الكلية لـ GDB
على أكبر مقياس، يمكن القول إن GDB له جانبان:
- «جانب الرموز» (symbol side) يهتمّ بالمعلومات الرمزية الخاصة بالبرنامج. وتتضمّن المعلومات الرمزية أسماء الدوال والمتغيّرات وأنواعها، وأرقام الأسطر، واستخدام سجلّات الآلة، وغيرها. يستخرج جانب الرموز المعلومات الرمزية من الملف التنفيذي للبرنامج، ويحلّل التعابير، ويجد عنوان الذاكرة الخاص بسطر معيّن، ويسرد الشيفرة المصدرية، ويعمل عمومًا مع البرنامج كما كتبه المبرمج.
- «جانب الهدف» (target side) يهتمّ بالتحكّم في نظام الهدف. ولديه وسائل لبدء البرنامج وإيقافه، ولقراءة الذاكرة والسجلّات وتعديلها، وللالتقاط الإشارات، وغيرها. وتختلف تفاصيل كيفية ذلك اختلافًا كبيرًا بين الأنظمة؛ فمعظم الأنظمة من طراز Unix توفّر استدعاء نظام خاصًا اسمه
ptraceيمنح إحدى العمليات القدرة على قراءة حالة عملية أخرى وكتابتها. وبذلك فإن جانب الهدف في GDB يتعلق في معظمه بإجراء استدعاءاتptraceوتفسير نتائجها. أما عند التصحيح المتقاطع (cross-debugging) لنظام مُدمج، فيقوم جانب الهدف بدلًا من ذلك ببناء حزم رسائل لإرسالها عبر وصلة، ثم انتظار حزم استجابة في المقابل.
الجانبان مستقلّان إلى حدّ ما عن بعضهما؛ فبإمكانك التنقّل في شيفرة برنامجك، وعرض أنواع المتغيّرات، إلخ، من دون تشغيل البرنامج فعليًا. وعلى العكس، من الممكن إجراء تنقيح بلغة الآلة خالصة حتى في غياب أي رموز.
وفي المنتصف، الذي يربط الجانبين معًا، يوجد مفسّر الأوامر وحلقة التحكّم الرئيسية بالتنفيذ.
4.4. أمثلة على العمل
لنأخذ حالة بسيطة تبيّن كيف يتّصل كل ذلك ببعضه، ولنتأمّل أمر print المذكور أعلاه. يجد مفسّر الأوامر دالةَ الأمر print، وهي تحلّل التعبير إلى بنية شجرية بسيطة ثم تقيّمه بالسير على تلك الشجرة. وفي مرحلة ما، يستشير المُقيِّم جدول الرموز ليتبيّن أن positive_variable متغيّر عام من نوع عدد صحيح مخزَّن، مثلًا، في عنوان الذاكرة 0x601028. ثم يستدعي عندئذٍ دالةً من جانب الهدف تقرأ بايتات الذاكرة الأربعة في ذلك العنوان، ويسلّمها إلى دالة تنسيق تعرضها كعدد عشري.
ولعرض الشيفرة المصدرية وصورتها المُصرَّفة، يقوم GDB بمزيج من عمليات القراءة من ملف الشيفرة المصدرية ومن نظام الهدف، ثم يستخدم معلومات أرقام الأسطر التي يولّدها المُصرِّف لربط الاثنتين. وفي المثال هنا، السطر 232 له العنوان 0x4004be، والسطر 233 عند 0x4004ce، وهكذا.
[...]
232 result = positive_variable * arg1 + arg2;
0x4004be <+10>: mov 0x200b64(%rip),%eax # 0x601028 <positive_variable>
0x4004c4 <+16>: imul -0x14(%rbp),%eax
0x4004c8 <+20>: add -0x18(%rbp),%eax
0x4004cb <+23>: mov %eax,-0x4(%rbp)
233 return result;
0x4004ce <+26>: mov -0x4(%rbp),%eax
[...]
يخفي الأمر step الخاص بالتنفيذ خطوةً بخطوة رقصةً معقّدة تجري في الكواليس. عندما يطلب المستخدم الانتقال إلى السطر التالي في البرنامج، يُطلَب من جانب الهدف أن ينفّذ تعليمةً واحدة فقط من البرنامج ثم يوقفه من جديد (وهذا أحد الأمور التي يستطيع ptrace إنجازها). وبعد إخطاره بأن البرنامج قد توقّف، يطلب GDB قيمة السجلّ PC (عدّاد البرنامج) (وهي عملية أخرى من عمليات جانب الهدف)، ثم يقارنها بالنطاق العِنَدي الذي يقول جانب الرموز إنّه مرتبط بالسطر الحالي. فإذا كان PC خارج ذلك النطاق، ترك GDB البرنامج متوقفًا، واستنتج سطر الشيفرة المصدرية الجديد، وأبلغ المستخدم به. وإذا ظلّ PC داخل نطاق السطر الحالي، خطوة GDB بتعليمة أخرى ثم أعاد الفحص، مكرِّرًا ذلك حتى يبلغ PC سطرًا مختلفًا. وتُقدِّم هذه الخوارزمية الأساسية ميزة أنها تفعل الشيء الصحيح دائمًا، سواء كان السطر يحتوي على قفزات أو استدعاءات روتين فرعي أو ما شابه، ولا تشترط من GDB تفسير كل تفاصيل مجموعة تعليمات الآلة. أما العيب فهو أن كل خطوة فردية تنطوي على تفاعلات كثيرة مع الهدف، وهو ما يؤدي، في بعض الأهداف المُدمجة، إلى خطوات فردية بطيئة بشكل ملحوظ.
4.5. القابلية للنقل
بصفته برنامجًا يحتاج إلى وصول واسع يمتدّ نزولًا حتى إلى السجلّات الفيزيائية على الشريحة، صُمِّم GDB منذ البداية ليكون قابلًا للنقل عبر مجموعة متنوعة من الأنظمة. غير أن استراتيجيته في القابلية للنقل تغيّر كثيرًا على مرّ السنين.
في الأصل، بدأ GDB على غرار بقية برمجيات GNU في تلك الحقبة؛ إذ كُتب بمجموعة فرعية مشتركة ومُدنية من C، واستخدم مزيجًا من ماكروهات المعالج المسبق (preprocessor macros) وقطع Makefile للتكيّف مع بنية معمارية ونظام تشغيل محدّدين. وبرغم أن الهدف المعلَن لمشروع GNU كان «نظام تشغيل GNU» مستقلًا بذاته، إلا أن التهيئة الانطلاقية (bootstrapping) كان يتعيّن إنجازها على مجموعة متنوعة من الأنظمة القائمة؛ فنواة Linux كانت لا تزال على بُعد سنوات في المستقبل. وسكربت configure المكتوب بلغة الصدفة (shell) هو الخطوة المفتاحية الأولى في هذه العملية. وهو يقدر على أمور متنوعة، مثل إنشاء رابط رمزي (symbolic link) من ملف خاص بنظام ما إلى اسم ترويسة عام، أو بناء ملفات من قطع صغيرة، والأهم من ذلك بناء ملف Makefile المستخدَم في بناء البرنامج.
للبرمجيات مثل GCC وGDB احتياجات إضافية في قابلية النقل لا تحتاجها حاجاتٌ مثل cat أو diff، ومع الوقت انفصلت أجزاء قابلية النقل في GDB إلى ثلاث فئات، لكلٍّ منها قطعة Makefile خاصة بها وملف ترويسة خاص بها.
- تعريفات «المضيف» (Host) تخصّ الآلة التي يعمل عليها GDB نفسه، وقد تضمّ أمورًا مثل أحجام أنواع الأعداد الصحيحة في المضيف. وقد نُفِّذت في الأصل بوصفها ملفات ترويسة يكتبها البشر، ثم لم يمض وقت طويل قبل أن يدرك الجميع أنها يمكن حسابها بجعل
configureيشغّل برامج اختبار صغيرة، بالمُصرِّف ذاته الذي سيُستخدم في بناء الأداة. وهذا هو كل ما قام عليهautoconfaut12، واليومَ تستخدم كل أدوات GNU تقريبًا، وكثير من برامج Unix (بل معظمها)، سكربتات configure المولَّدة بواسطةautoconf. - تعريفات «الهدف» (Target) خاصة بالآلة التي يُشغَّل عليها البرنامج الجاري تصحيح أخطاءه. فإذا كان الهدف هو المضيف نفسه، فإنا نُجري تنقيحًا «أصليًا» (native)، وإلا فالتنقيح «متقاطع» (cross)، مستخدمين وصلة ما تصل بين النظامين. وتنقسم تعريفات الهدف بدورها إلى فئتين رئيسيتين: تعريفات «البنية المعمارية» (Architecture): وهذه تعرّف كيف يُفكّ تجميع شيفرة الآلة، وكيف يُسار في مكدّس الاستدعاءات، وأي تعليمة فخّ (trap) يجب إدراجها عند نقاط التوقف. وقد نُفِّذت في الأصل بماكروهات، ثم نُقلت إلى C عادية يُطالَع بها عبر كائنات «gdbarch»، وهو ما نتناوله بمزيد من التفصيل أدناه.
- تعريفات «الأصلية» (Native): وهذه تعرّف تفاصيل وسائط
ptrace(التي تختلف اختلافًا كبيرًا بين نكهات Unix المختلفة)، وكيف يمكن العثور على المكتبات المشتركة التي جرى تحميلها، وما شابه ذلك، وكلها لا تنطبق إلا في حالة التنقيح الأصلي. وتُعدّ تعريفات «الأصلية» آخر معقل لماكروهات بأسلوب الثمانينيات، وإن كانت معظمها قد صارت تُستنتج اليوم باستخدامautoconf.
4.6. هياكل البيانات
قبل الغوص في أجزاء GDB، دعنا نطّلع على هياكل البيانات الرئيسة التي يعمل عليها GDB. ولأن GDB برنامج مكتوب بلغة C، فإن هذه الهياكل منفَّذة بوصفها structات لا بوصفها كائنات بأسلوب C++؛ غير أنها في معظم الحالات تُعامَل معاملة كائنات، وسنتبع هنا العادة التي يتبعها مطوّرو GDB في مناداتها كائنات.
نقاط التوقف
نقطة التوقف هي النوع الرئيس من الكائنات الذي يمكن للمستخدم الوصول إليه مباشرة. ويُنشئ المستخدم نقطة توقف بالأمر break، الذي تحدّد وسائطه موقعًا (location)، قد يكون اسم دالة، أو رقم سطر في الشيفرة المصدرية، أو عنوانًا في الذاكرة. ويمنح GDB كائن نقطة التوقف رقمًا صحيحًا صغيرًا موجبًا، يستخدمه المستخدم بعد ذلك للتعامل مع نقطة التوقف. وداخل GDB، تكون نقطة التوقف struct من نوع C تحوي عددًا من الحقول. ويُترجَم الموقع إلى عنوان آلي، لكنه يُحفظ أيضًا في صورته الأصلية، إذ قد يتغيّر العنوان ويحتاج إلى إعادة حساب، مثلًا إذا أُعيد تصريف البرنامج وأُعيد تحميله في جلسة.
وتشارك فعليًا عدة أنواع من الكائنات الشبيهة بنقاط التوقف في struct نقطة التوقف نفسه، بما في ذلك نقاط المراقبة (watchpoints) ونقاط الالتقاط (catchpoints) ونقاط التتبّع (tracepoints). ويساعد هذا على ضمان توفّر وسائل الإنشاء والتعالج والحذف بشكل متسق.
ويشير مصطلح «الموقع» أيضًا إلى عناوين الذاكرة التي ستُثبَّت عندها نقطة التوقف. وفي حالة الدوال المُضمَّنة (inline) وقوالب C++، قد يقابل نقطة توقف واحدة يحدّدها المستخدم عدة عناوين؛ فمثلًا، كل نسخة مُضمَّنة من دالة تستلزم موقعًا منفصلًا لنقطة التوقف الموضوعة على سطر في جسم تلك الدالة.
الرموز وجداول الرموز
جداول الرموز بنية بيانات محورية في GDB، وقد تكون كبيرة إلى حدّ كبير، يبلغ حجمها أحيانًا عدة غيغابايتات من الذاكرة العشوائية. وإلى حدّ ما هذا أمر لا مفرّ منه؛ فالتطبيق الكبير المكتوب بـ C++ يمكن أن يحوي ملايين الرموز بذاته، وهو يستورد ملفات ترويسة نظام قد تحوي ملايين إضافية من الرموز. وكل متغيّر محلي، وكل نوع مُسمًّى، وكل قيمة في تعداد—كل هذه رموز منفصلة.
ويستخدم GDB عددًا من الحيل لتقليل حجم جدول الرموز، مثل جداول الرموز الجزئية (وسنعود إليها لاحقًا)، وحقول البِتّات (bit fields) في structات، وما شابه.
إلى جانب جداول الرموز التي تربط أساسًا بين النصوص الحرفية ومعلومات العنوان والنوع، يبني GDB جداول أسطر تدعم البحث في اتجاهين: من أسطر الشيفرة المصدرية إلى العناوين، ثم من العناوين عكسيًا إلى أسطر الشيفرة المصدرية. (فمثلًا، تعتمد خوارزمية التنفيذ خطوة بخطوة الموصوفة سابقًا بشكل جوهري على تعيين العنوان إلى الشيفرة المصدرية.)
إطارات المكدس
تتشارك اللغات الإجرائية التي صُمِّم GDB من أجلها بنية تشغيل مشتركة، إذ إن نداءات الدوال تدفع عداد البرنامج على مكدّس، إلى جانب مزيج ما من وسائط الدالة ووسائطها المحلية. وتُسمّى هذه الحزمة إطار مكدس (stack frame)، أو «إطار» اختصارًا، وفي أي لحظة من تنفيذ البرنامج يتكوّن المكدس من تسلسل من الإطارات المتسلسلة ببعضها. وتختلف تفاصيل إطار المكدس جذريًا من بنية شريحة إلى أخرى، وهي أيضًا معتمدة على نظام التشغيل والمُصرِّف وخيارات التحسين.
قد تحتاج عملية نقل GDB إلى شريحة جديدة إلى حجم كبير من الشيفرة لتحليل المكدس، لأن البرامج (وخاصةً المعطوبة منها، وهي ما يهمّ مستخدمي المنقِّح في الغالب) يمكن أن تتوقف في أي موضع، ولعلّ الإطارات تكون ناقصة أو جزئيًا مُستبدَلة من البرنامج. والأمر الأسوأ، فإن بناء إطار مكدس لكل نداء دالة يُبطِّئ التطبيق، وسيستغلّ أي مُصرِّف محسِّن جيد كل فرصة لتبسيط إطارات المكدس، أو حتى إلغائها تمامًا، كما في نداءات الذيل.
تُسجَّل نتيجة تحليل GDB الخاص بالشريحة في سلسلة من كائنات الإطار. كان GDB في الأصل يتتبّع الإطارات عبر القيمة الحرفية لسجلّ مؤشر الإطار الثابت. وتفشل هذه المقاربة مع نداءات الدوال المُضمَّنة وسائر أنواع تحسينات المُصرِّف، فمنذ عام 2002 أدخل مطوّرو GDB كائنات إطار صريحة تسجّل ما تمّ استنتاجه عن كل إطار، وقد رُبطت ببعضها الواحد تلو الآخر بما يحاكي إطارات مكدّس البرنامج.
التعبيرات
كما في حال إطارات المكدس، يفترض GDB قدرًا من التوافق المشترك بين تعبيرات اللغات المختلفة التي يدعمها، ويمثّلها جميعًا في بنية شجرية مبنية من كائنات عُقد (nodes). ومجموعة أنواع العُقد هي في الواقع اتحادُ كل أنواع التعبيرات الممكنة في جميع اللغات المختلفة؛ وعلى خلاف ما يحدث في المُصرِّف، فلا يوجد سبب يمنع المستخدم من محاولة طرح متغيّر Fortran من متغيّر C—فقد يكون الفرق بينهما قوةً من قوى الاثنين واضحة، وهذا ما يخلق لحظة «آها».
القيم
قد تكون نتيجة التقييم نفسها أعقد من عدد صحيح أو عنوان ذاكرة، ويحتفظ GDB كذلك بنتائج التقييم في قائمة تاريخ مرقّمة، يمكن الإشارة إليها في تعبيرات لاحقة. ولجعل كل ذلك يعمل، لدى GDB بنية بيانات للقيم. وتحمل structات القيم عددًا من الحقول التي تسجّل خصائص مختلفة؛ ومن المهمّ منها إشارة إلى ما إذا كانت القيمة قيمةَ يمين (r-value) أم قيمةَ يسار (l-value) (يمكن الإسناد إلى قيم اليسار، كما في C)، وما إذا كان يلزم حساب القيمة على نحو كسول (lazily).
4.7. جانب الرموز
جانب الرموز في GDB مسؤول أساسًا عن قراءة الملف التنفيذي، واستخراج أي معلومات رمزية يجدها فيه، وبناء تلك المعلومات في جدول رموز.
تبدأ عملية القراءة بمكتبة BFD. وBFD مكتبة شبه عامة للتعامل مع الملفات الثنائية وملفات الكائنات؛ فهي تعمل على أي مضيف، ويمكنها قراءة الصيغة الأصلية a.out من Unix وCOFF (المستخدَمة في System V Unix وMS Windows) وELF (Unix الحديث وGNU/Linux ومعظم الأنظمة المُدمجة) وبعض صيغ الملفات الأخرى. وداخلًا، تمتلك المكتبة بنية معقّدة من ماكروهات C تتوسّع إلى شيفرة تستوعب التفاصيل الغامضة لصيغ ملفات الكائنات لعشرات الأنظمة المختلفة. وقد أُدخلت عام 1990، وتُستخدَم BFD أيضًا من قبل المُجمِّع (assembler) والرابط (linker) الخاصَّين بـ GNU، وقدرةَ GDB على إنتاج ملفات كائنات لأي هدف هي مفتاح التطوير المتقاطع باستخدام أدوات GNU. (ونقل BFD هو أيضًا الخطوة المفتاحية الأولى في نقل الأدوات إلى هدف جديد.)
لا يستخدم GDB من BFD إلا القراءة فقط، إذ يستخدمها لسحب كتل من البيانات من الملف التنفيذي إلى ذاكرة GDB. ويمتلك GDB بعدها مستويين خاصين به من دوال القراءة. المستوى الأول مخصّص للرموز الأساسية، أو «الرموز الدنيا» (minimal symbols)، وهي مجرّد الأسماء التي يحتاجها الرابط لينجز عمله. وهذه سلاسل نصية مصحوبة بعناوين ولا شيء يذكر غير ذلك؛ فنحن نفترض أن العناوين في مقاطع النص هي دوال، والعناوين في مقاطع البيانات هي بيانات، وهكذا.
أما المستوى الثاني فهو المعلومات الرمزية التفصيلية، التي تملك عادةً صيغة خاصة بها تختلف عن الصيغة الأساسية للملف التنفيذي؛ فمثلًا، المعلومات الموجودة في صيغة التنقيح DWARF مضمّنة في مقاطع ذات أسماء خاصة داخل ملف ELF. وفي المقابل، استخدمت صيغة التنقيح القديمة stabs في Berkeley Unix رموزًا موسومة بصفة خاصة مخزَّنة في جدول الرموز العام.
الشيفرة المكلَّفة بقراءة المعلومات الرمزية مملّة إلى حدّ ما، لأن صيغ الرموز المختلفة تُرمِّز كل نوع من معلومات الأنواع يمكن أن يوجد في برنامج مصدري، غير أن كل صيغة تتعامل مع ذلك بطريقتها الفريدة. ويكفي قارئ GDB أن يجتاز الصيغة، ويُنشئ رموز GDB نعتقد أنها تطابق ما يقصده تنسيق الرموز.
جداول الرموز الجزئية
في برنامج ذا حجم ملحوظ (مثل Emacs أو Firefox)، قد يستغرق بناء جدول الرموز وقتًا طويلًا، ربما عدة دقائق. وتُظهر القياسات باتساق أن الوقت ليس في قراءة الملفات كما قد يُتوقَّع، بل في بناء رموز GDB في الذاكرة. فهناك حرفيًا ملايين الكائنات الصغيرة المترابطة، والزمن يتراكم.
لن يُطَّلع على معظم المعلومات الرمزية في أي جلسة، لأنها محلية إلى دوال قد لا يطّلع عليها المستخدم أبدًا. ولهذا، حين يسحب GDB رموز برنامج ما أول مرة، فإنه يجري مسحًا سطحيًا للمعلومات الرمزية، باحثًا عن الرموز المرئية عالميًا فقط، ومسجِّلًا إياها وحدها في جدول الرموز. ولا تُستكمل المعلومات الرمزية الكاملة لدالة أو طريقة إلا إذا توقف المستخدم في داخلها.
تتيح جداول الرموز الجزئية لـ GDB أن يبدأ العمل في ثوانٍ معدودة فقط، حتى مع البرامج الكبيرة. (أما رموز المكتبات المشتركة فتُحمَّل ديناميكيًا أيضًا، لكن العملية تختلف اختلافًا كبيرًا. فعادةً ما يستخدم GDB تقنية خاصة بالنظام ليُخطَر عند تحميل المكتبة، ثم يبني جدول رموز يحوي الدوال عند العناوين التي حددها الرابط الديناميكي.)
دعم اللغات
يتكوّن دعم اللغة المصدري أساسًا من تحليل التعابير وطباعة القيم. أمّا تفاصيل تحليل التعابير فيُترك أمرها لكل لغة، لكن المحلِّل العام يقوم على قواعد Yacc يغذّيه محلِّل معجمي مصنوع يدويًا. وإلى جانب هدف GDB في منح المستخدم التفاعلي مرونة أكبر، لا يُتوقّع من المحلِّل أن يكون متشدّدًا على نحو خاص؛ فمثلًا، إن استطاع تخمين نوع معقول لتعبير ما، فسيكتفي بافتراض ذلك النوع بدلًا من مطالبة المستخدم بإضافة cast أو تحويل نوع.
ولأن المحلِّل لا يحتاج إلى معالجة الجمل أو تعريفات الأنواع، فهو أبسط بكثير من محلِّل اللغة الكامل. وعلى نحو مماثل، في ما يخصّ الطباعة، توجد حفنة من أنواع القيم فحسب التي يلزم عرضها، وكثيرًا ما تستدعي دالة الطباعة الخاصة باللغة كودًا عامًّا لإتمام المهمة.
4.8. جانب الهدف
كل ما يعنيه جانب الهدف هو التحكّم في تنفيذ البرنامج وفي بياناته الخام. بمعنى آخر، فإن جانب الهدف هو منقِّح أخطاء منخفض المستوى كامل؛ فإذا اكتفيت بال stepping على مستوى التعليمات وطرح الذاكرة الخام، يمكنك استخدام GDB من دون أي رموز على الإطلاق. (ولعلّك ستنتهي إلى العمل في هذا الوضع على أي حال، إذا اتّفق أن يتوقف البرنامج داخل مكتبة جُرِّدت رموزها.)
متجهات الهدف ومكدّس الهدف
كان جانب الهدف في GDB في الأصل مكوَّنًا من حفنة من الملفات الخاصة بكل منصة، تتولّى تفاصيل استدعاء ptrace وإطلاق الملفات التنفيذية وما شابه. وهذا ليس مرنًا بما يكفي لجلسات تنقيح طويلة الأمد، قد ينتقل فيها المستخدم من التنقيح المحلي إلى التنقيح عن بُعد، ومن الملفات إلى الذاكرة الصائبة إلى البرامج الحيّة، ويربط البرنامج بالعملية ثم يفصلها، إلخ. ولهذا فإن John Gilmore أعاد تصميم جانب الهدف في GDB عام 1990، ليُمرِّر جميع العمليات الخاصة بالهدف عبر متجه الهدف (target vector)، وهو في الجوهر صنف من الكائنات، كل كائن منها يعرّف تفاصيل نوعٍ من أنظمة الهدف. ويُنفَّذ كل متجه هدف في صورة بنية من عدة عشرات من مؤشّرات الدوال (وهي تسمّى غالبًا «طرائق» (methods))، تمتدّ أغراضها من قراءة الذاكرة والسجلّات وكتابتهما، إلى استئناف تنفيذ البرنامج، إلى ضبط وسائط معالجة المكتبات المشتركة. وهناك نحو 40 متجه هدف في GDB، تمتدّ من متجه الهدف واسع الاستخدام في Linux وصولًا إلى متجهات غامضة مثل المتجه الذي يشغّل Xilinx MicroBlaze. أما دعم الذاكرة الصائبة فيستخدم متجه هدف يجلب بياناته بقراءة ملف ذاكرة صائبة (corefile)، وهناك متجه هدف آخر يقرأ البيانات من الملف التنفيذي.
ومن المفيد كثيرًا أن نمزج طرائق من عدة متجهات هدف. لنأخذ طباعة متغيّر عام مُهيَّأ على Unix؛ فقبل أن يبدأ البرنامج في العمل، ينبغي أن تعمل طباعة المتغيّر، غير أنه لا توجد عند تلك النقطة عملية يمكن القراءة منها، ولا بدّ من أن تأتي البايتات من مقطع .data في الملف التنفيذي. ولهذا يستخدم GDB متجه الهدف الخاص بالملفات التنفيذية ويقرأ من الملف الثنائي. أما بينما يكون البرنامج قيد العمل، فمن المفترض أن تأتي البايتات بدلًا من ذلك من فضاء عناوين العملية. ولهذا يمتلك GDB «مكدّس هدف» تُدفع فيه متجهات الهدف الخاصة بالعمليات الحيّة فوق متجه الهدف الخاص بالملف التنفيذي عند بدء العملية في العمل، وتُسحب عند خروجها.
وفي الواقع، لا يتصف مكدّس الهدف بأنه مكدّسيّ بالقدر الذي قد يُفترض. فمتجهات الهدف ليست متعامدة حقًا مع بعضها؛ فإذا كان لديك في الجلسة ملف تنفيذي وعملية حيّة معًا، فرغم أن منطقيًا تجعل طرائق العملية الحيّة تتجاوز طرائق الملف التنفيذي، فإن عكس ذلك لا يكون منطقيًا تقريبًا أبدًا. ولهذا انتهى GDB إلى فكرة الطبقة (stratum)، تكون فيها متجهات الهدف «الشبيهة بعمليات» كلها في طبقة واحدة، بينما تُسنَد متجهات الهدف «الشبيهة بملفات» إلى طبقة أدنى، ويمكن لمتجهات الهدف أن تُدرَج إلى جانب دفعها وسحبها.
(ولأن مشرفي GDB لا يحبّون مكدّس الهدف كثيرًا، لم يقترح أحد—ولمجرِّب—أي بديل أفضل.)
Gdbarch
بصفته برنامجًا يتعامل مباشرةً مع تعليمات وحدة المعالجة المركزية (CPU)، يحتاج GDB إلى معرفة عميقة بتفاصيل الشريحة. فهو يحتاج إلى معرفة كل السجلّات، وأحجام مختلف أنواع البيانات، وحجم وشكل فضاء العناوين، وكيفية عمل اتفاقية النداء (calling convention)، وأي تعليمة ستسبّب استثناء فخّ (trap)، وما شابه. ويتراوح شيفرة GDB الخاصة بكل ذلك عادةً بين 1,000 وأكثر من 10,000 سطر C، بحسب تعقيد البنية المعمارية.
كان هذا يُعالَج في الأصل بماكروهات المعالج المسبق الخاصة بكل هدف، غير أن المنقِّح لمّا أصبح أكثر تقدّمًا كبرت هذه الماكروهات شيئًا فشيئًا، ومع الوقت صارت تعريفات الماكروهات الطويلة دوالّ C عادية تُستدعى من الماكروهات. وبرغم أن هذا كان مفيدًا، فلم يكن مفيدًا إلى حدٍّ بعيد مع التنويعات المعمارية (ARM مقابل Thumb، وإصدارات 32 بت مقابل 64 بت من MIPS أو x86، إلخ)، والأمر أسوأ أن تصاميم متعددة البنى كانت في الأفق، ولا تصلح لها الماكروهات البتّة. وفي عام 1995 اقترحت حلّ ذلك بتصميم قائم على الكائنات، اعتبارًا من عام 1998 موّلت شركة Cygnus Solutions Andrew Cagney لبدء عملية التحويل. (كانت Cygnus Solutions شركة أُنشئت عام 1989 لتقديم الدعم التجاري للبرمجيات الحرة، واستحوذت عليها Red Hat في عام 2000.) واستغرق إتمام المهمة عدة سنوات ومساهمات من عشرات الهاكرز، وطالت نحو 80,000 سطر شيفرة في المجمل.
تُسمَّى البنى التي أُدخلت كائنات gdbarch، وقد يبلغ عددها في الوقت الحالي ما يقارب 130 طريقة ومتغيّرًا تعرّف بنية هدف معمارية، وإن كان هدف بسيط قد لا يحتاج سوى اثني عشر أو نحوها.
ولكي تشعر بما يقارنه الأسلوبان القديم والجديد، انظر إلى التصريح بأن الأعداد العشرية المزدوجة الطويلة (long double) في x86 حجمها 96 بت في gdb/config/i386/tm-i386.h، نحو عام 2002:
#define TARGET_LONG_DOUBLE_BIT 96
ومن gdb/i386-tdep.c، في عام 2012:
i386_gdbarch_init( [...] )
{
[...]
set_gdbarch_long_double_bit (gdbarch, 96);
[...]
}
التحكم بالتنفيذ
قلب GDB هو حلقة التحكّم بالتنفيذ. وقد ألمَحنا إليها سابقًا عند وصف خطوة واحدة تتجاوز سطرًا؛ إذ كانت الخوارزمية تستلزم الدوران على عدة تعليمات حتى تُعثر على واحدة مرتبطة بسطر شيفرة مصدرية مختلف. وتُسمّى هذه الحلقة wait_for_inferior، أو «wfi» اختصارًا.
من الناحية المفاهيمية تقع هذه الحلقة داخل حلقة الأوامر الرئيسية، ولا يُدخَل إليها إلا في الأوامر التي تستأنف تنفيذ البرنامج. وعندما يكتب المستخدم continue أو step ثم ينتظر ريثما يبدو أنه لا يحدث شيء، فإن GDB قد يكون مشغولًا بالفعل وبشكل كبير. وإلى جانب حلقة الخطوة الواحدة المذكورة أعلاه، قد يكون البرنامج يصطدم بتعليمات الفخّ ويبلّغ عن الاستثناء إلى GDB. فإذا كان الاستثناء ناتجًا عن كون الفخّ نقطة توقف أدخلها GDB، فاختبر عندئذٍ شرط نقطة التوقف، وإذا كان خاطئًا، أزال الفخّ، ونفّذ التعليمة الأصلية خطوة بخطوة، وأعاد إدخال الفخّ، ثم ترك البرنامج يستأنف. وعلى نحو مماثل، إذا رُفعت إشارة، فقد يختار GDB تجاهلها، أو معالجتها بإحدى الطرق العديدة المحدَّدة مسبقًا.
وكل هذا النشاط تُديره wait_for_inferior. كانت في الأصل حلقة بسيطة، تنتظر توقف الهدف ثم تقرّر ما العمل بشأنه، لكن مع الحاجة إلى معالجات خاصة في عمليات النقل إلى مختلف الأنظمة، نمت إلى ألف سطر، وتتشابك فيها عبارات goto عبرها لأسباب لا يفهمها أحد على نحو جيّد. فمثلًا، مع تكاثر نكهات Unix، لم يكن هناك شخص واحد يفهم جميع دقائقها، ولم يكن في متناولنا كلّها لإجراء اختبارات الانحدار (regression testing)، لذا كانت هناك دافعة قوية لتعديل الشيفرة على نحو يحفظ السلوك تمامًا لعمليات النقل القائمة—وإضافة goto تقفز فوق جزء من الحلقة كانت تكتيكًا سهلًا إلى حدّ ما.
كما أن الحلقة الكبيرة الواحدة كانت تمثل مشكلة أيضًا بالنسبة إلى أي نوع من المعالجة اللامتزامنة أو إلى تنقيح أخطاء البرامج متعددة الخيوط (threaded)، حيث يريد المستخدم بدء خيط واحد وإيقافه مع السماح لبقية البرنامج بالاستمرار في العمل.
استغرق التحويل إلى نموذج قائم على الأحداث عدة سنوات. فكككت wait_for_inferior عام 1999، وأدخلت بنية حالة للتحكّم بالتنفيذ لتحلّ محلّ كومة المتغيّرات المحلية والعالمية، وحوّلت تشابك القفزات إلى دوالّ أصغر مستقلّة. وفي الوقت نفسه أدخلت Elena Zannoni وغيرها طوابير أحداث تضمّ كلًّا من مُدخلات المستخدم وإشعارات العملية المُجرَّبة.
بروتوكول الاتصال عن بُعد
رغم أن معمارية متجهات الهدف في GDB تتيح مجموعة واسعة جدًا من طرائق التحكّم في برنامج يعمل على حاسوب آخر، فإننا نستخدم بروتوكولاً واحدًا مفضَّلًا. وهو لا يحمل اسمًا مميِّزًا، ويُشار إليه عادةً مجرّدًا بـ «بروتوكول الاتصال عن بُعد»، أو «بروتوكول GDB عن بُعد»، أو «بروتوكول الاتصال التسلسلي عن بُعد» (ويُختصر «RSP»)، أو «بروتوكول remote.c» (نسبةً إلى ملف الشيفرة المصدرية الذي ينفّذه)، وأحيانًا «بروتوكول stub» في إشارة إلى تطبيق الهدف للبروتوكول.
البروتوكول الأساسي بسيط، وهو يعكس الرغبة في جعله يعمل على الأنظمة المُدمجة الصغيرة في الثمانينيات، وكانت ذاكراتها تُقاس بالكيلوبايت. فمثلًا، تطلب حزمة البروتوكول $g كل السجلّات، وتتوقّع ردًّا يتكوّن من كل بايتات كل السجلّات مجتمعة—مع الافتراض بأن عددها وحجمها وترتيبها ستطابق ما يعرفه GDB عنها.
يتوقّع البروتوكول ردًّا واحدًا على كل حزمة مُرسَلة، ويفترض أن الاتصال موثوق، فلا يضيف إلى الحزم المُرسَلة إلا مجموع التحقّق (checksum) (فـ $g تُرسَل فعليًا بوصفها $g#67 عبر الوصلة).
ولرغم أن أنواع الحزم المطلوبة ليست سوى حفنة (تقابل نحو اثني عشر من طرائق متجه الهدف الأكثر أهمية)، فقد أُضيفت على مرّ السنين عشرات من الحزم الاختيارية الإضافية، لدعم كل شيء من نقاط التوقف العتادية، إلى نقاط التتبّع، إلى المكتبات المشتركة.
وعلى الهدف نفسه، قد يأتي تطبيق بروتوكول الاتصال عن بُعد بأشكال متنوعة للغاية. فالبروتوكول موثَّق بالكامل في دليل GDB، ما يعني أنه من الممكن كتابة تطبيق لا يرهقه ترخيص GNU، وقد أدمج فعلًا كثير من مصنّعي المعدّات شيفرة تتحدّث بروتوكول GDB عن بُعد، في المختبر وفي الميدان. ونظام IOS من Cisco، الذي يشغّل كثيرًا من معدّات شبكاتهم، مثال معروف جيدًا.
غالبًا ما تُشار إلى تطبيق الهدف للبروتوكول بـ «كعب التنقيح» (debugging stub)، أو مجرّد «كعب» (stub)، بما يوحي بأنه لا يُتوقّع منه أن يبذل جهدًا كبيرًا منفردًا. وتضمّ شيفرة GDB بضعة أمثلة من هذه الكعاب، وهي عادةً نحو 1,000 سطر من C منخفض المستوى. وعلى لوحة خالية تمامًا من نظام تشغيل، يتعيّن على الكعب أن يثبّت معالجه الخاصة لاستثناءات العتاد، والأهم منها التقاط تعليمات الفخّ. وسيحتاج أيضًا إلى شيفرة تشغيل تسلسلي إذا كانت وصلة العتاد خطًا تسلسليًا. أمّا معالجة البروتوكول الفعلية فبسيطة، إذ إن جميع الحزم المطلوبة محارف مفردة يمكن فكّ ترميزها بعبارة switch.
وتتمثل مقاربة أخرى لبروتوكول الاتصال عن بُعد في بناء «sprite» يعمل كوسيط بين GDB وعتاد التنقيح المتخصّص، بما في ذلك أجهزة JTAG و«wigglers» وما شابه. وكثيرًا ما يكون لدى هذه الأجهزة مكتبة يجب أن تعمل على الحاسوب الموصول فعليًا بلوحة الهدف، وغالبًا ما لا تكون واجهة برمجيات هذه المكتبة متوافقة معماريًا مع باطن GDB. ولهذا، رغم أن بعض إعدادات GDB استدعت مكتبات التحكّم بالعتاد مباشرةً، فقد ثبت أن تشغيل الـ sprite بوصفه برنامجًا مستقلًّا يفهم بروتوكول الاتصال عن بُعد ويترجم الحزم إلى استدعاءات مكتبة الجهاز هو نهج أبسط.
GDBserver
تتضمّن شيفرة GDB فعلًا تطبيقًا واحدًا كاملًا وعاملًا لجانب الهدف في بروتوكول الاتصال عن بُعد: GDBserver. وGDBserver برنامج أصلي (native) يعمل تحت نظام تشغيل الهدف، ويتحكّم في برامج أخرى على نظام الهدف باستخدام دعمه للتنقيح الأصلي، استجابةً لحزم يتلقّاها عبر بروتوكول الاتصال عن بُعد. وبعبارة أخرى، فإنّه يعمل نوعًا من الوسيط للتنقيح الأصلي.
لا يفعل GDBserver أي شيء لا يستطيع GDB الأصلي فعله؛ فإذا كان نظام هدفك قادرًا على تشغيل GDBserver، نظريًا فهو قادر أيضًا على تشغيل GDB. غير أن GDBserver أصغر بعشرة أضعاف ولا يحتاج إلى إدارة جداول الرموز، لذا فهو مريح جدًا لاستخدامات GNU/Linux المُدمجة وما شابه.
الشكل 4.2: GDBserver
يتشارك GDB وGDBserver قدرًا من الشيفرة، غير أن تغليف التحكّم بالعمليات الخاص بنظام التشغيل في كيان مستقلّ هو فكرة بديهية، غير أن فصل الاعتماديات الضمنية في GDB الأصلي ينطوي على صعوبات عملية، وقد تماطلت عملية التحويل ببطء.
4.9. واجهات GDB
GDB في جوهره منقِّح أخطاء يعمل على سطر الأوامر. وقد جرّب الناس على مرّ الوقت مخططات مختلفة لجعله منقِّحًا رسوميًا بنوافذ، لكن، وعلى رغم الوقت والجهد المبذولين، لم يحظَ أي منها بقبول عام.
واجهة سطر الأوامر
تستخدم واجهة سطر الأوامر مكتبة GNU القياسية readline للتعامل مع التفاعل مع المستخدم محرفًا بمحرف. وتتولّى Readline أمورًا مثل تحرير السطر وإكمال الأوامر؛ فبإمكان المستخدم مثلًا استخدام مفاتيح المؤشّر للعودة إلى الوراء في السطر وتصحيح محرف.
ثم يأخذ GDB الأمر الذي أعادته readline ويبحث عنه باستخدام بنية متتالية من جداول الأوامر، حيث تختار كل كلمة تالية من الأمر جدولًا إضافيًّا. فمثلًا، set print elements 80 ينطوي على ثلاثة جداول؛ الأول هو جدول جميع الأوامر، والثاني هو جدول الخيارات التي يمكن set، والثالث هو جدول خيارات طباعة القيم، ومنها elements هو الخيار الذي يحدّد عدد الكائنات المطبوعة من تجميع (aggregate) مثل نص أو مصفوفة. وبعد أن تستدعي الجداول المتتالية دالةَ معالجة أمر فعلية، تتولّى هذه الدالة التحكّم، ويصبح تحليل الوسائط رهين الدالة تمامًا. فبعض الأوامر، مثل run، تتعامل مع وسائطها بطريقة مشابهة لمعايير C التقليدية argc/argv، بينما يفترض آخرون، مثل print، أن ما تبقّى من السطر تعبير واحد بلغة برمجة، ويسلّمون السطر بأكمله إلى محلِّل خاص بتلك اللغة.
واجهة الآلة
إحدى طرق توفير واجهة رسومية للتنقيح هي استخدام GDB بوصفه نوعًا من «الواجهة الخلفية» (backend) لبرنامج واجهة رسومي، يترجم نقرات الفأرة إلى أوامر ويصيّر نتائج الطباعة في نوافذ. وقد نجح هذا عدة مرات، بما في ذلك KDbg وDDD (منقِّح عرض البيانات)، لكنه ليس المقاربة المثلى لأن النتائج أحيانًا تُصيَّر لتكون مقروءة للإنسان، فتُحذف التفاصيل ويعتمد الأمر على قدرة الإنسان على توفير السياق.
ولحلّ هذه المشكلة، يملك GDB واجهة «مستخدم» بديلة، تُعرف باسم واجهة الآلة (Machine Interface)، أو MI اختصارًا. وما زالت في جوهرها واجهة سطر أوامر، لكن الأوامر والنتائج كلاهما يحمل صياغة إضافية تجعل كل شيء صريحًا—فكل وِسيط محاط بعلامات اقتباس، والمخرجات المركّبة تحمل محدِّدات للمجموعات الفرعية وأسماء وسائط لأجزاء المكوّنات. وإضافةً إلى ذلك، يمكن أن تُسبق أوامر MI بمعرّفات تسلسل تُصدَر في النتائج، ما يضمن تطابق النتائج المُبلَّغة مع الأوامر الصحيحة.
ولكي ترى كيف يقارن الصيغتان، إليك أمر step عادي وردّ GDB:
(gdb) step
buggy_function (arg1=45, arg2=92) at ex.c:232
232 result = positive_variable * arg1 + arg2;
ومع MI، يصبح الإدخال والإخراج أكثر إسهابًا، لكنه أسهل على البرمجيات الأخرى في تحليله بدقّة:
4321-exec-step
4321^done,reason="end-stepping-range",
frame={addr="0x00000000004004be",
func="buggy_function",
args=[{name="arg1",value="45"},
{name="arg2",value="92"}],
file="ex.c",
fullname="/home/sshebs/ex.c",
line="232"}
بيئة التطوير Eclipseecl12 هي أبرز عميل لـ MI.
واجهات مستخدم أخرى
تشمل الواجهات الأمامية الإضافية نسخة قائمة على tcl/tk تُدعى GDBtk أو Insight، وواجهة قائمة على curses تُدعى TUI، وهي التي تبرع بها Hewlett-Packard في الأصل. وGDBtk واجهة رسومية تقليدية متعددة الألواح مبنية باستخدام مكتبة tk، في حين أن TUI واجهة مقسّمة الشاشة (split-screen).
4.10. عملية التطوير
المشرفون
بصفته برنامج GNU أصلي، بدأ تطوير GDB متّبعًا «نموذج الكاتدرائية» (cathedral model) في التطوير. فبعد أن كتبه Stallman في الأصل، مرّ GDB بسلسلة من «المشرفين» (maintainers)، كان كل واحد منهم مزيجًا من المعماري ومراجع الرقع ومدير الإصدار، مع حصر الوصول إلى مستودع الشيفرة المصدرية في أيدي عدد قليل من موظفي Cygnus.
وفي عام 1999، انتقل GDB إلى مستودع شيفرة مصدرية عام وتوسّع إلى فريق من عدة عشرات من المشرفين، بمعونة عشرات الأشخاص ذوي صلاحية الإيداع (commit). وقد سرّع هذا التطوير بدرجة كبيرة، إذ انتقلت الحصيلة الأسبوعية من نحو 10 عمليات إيداع إلى 100 أو أكثر.
الاختبار مرّتين
ولأن GDB شديد التخصّص في الأنظمة، ولديه عدد كبير من عمليات النقل إلى أنظمة تمتدّ من الأصغر إلى أكبر الأنظمة في عالم الحوسبة، ولديه مئات الأوامر والخيارات وأنماط الاستخدام، فإن من الصعب حتى لمختبِر GDB ذي الخبرة أن يتنبّأ بجميع آثار التغيير.
هنا تحديدًا يأتي دور مجموعة الاختبارات. وتتألف مجموعة الاختبارات من عدد من برامج الاختبار مقترنة بسكربتات expect، باستخدام إطار اختبار قائم على tcl يُدعى DejaGNU. والنموذج الأساسي أن كل سكربت يقود GDB أثناء تنقيحه لبرنامج اختبار، فيرسل أوامر ثم يطابق المخرجات مع تعبيرات نمطية (regular expressions).
كما تمتلك مجموعة الاختبارات القدرة على إجراء تنقيح متقاطع على العتاد الحيّ وعلى المحاكيات، وعلى إدراج اختبارات خاصة ببنية معمارية أو إعداد بعينه.
وفي نهاية عام 2011، تضمّ مجموعة الاختبارات نحو 18,000 حالة اختبار، تشمل اختبارات الوظائف الأساسية، واختبارات خاصة بكل لغة، واختبارات خاصة بكل بنية معمارية، واختبارات MI. معظمها عامّ ويُنفَّذ مع أي إعداد. ويُتوقّع من المساهمين في GDB تشغيل مجموعة الاختبارات على الشيفرة بعد التصحيح (patched sources) وعدم ملاحظة أي انحدار (regressions)، ويُتوقّع أن ترافق كل ميزة جديدة اختبارات جديدة. غير أنه بما أنه لا يتوفّر لأحد الوصول إلى كل المنصات التي قد يتأثّر بها تغيير، نادرًا ما نصل إلى صفر إخفاقات؛ فبين 10 و20 إخفاقًا هو رقم معقول عادةً للنسخة الجذعية (trunk snapshot) المهيّأة للتنقيح الأصلي، وستكون لدى بعض الأهداف المُدمجة إخفاقات أكثر.
4.11. الدروس المستفادة
التطوير المفتوح يفوز
بدأ GDB بوصفه نموذجًا مثاليًا لعملية التطوير «الكاتدرائية» التي يحتفظ فيها المشرف بسيطرة وثيقة على الشيفرة المصدرية، ولا يرى العالم الخارجي إلّا التقدّم عبر لقطات دورية. وقد ذُرِّع ذلك ببرِّ ندرة نسبيّة في تقديم الرقع، غير أن العملية المغلقة كانت في الواقع تُثبّط الرقع. ومنذ تبنّي العملية المفتوحة، صار عدد الرقع أكبر بكثير مما كان في أي وقت مضى، والجودة لا تقلّ عن السابق بل تفوقه.
ضع خطة، لكن توقّع أن تتغير
عملية تطوير البرمجيات مفتوحة المصدر فوضوية بطبيعتها إلى حدٍّ ما، إذ يعمل أفراد مختلفون على الشيفرة مدّةً من الوقت، ثم ينصرفون تاركين الأمر لغيرهم ليواصلوا.
غير أنه يبقى من المفيد إعداد خطة تطوير ونشرها. فهي تساعد المطوّرين في توجيه عملهم أثناء انشغالهم بالمهام المرتبطة بها، ويمكن عرضها على الجهات المحتملة التمويل، وتتيح للمتطوّعين التفكير فيما يمكنهم فعله لخدمة الخطة.
لكن لا تحاول فرض التواريخ أو الأطر الزمنية؛ فحتى لو تفوّق الجميع حول اتجاه ما، من غير المرجّح أن يستطيع الناس ضمان الجهد بدوام كامل بما يكفي للانتهاء في تاريخ مختار.
ولنفس السبب، لا تتمسّك بالخطة نفسها إن قديمت. فلفترة طويلة كان لدى GDB خطة لإعادة هيكلته في صورة مكتبة، libgdb، ذات واجهة برمجية محدّدة المعالم، يمكن ربطها ببرامج أخرى (وخاصة ذات واجهات رسومية)؛ بل غُيِّرت عملية البناء نفسها لبناء libgdb.a كخطوة وسيطة. وبرغم أن الفكرة تكرّرت دوريًا منذ ذلك الحين، فإن هيمنة Eclipse وMI أدّت إلى تجاوز المبرّر الرئيسي لوجود المكتبة، وحلول يناير 2012 تخلّينا عن مفهوم المكتبة ونعمل الآن على إزالة أجزاء الشيفرة التي فقدت معناها.
كان كل شيء رائعًا لو كنا ذوي ذكاء لا نهائي
بعد أن رأيت بعض التغييرات التي أجريناها، قد تساءل: لماذا لم نضبط الأمور على نحو صحيح منذ البداية؟ ببساطة، لم نكن ذكاءنا كافيًا.
بكل تأكيد كان من الممكن أن نتنبّأ بأن GDB سيكون رائجًا إلى حدّ كبير، وأن يُنقَل إلى عشرات وعشرات البنى المعمارية، أصولية كانت أم متقاطعة. ولو كنا نعلم ذلك، لكنا بدأنا بكائنات gdbarch، بدلًا من قضاء سنوات في ترقية ماكروهات ومتغيّرات عامة قديمة؛ والأمر نفسه ينطبق على متجه الهدف.
وبكل تأكيد كان من الممكن أن نتنبّأ بأن GDB سيُستخدم مع واجهات رسومية. فبعد كل شيء، في عام 1986 كان كلٌّ من Mac ونظام X Window قد صدرا قبل عامين بالفعل! وبدلًا من تصميم واجهة أوامر تقليدية، كان بإمكاننا تهيئة GDB للتعامل مع الأحداث بشكل لامتزامن.
غير أن الدرس الحقيقي ليس أن مطوّري GDB كانوا بلا ذكاء، بل أننا لم نقدّر — مهما بلغ ذكاؤنا — التنبّؤ بكيفية تطوّر GDB. ففي عام 1986 لم يكن جليًّا البتّة أن واجهة النوافذ والفأرة ستصبح منتشرة في كل مكان؛ ولو كانت النسخة الأولى من GDB متوافقة تمامًا مع الاستخدام الرسومي لبدونا عباقرة، لكنه كان ذلك حظًّا محضًا. وبدلًا من ذلك، بجعل GDB مفيدًا ضمن نطاق أكثر محدودية، بنينا قاعدة مستخدمين مكنّتنا من تطوير وإعادة هندسة أوسع لاحقًا.
تعلَّم التعايش مع الانتقالات الناقصة
حاول إتمام الانتقالات، لكنها قد تستغرق وقتًا؛ فتوقّع أن تعيش معها وهي ناقصة.
في قمة GCC عام 2003، أعرب Zack Weinberg عن أسفه إزاء «الانتقالات الناقصة» في GCC، حيث أُدخلت بنية تحتية جديدة لكن لم يكن بالإمكان إزالة البنية التحتية القديمة. ولدى GDB هذه أيضًا، لكن يمكننا أن نذكر عددًا من الانتقالات التي أُتمّمت، مثل متجه الهدف وgdbarch. ومع ذلك، قد تستغرق سنوات لإتمامها، ويجب في أثناء ذلك إبقاء المنقِّح يعمل.
لا تتعلّق بالشيفرة أكثر من اللازم
حين تقضي وقتًا طويلًا مع مجموعة شيفرة واحدة، وكانت برنامجًا مهمًّا يدفع الفواتير أيضًا، من السهل أن تتعلّق بها، وأن تشكيل تفكيرك ليتماشى مع الشيفرة، لا العكس.
لا تفعل ذلك.
كل ما في الشيفرة نتاج سلسلة من القرارات الواعية: بعضها مُلهم وبعضها أقلّ من ذلك. فالحيلة البليدة لتوفير المساحة التي اعتُمدت عام 1991 صارت تعقيدًا بلا جدوى في ظل الذواكر التي تتجاوز الغيغابايت عام 2011.
دعم GDB ذات يوم للحاسوب الخارق Gould. وعندما أطفأوا آخر آلة من نوعها، نحو عام 2000، لم يكن هناك أي معنى للاحتفاظ بتلك الأجزاء. وكانت تلك الحادثة بداية عملية تقاعد الميزات داخل GDB، ومعظم الإصدارات صارت تتضمن الآن إخراج قطعة أو أخرى من الخدمة.
وفي الواقع، هناك عدد من التغييرات الجذرية على الطاولة أو جارية بالفعل، تمتدّ من تبنّي Python للكتابة النصّية (scripting)، إلى دعم تنقيح أخطاء الأنظمة متعددة النوى عالية التوازي، إلى إعادة الترميز بـ C++. وقد تستغرق هذه التغييرات سنوات لإتمامها؛ وكلّما طال ذلك كان سببًا أقوى للبدء فيها الآن.