الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

تتبع الأخطاء و اصلاحها

بدأه C++er في 29 يونيو 2010 · 6 رد · 1,364 مشاهدة · في لغة C و ++C
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

عندما اتطلع لأكواد مكتبات مثل QT او Wild Magic أو Wine او ReactOS و غيره و غيره أجد دائما مبرمجى هذه المكتبات اعتمدوا نظام معين لتتبع الأخطاء و تحليلها لذا بعد دراسة العديد من هذه الأنظمه قررت أن اصنع واحد خاص بى، لذا قبل أن أبدأ بشرح الكيفيه التى أعتمدتها داخل مشاريعى لنعرف اولا كيف يتم تتبع هذه الأخطاء.

1) ما المقصود بتتبع الأخطأ؟

تتبع الأخطأ هو كتابة كود أضافى يهدف إلى التحقق من نتائج بعض الأكواد التى نكتبها فإن كانت نتائجها غير ما نتوقع المفترض ان تظهر لنا رساله تفيد بذلك، فمثلا أنت تقوم بعمل داله المفترض أن تقرأ اول 10 بايت من ملف لذا سيكون بها معامل واحد على الأقل نوعه std::string و أسمه path، المعامل path سيتم تمرير له مسار الملف الذى سنقرأ منه لذا المفترض فى الوضع الطبيعى إذا كان path فارغ فسنقوم بإظهار رسالة خطأ

لاحظ أن الكود الإضافى هذا سيكون داخل النسخ الخاصه بالـ Debug فقط و عند تحويل البرنامج للنسخه الـ Release سيتم حذف جميع هذه الأكواد.

2) و ما هى انواع تتبع الأخطأ؟

  • Assertion: هذا النوع من اساليب تتبع الأخطأ يستخدم تبعا لشرط معين فإن كان الشرط True فسيتوقف عمل البرنامج عند هذا المكان و إن كان False فلن يتوقف عمل البرنامج، فمثلا فى مثال مسار الملف السابق ذكره إذا كان المسار خالى أى path.empty() == true ففى هذه الحاله الشرط تحقق و لابد من ان يظهر خطأ، اما إذا لم يتحقق الشرط فالمفترض ان يكمل البرنامج عمله دون مشاكل.
  • Trace: هذا النوع من اساليب تتبع الأخطأ يستخدم لطباعة رسالة معينه دون أن يتم التحقق من اى شئ فقط يطبع رسائل نصيه.

3) أين تتم طباعة مخرجات الـ Assertion او الـ Trace؟

سؤال جيد و سيحتاج منا ان نتكلم عن ما يسمى بـ Standard Streams. كل برنامج تقوم بعمله يرتبط به بشكل افتراضى 3 ملفات، هذه الملفات يتم معالجتها من قبل نظام التشغيل و لا دخل لك بها أكثر من امكانية ان تقوم بالكتابة او القراءة منها و يتم حذفها بعد انتهاء عمل برنامجك.

هذه الـ Streams هى:

  • Input Stream: هذا الملف يتم الإحتفاظ فيه بأى مدخلات لبرنامجك مثلا عندما يعمل برنامجك على شاشة الـ Console حينها اى بيانات يدخلها المستخدم يتم حفظها داخل هذه الـ Stream.
  • Output Stream: هذا الملف يتم الإحتفاظ فيه بأى مخرجات من برنامجك و هى الإفتراضيه فمثلا عندما تقوم بالطباعه على شاشة الكونسول فأنت فعليا تكتب فى هذا الملف و شاشة الكونسول تطبع ما هو موجود فيه.
  • Error Stream: هى تشبه تماما الـ Output Stream و تستخدم للإحتفاظ بمخرجات برنامجك فقط إن طلبت من برنامجك ذلك و من اسمها سيتم اعتبار ما تحتويه من رسائل عبارة عن رسائل خطأ.

قد تقول ما الفرق بين Stream و ملف؟ فعليا الملف هو البيانات مخزنه داخل القرص الصلب و الـ Stream هى قناة بين البيانات الموجوده بالقرص الصلب و برنامجك.

موضوع الـ Standard Streams كبير و لا يسعه موضوع واحد للكتابه فيه و لكن يمكن ان اعطيك طرف الخيط و هو تعلم مكتبة IO Streams.

