مثال تعليمي لإظهار أن printf في بعض الأحيان قد يخفي الأخطاء

StackOverflow https://stackoverflow.com/questions/3110711

سؤال

أتذكر عندما كنت في مسار البرمجة C ، اقترح المعلم ذات مرة أن أستخدمه printf لمشاهدة تنفيذ البرنامج الذي كنت أحاول تصحيحه. كان لهذا البرنامج خطأ تجزئة مع سبب لا أستطيع تذكره في الوقت الحالي. اتبعت نصيحته واختفت خطأ التجزئة. لحسن الحظ ، أخبرني TA ذكي أن ألتزم بدلاً من استخدامه printfس. في هذه الحالة ، كان من المفيد القيام به.

لذا ، أردت اليوم أن أظهر لشخص يستخدمه printf يمكن أن يخفي خطأ ، لكن لا يمكنني العثور على هذا الكود القديم الذي كان يحتوي على هذا الخطأ الغريب (الميزة؟ هممم).

سؤال: هل واجه أي منكم هذا السلوك أيضًا؟ كيف يمكنني إعادة إنتاج شيء مثل هذا؟

يحرر:

أرى أن سؤالي جزء يطالب رأيي بـ "الاستخدام printf خطأ ". أنا لا أقول ذلك بالضبط ولا أحب أخذ آراء متطرفة ، لذلك أقوم بتحرير السؤال قليلاً. أوافق على ذلك printf هي أداة جيدة ، لكنني أردت فقط إعادة إنشاء حالة حيث printfS جعل خطأ التجزئة يختفي وبالتالي ، إثبات أن المرء يجب أن يكون حذرا.

هل كانت مفيدة؟

المحلول

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

لذلك باستخدام printf البيانات لديها أسباب وجيهة. سواء لتصحيح أو printf يجب أن تقرر على أساس كل حالة على حدة. لاحظ أن الاثنين ليسا حصريين على أي حال - أنت يستطيع رمز التصحيح حتى لو كان يحتوي printf المكالمات :-)

نصائح أخرى

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

يبدو أنك تتعامل مع أ Heisenbug.

لا أعتقد أن هناك أي شيء "خطأ" بطبيعته باستخدام printf كأداة تصحيح الأخطاء. لكن نعم ، مثل أي أداة أخرى ، لها عيوبها ، ونعم كان هناك أكثر من OCCASISION حيث قامت إضافة عبارات printf بإنشاء Heisenbug. ومع ذلك ، فقد ظهرت أيضًا في ظهور Heisenbugs نتيجة لتغييرات تخطيط الذاكرة التي قدمها مصحح الأخطاء ، وفي هذه الحالة أثبتت PrintF أنها لا تقدر بثمن في تتبع الخطوات التي تؤدي إلى الانهيار.

IMHO لا يزال كل مطور يعتمد هنا وهناك على المطبوعات. لقد تعلمنا للتو أن نسميهم "سجلات مفصلة".

أكثر من ذلك ، المشكلة الرئيسية التي رأيتها هي أن الناس يعاملون printfs وكأنهم لا يقهرون. على سبيل المثال ، ليس من النادر في جافا رؤية شيء مثل

System.out.println("The value of z is " + z + " while " + obj.someMethod().someOtherMethod());

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

شيء آخر تفعله المطبوعات هو أنها تقدم التأخير. لقد رأيت الكود مع ظروف السباق في بعض الأحيان "يتم إصلاح" عند تقديم المطبوعات. لن أتفاجأ إذا كانت بعض التعليمات البرمجية تستخدم ذلك.

أتذكر مرة واحدة محاولة تصحيح برنامج على Macintosh (Circa 1991) حيث كان رمز التنظيف الذي تم إنشاؤه في برنامج التحويل البرمجي لإطار مكدس ما بين 32 كيلو و 64 كيلو كيل -سيتم تمديد كمية Bit Bext إلى سجل العناوين في 68000). كان التسلسل مثل:

  copy stack pointer to some register
  push some other registers on stack
  subtract about 40960 from stack pointer
  do some stuff which leaves saved stack-pointer register alone
  add -8192 (signed interpretation of 0xA000) to stack pointer
  pop registers
  reload stack pointer from that other register