نعود الأن لنقطة طباعة المخرجات، عندما تقوم بإستخدام اسلوب الـ Trace لإظهار رساله حينها انت تريد اظهار رسالة عاديه لذا سنستخدم printf (و يمكنك استخدام cout إن اردت) لكتابة الرسالة التى يريدها المبرمج داخل Output Stream، أما إذا تم استخدام إسلوب Assertion حينها سنقوم بإستخدام الداله fprintf (و يمكنك استخدام cerr) للكتابة داخل Error Stream.

نقطه أخرى إذا كان برنامجك من نوع Console Application فسيتم عمل redirect للبيانات التى ستكتب داخل الـ Output, Error Streams إلى شاشة الـ Console و إذا كان برنامجك غير ذلك فالـ Debugger الذى تستخدمه بشكل تلقائى يقوم بربط الـ Output, Error Streams بشاشة لديه فى الأغلب تسمى Output Window

4) جيد و لكن حتى الأن لم اعرف كيف سأقوم بإيقاف تنفيذ البرنامج عندما استخدم Assertion؟

حقيقة هذه هى اهم نقطة فى عمل دوال الـ Assertion و هى ايقاف تنفيذ عمل البرنامج عند حدوث فشل للـ Assertion لذا ركز معى جيدا فيما يلى:

توجد وسيلة واحده فقط يمكنك من خلالها ايقاف تنفيذ عمل البرنامج و هى من خلال السسمبلى و بالتحديد عن طريق التعليمه int 3 داخل الأنظمة التى تعمل على معالجات Intel و AMD و VIA و عن طريق التعليمه trap داخل الأنظمة التى تعمل على معالجات MIPS و PPC و SPARC و ARM، جرب الكود التالى لترى ما اعنى:

- لمعالجات Intel و AMD و VIA

  • مترجم GCC
    int main()
    {
      asm("int 3");
    }


  • مترجم Intel او VC
    int main()
    {
        __asm { int 3 }
    }


- لمعالجات MIPS و PPC و SPARC و ARM مع مترجم GCC

int main()
{
  asm("trap");
}

عند تنفيذ أى من الأكواد السابقه ستجد ان الـ Debugger قد توقف عن العمل عن السطر الذى كتب به int3 او trap.

5) حسنا، جمعنا كل اجزاء حلول اللغز ماذا ينقص حتى نبدأ فى تركيبها سويا؟

حتى الأن انت لديك الأفكار و كيفية تنفيذها و لكن بقى خطوة واحده و هى كيف سيقوم برنامجك بتطبيق هذه الأفكار و ماذا تحتاج لتطبيقها؟

بالنسبه للـ Assertion : عندما يحدث ان تطبع رسالة الـ Assertion فلابد ان تحدد رقم السطر الذى حدث به الخطأ و ايضا اسم الملف و اسم الداله و الكود الذى سبب الخطأ، الرساله نفسها ستأخذ الشكل التالى:

Assertion Faild for "code_block" in file "file_name" in function "function_name" at line "line_number".

كل الكلمات التى كتبت بين علامتى تنصيص سيتم حذفهم و وضع ما يقابل كل منهم فى المكان المناسب له.

بالنسبه للـ Trace : سيوجد لدينا عدة نسخ منها فمثلا سيوجد لدينا نسخه تطبع رسالة مجرده و اخرى تطبع رسالة بناءا على شرط و اخرى تطبع رسالة و يمكن عمل format لهذه الرساله و اخرى تطبع رسالة بناءا على شرط و يمكن عمل format لهذه الرساله.

كل السابقين سيتم تضمينهم كدوال ثم سيتم عمل ماكرو ليتم استخدامهم لاستدعاء تلك الدوال و ستعرف لماذا لاحقا، سنقوم الأن بتضمين الدوال لما يعمل مع المترجم Intel و VC (و هذا لأننا سنكتب كود الأسمبلى الذى يختلف فى صيغته مع ما يكتب داخل الـ GCC):

- الداله my_assert

#include <stdio.h>

void my_assert(bool val, const char* exp, const char* file_name,
                   const char* function_name, int line_number)
{
	if (!val)
	{
		fprintf(stderr, "assertion failed for \"%s\" in file \"%s\" in function \"%s\" at line \"%i\"\n", exp, file_name, function_name, line_number);
		__asm { int 3 }
	}
}

الداله السابقه تطلب منك عدة معاملات تبدأ بـشرط منطقى يليه الكود كنص يليه اسم الملف يليه اسم الداله يليه رقم السطر الذى حدث الخطأ به، قد تقول و من سيدخل كل تلك المعاملات و سأقول لك اصبر قليلا و ستعرف.

- الداله my_trace

#include <stdio.h>

void my_trace(const char* message)
{ printf(message); }

- الداله my_trace_if

void my_trace_if(bool val, const char* message)
{ if (val) printf(message); }

إذا كان الشرط val بـ true سيتم كتابة الرساله message داخل output stream.

- الداله my_trace_format

#include <stdio.h>
#include <stdarg.h>

- الداله my_trace_format
void my_trace_format(const char* message, ...)
{
	va_list arg;
	va_start(arg, message);
	vprintf(message, arg);
	va_end(arg);
}

بدءا من المعامل الثانى و حتى الأخير انا لا اعرفهم و بالتالى لا استطيع تمريرهم للداله printf لذا فأنا استخدم الداله vprintf و التى بدلا من تأخذ معاملات عديده تأخذ النص المراد عمل format له كمعامل اول و المعامل الثانى هو موقع اول معامل لهذه الداله داخل الذاكره، يبدو انك تهت منى إذا الأفضل ان تقرا الشرح الخاص بها داخل موقع cplusplus.

- الداله my_trace_format_if

#include <stdio.h>
#include <stdarg.h>

void my_trace_format_if(bool val, const char* message, ...)
{
	if (val)
	{
		va_list arg;
		va_start(arg, message);
		vprintf(message, arg);
		va_end(arg);
	}
}

هذه الداله مثل السابقه و لكنها تطبع النص فقط إذا كان الشرط صحيح.

الدوال انتهت بقى أخر امر و هو الماكرو، انظر معى الكود التالى:

#ifdef NDEBUG
	#define MY_ASSERT(exp)
	#define MY_TRACE(msg)
	#define MY_TRACE_IF(exp, msg)
	#define MY_TRACE_FORMAT(msg, ...)
	#define MY_TRACE_FORMAT_IF(exp, msg, ...)
#else
	#define MY_ASSERT(exp) my_assert(exp, #exp, __FILE__, __FUNCTION__, __LINE__);
	#define MY_TRACE(msg)  my_trace(msg);
	#define MY_TRACE_IF(exp, msg)     my_trace_if(exp, msg);
	#define MY_TRACE_FORMAT(msg, ...) my_trace_format(msg, __VA_ARGS__);
	#define MY_TRACE_FORMAT_IF(exp, msg, ...)  my_trace_format_if(exp, msg, __VA_ARGS__);
#endif

الماكرو NDEBUG يتم تعريفه فى حالة ان البرنامج فى وضع Release و حينها المفترض ان الأكواد الخاصه بالـ Debug ألا يتم وضعها داخل البرنامج لذا ستجد أن الماكرو لا يشير لشى و ذلك على العكس مع إذا كان NDEBUG غير معرف.

فى حالة الماكرو الذى يستقبل عدة معاملات يتم الحصول على هذه المعاملات بإستخدام الماكرو __VA_ARGS__.

بهذا نكون قد انتهينا من صنع نسختنا الخاصه من دوال التنقيح و ستد فى المرفقات النسخه التى صنعنها هنا.

my_debug.rar

و الله ولي التوفيق

تم تعديل هذه المشاركة بواسطة محمد علاء الدين في 29 يونيو 2010 في 23:24

3

مدونتي: C++ Tips and Tricks

#2

جزاك الله أخي محمد, موضوع جميل و مفيد جدا

لي ملاحظتان:

1- أليس من المفروض التحقق من مدخلات الدالة سؤاء كنا في ال Debug أو ال Release؟ يعني ماذا سيحدث إذا إستخدم أحد ما (من فريق التطوير) هذه الدالة و قام بإرسال معاملات خاطئه؟

2- هل يوجد مكتبة logging في ال C كما لدينا مثلا في الجافا؟ فنحن نتعامل مع مثل هذه المواقف عن طريق مكاتب ال Logging مثل log4j

و ال Logging درجات (trace, info, debug, warning, error, etc) , في خلال الكود أقوم بعمل log للبيان على حسب درجته, و في وقت التشغيل, عن طريق ملف متغيرات خارجيه (ملف configuration , متغير نظام أو غيره) نقوم بإخبار البرنامج بعمل Logging على مستوي معين و بالتالي فإن ما نريده أن يخرج لنا في مخرجات ال logger (ملف , ال std أو غيره) يكون بناء على إختيارنا.