كان التأثير الصافي هو أن كل شيء كان على ما يرام إلا أن السجلات المحفوظة كانت تالفة ، وأحمل أحدها ثابتًا (عنوان صفيف عالمي). إذا قام المترجم بتحسين متغير إلى سجل أثناء قسم من الكود ، فإنه يوضح أنه في ملف معلومات التصحيح حتى يتمكن مصحح الأخطاء من إخراجه بشكل صحيح. عندما يتم تحسين ثابت للغاية ، يبدو أن المترجم لا يتضمن مثل هذه المعلومات ، حيث يجب ألا تكون هناك حاجة. لقد تتبعت الأشياء عن طريق القيام بـ "printf" لعنوان الصفيف ، وضبط نقاط التوقف حتى أتمكن من عرض العنوان قبل وبعد printf. أبلغ مصحح الأخطاء بشكل صحيح عن العنوان قبل وبعد printf ، لكن printf أخرجت القيمة الخاطئة ، لذلك قمت بفك الرمز ورأيت أن printf دفعت السجل A3 على المكدس ؛ عرض السجل A3 قبل PrintF أظهر أنه كان له قيمة مختلفة إلى حد ما عن عنوان الصفيف (أظهر printf القيمة A3 التي تم وضعها بالفعل).

لا أعرف كيف كنت سأتتبع ذلك من أي وقت مضى إذا لم أتمكن من استخدام كل من تصحيح الأخطاء والطباعة معًا (أو ، في هذا الشأن ، إذا لم أفهم 68000 رمز تجميع).

تمكنت من القيام بذلك. كنت أقرأ البيانات من ملف مسطح. ذهبت الخوارزمية المعيبة على النحو التالي:

  1. احصل على طول ملف الإدخال بالبايت
  2. تخصيص مجموعة متغيرة طولها من chars لتكون بمثابة مخزن مؤقت
    • الملفات صغيرة ، لذلك لست قلقًا بشأن فائض المكدس ، ولكن ماذا عن ملفات الإدخال ذات الطول الصفري؟ وجه الفتاة!
  3. إرجاع رمز الخطأ إذا كان طول ملف الإدخال هو 0

لقد وجدت أن وظيفتي ستعمل بشكل موثوق على خطأ SEG - ما لم يكن هناك printf في مكان ما في جسم الوظيفة ، وفي هذه الحالة ستعمل تمامًا كما كنت أقصد. كان إصلاح خطأ SEG هو تخصيص طول الملف بالإضافة إلى واحد في الخطوة 2.

كان لدي تجربة مماثلة. ها هي مشكلتي المحددة ، والسبب:

// Makes the first character of a word capital, and the rest small
// (Must be compiled with -std=c99)
void FixCap( char *word )
{
  *word = toupper( *word );
  for( int i=1 ; *(word+i) != '\n' ; ++i )
    *(word+i) = tolower( *(word+i) );
}

المشكلة هي مع حالة الحلقة - لقد استخدمت " n" بدلاً من الحرف الخالي ، " 0". الآن ، لا أعرف بالضبط كيف يعمل PrintF ، لكن من هذه التجربة أظن أنه يستخدم بعض مواقع الذاكرة بعد متغيراتي كمساحة مؤقتة / عمل. إذا كان عبارة PRINTF قد أدى إلى كتابة حرف ' n' في موقع ما بعد تخزين كلامي ، فستتمكن وظيفة FixCap من التوقف عند مرحلة ما. إذا قمت بإزالة printf ، فسيستمر في الحلقات ، وتبحث عن ' n' ولكن لا تجدها أبدًا ، حتى تتسرب.

لذلك في النهاية ، فإن السبب الجذري لمشكلتي هو أنني أحيانًا أكتب " n" عندما أعني " 0". إنه خطأ ارتكبت من قبل ، وربما سأقوم به مرة أخرى. لكنني الآن أعرف أن أبحث عنه.

حسنًا ، ربما يمكنك تعليمه كيفية استخدام GDB أو غيرها من برامج تصحيح الأخطاء؟ أخبره أنه إذا اختفى خطأ جوس بفضل "printf" ، فإنه لم يختفي حقًا ويمكن أن يظهر مرة أخرى. يجب إصلاح الخلل ، وليس تجاهل.

سيعطيك هذا تقسيمًا بحلول 0 عند إزالة خط printf:

int a=10;
int b=0;
float c = 0.0;

int CalculateB()
{
  b=2;
  return b;
}
float CalculateC()
{
  return a*1.0/b;
}
void Process()
{
  printf("%d", CalculateB()); // without this, b remains 0
  c = CalculateC();
}

ماذا ستكون قضية التصحيح؟ طباعة أ char *[] صفيف قبل الاتصال exec() فقط لأرى كيف كان رمزين - أعتقد أن هذا استخدام صحيح جدا printf().

ومع ذلك ، إذا كان التنسيق يتغذى على printf() هو من التكلفة والتعقيد الكافيين التي قد يغير بالفعل تنفيذ البرنامج (السرعة ، في الغالب) ، قد يكون تصحيح الأخطاء هو أفضل طريقة للذهاب. ثم مرة أخرى ، يأتي التصرفات والأمور أيضًا بتكلفة. يمكن للمرء أن يعرض السباقات التي قد لا تظهر في غيابها.

كل هذا يتوقف على ما تكتبه والخطأ الذي تطارده. الأدوات المتوفرة هي مصالح ، printf() (تجميع عمليات تسجيل الدخول إلى printf كذلك) التأكيدات والرائعين.

هل براغي شفرة أفضل من الأنواع الأخرى؟ يعتمد على ما تحتاجه. لاحظ ، أنا لا أقول أن التأكيدات جيدة أو سيئة. إنها مجرد أداة أخرى.

تتمثل إحدى طرق التعامل مع هذا في إعداد نظام من وحدات الماكرو مما يجعل من السهل إيقاف تشغيل printfs مع الاضطرار إلى حذفها في الكود الخاص بك. أنا استخدم شيئًا كهذا:

#define LOGMESSAGE(LEVEL, ...) logging_messagef(LEVEL, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__);

/* Generally speaking, user code should only use these macros.  They
 * are pithy. You can use them like a printf:
 *
 *    DBGMESSAGE("%f%% chance of fnords for the next %d days.", fnordProb, days);
 *
 * You don't need to put newlines in them; the logging functions will
 * do that when appropriate.
 */
#define FATALMESSAGE(...) LOGMESSAGE(LOG_FATAL, __VA_ARGS__);
#define EMERGMESSAGE(...) LOGMESSAGE(LOG_EMERG, __VA_ARGS__);
#define ALERTMESSAGE(...) LOGMESSAGE(LOG_ALERT, __VA_ARGS__);
#define CRITMESSAGE(...) LOGMESSAGE(LOG_CRIT, __VA_ARGS__);
#define ERRMESSAGE(...) LOGMESSAGE(LOG_ERR, __VA_ARGS__);
#define WARNMESSAGE(...) LOGMESSAGE(LOG_WARNING, __VA_ARGS__);
#define NOTICEMESSAGE(...) LOGMESSAGE(LOG_NOTICE, __VA_ARGS__);
#define INFOMESSAGE(...) LOGMESSAGE(LOG_INFO, __VA_ARGS__);
#define DBGMESSAGE(...) LOGMESSAGE(LOG_DEBUG, __VA_ARGS__);
#if defined(PAINFULLY_VERBOSE)
#   define PV_DBGMESSAGE(...) LOGMESSAGE(LOG_DEBUG, __VA_ARGS__);
#else
#   define PV_DBGMESSAGE(...) ((void)0);
#endif

logging_messagef() هل وظيفة محددة في منفصلة .c ملف. استخدم وحدات الماكرو XMessage (...) في الكود الخاص بك اعتمادًا على الغرض من الرسالة. أفضل شيء في هذا الإعداد هو أنه يعمل لتصحيح الأخطاء والتسجيل في نفس الوقت ، و logging_messagef() يمكن تغيير الوظيفة للقيام بعدة أشياء مختلفة (printf إلى STDERR ، إلى ملف السجل ، واستخدام syslog أو بعض منشأة تسجيل النظام الأخرى ، وما إلى ذلك) ، ويمكن تجاهل الرسائل الموجودة أسفل مستوى معين في logging_messagef() عندما لا تحتاجهم. PV_DBGMESSAGE() هو بالنسبة لتلك رسائل الأخطاء الوفيرة التي تريد بالتأكيد إيقاف تشغيلها في الإنتاج.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top