ما رأيك يا محمد أن نقوم ببناء log4c :D

شكرا لك مره أخر, جزاك الله خيرا.

تعديل: لقد وجدت بالفعل مكتبة log4c mad.gif إذا علينا كتابة commons-logging للC :D

تم تعديل هذه المشاركة بواسطة هويدي في 30 يونيو 2010 في 16:09

#3
اقتباس

1- أليس من المفروض التحقق من مدخلات الدالة سؤاء كنا في ال Debug أو ال Release؟ يعني ماذا سيحدث إذا إستخدم أحد ما (من فريق التطوير) هذه الدالة و قام بإرسال معاملات خاطئه؟

نعم هذا صحيح لابد من التحقق من المدخلات، و لكن اسلوب التحقق نفسه يختلف فى وضع ال Debug عنه فى ال Release، انظر معى الكود التالى:

bool open_file (string path, fstream& file);
{
    #ifndef NDEBUG
        MY_ASSERT(path.empty())
    #else
        if (path.emtpy())
            throw new exception("path is empty.");
    #endif
}

المثال السابق يعرض الفكره التى توضح الإختلاف بين كود وضع الـ Debug و الـ Release.

اقتباس

2- هل يوجد مكتبة logging في ال C كما لدينا مثلا في الجافا؟ فنحن نتعامل مع مثل هذه المواقف عن طريق مكاتب ال Logging مثل log4j

و ال Logging درجات (trace, info, debug, warning, error, etc) , في خلال الكود أقوم بعمل log للبيان على حسب درجته, و في وقت التشغيل, عن طريق ملف متغيرات خارجيه (ملف configuration , متغير نظام أو غيره) نقوم بإخبار البرنامج بعمل Logging على مستوي معين و بالتالي فإن ما نريده أن يخرج لنا في مخرجات ال logger (ملف , ال std أو غيره) يكون بناء على إختيارنا.

داخل الـ C و الـ C++ متاح لك الأنواع debug و trace و error اما warning و info فغير متاحين، بالنسبه لـ debug و trace يمكنك الحصول عليهم من assert.h - الداله assert و stdio.h - الداله printf و بالنسبه لـ error فتستخدم #error و لا يوجد إا.%Dل لها.

اقتباس
تعديل: لقد وجدت بالفعل مكتبة log4c Posted Image إذا علينا كتابة commons-logging للC :D

مشروع تانى :ohmy: لا يعم انا مشغول جدا الأيام دى و كمان انا مشترك فى عندى كام مشروع انا شغال فيهم و لسه مخلصوش، ان شاء الله ممكن السنه القادمه لو كنا احياء :) .

و الله ولى التوفيق

مدونتي: C++ Tips and Tricks

#4

شكرا يا أخي على الرد, جزاك الله خيرا.

لكن تعتقد هل هذo للدالة:

bool open_file (string path, fstream& file);
{
	#ifndef NDEBUG
    	MY_ASSERT(path.empty())
	#else
    	if (path.emtpy())
        	throw new exception("path is empty.");
	#endif
}

تعطي إنطباع جيد لدى قارئيها, و ماذا لو جعلناها تبدو هكذا (في ال Debug , ال release ) :

bool open_file (string path, fstream& file);
{
	if (path.emtpy())
		throw new exception("path is empty.");
}
#5

انت تتكلم على اساس أن الداله سيتم استدعائها من المستخدم مباشرة لذا من المفترض رمى الإستثناء و لا حاجة لنا بكود الـ Debug، و أنا اتكلم على اساس ان الداله من الممكن أن تكون ضمن سلسلة من الإستدعائات و قد يكون المسار يتم تجميعه بشكل ديناميكى من خلال الدوال التى تستدعيها لذا فأنا اكتب هذا الكود حتى استطيع ان اعرف اذا تم تمرير مسار فارغ و ذلك حتى اصلح الخطأ من الدوال الأخرى و ليس فى هذه الداله.

و الله ولى التوفيق

مدونتي: C++ Tips and Tricks

#6

جزاك الله خيرا على التوضيح!

يبدو أن فكرة ال Debug وال Release ليست واضحة عندي بنسبة 100%

شكرا لك.

#7

ما شاء الله يا ابو حميد .. موضوع رائع من مشرف أروع :wub:

بصراحة .. موضوع في قمة الروعة.

جعله الله في موازين حسناتك :)

مواضيع مشابهة