تريد أن تلعب المخبر؟ العثور على علة في وظيفة من قائد منتصف الليل

علة

في هذه المقالة ، ندعوك لمحاولة العثور على خطأ في وظيفة بسيطة جدًا من مشروع GNU Midnight Commander. لماذا؟ بدون سبب معين. للمتعة فقط. حسنا ، حسنا ، إنها كذبة. لقد أردنا في الواقع أن نظهر لك خطأ آخر يواجهه المراجع البشري في صعوبة في العثور عليه ويمكن لمحلل الشفرات الثابتة PVS-Studio أن يصاب به دون جهد.

أرسل لنا مستخدم بريدًا إلكترونيًا في اليوم الآخر ، يسأل فيه عن سبب تلقيه تحذيرًا بشأن الوظيفة EatWhitespace (انظر الرمز أدناه). هذا السؤال ليس تافها كما قد يبدو. حاول أن تعرف بنفسك ما هو الخطأ في هذا الرمز.

static int EatWhitespace (FILE * InFile) /* ----------------------------------------------------------------------- ** * Scan past whitespace (see ctype(3C)) and return the first non-whitespace * character, or newline, or EOF. * * Input: InFile - Input source. * * Output: The next non-whitespace character in the input stream. * * Notes: Because the config files use a line-oriented grammar, we * explicitly exclude the newline character from the list of * whitespace characters. * - Note that both EOF (-1) and the nul character ('\0') are * considered end-of-file markers. * * ----------------------------------------------------------------------- ** */ { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); } /* EatWhitespace */ 

كما ترون ، EatWhitespace هي وظيفة صغيرة. جسده أصغر من التعليق عليه :). الآن ، دعونا نتحقق من بعض التفاصيل.

إليك وصف الدالة getc :

 int getc ( FILE * stream ); 

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

وإليك وصف وظيفة isspace :

 int isspace( int ch ); 

يتم التحقق مما إذا كان الحرف المحدد عبارة عن حرف مسافة بيضاء كما تم تصنيفه بواسطة لغة C المثبتة حاليًا. في الإعدادات المحلية الافتراضية ، تكون أحرف مسافة بيضاء كما يلي:

  • مساحة (0x20 ، '') ؛
  • خلاصة النموذج (0x0c ، '\ f') ؛
  • سطر التغذية LF (0x0a ، '\ n') ؛
  • حرف الإرجاع CR (0x0d ، '\ r') ؛
  • علامة تبويب أفقية (0x09 ، '\ t') ؛
  • علامة تبويب رأسية (0x0b ، '\ v').

قيمة الإرجاع قيمة غير صفرية إذا كان الحرف حرف مسافة بيضاء ؛ صفر خلاف ذلك.

من المتوقع أن تتخطى وظيفة EatWhitespace جميع أحرف مسافة بيضاء باستثناء سطر السطر '\ n'. ستتوقف الوظيفة أيضًا عن القراءة من الملف عندما تواجه نهاية الملف (EOF).

الآن بعد أن تعرف كل ذلك ، حاول العثور على الخطأ!

سيحرص الشخصان الوحيدان الموجودان أدناه على عدم إلقاء نظرة خاطفة على التعليق.

الشكل 1. الوقت للبحث عن الأخطاء. وحيدات ينتظرون.


الشكل 1. الوقت للبحث عن الأخطاء. وحيدات ينتظرون.

لا يوجد حتى الآن الحظ؟

حسنًا ، كما ترى ، لأننا كذبت علينا بشأن مسألة الإصدار . بوا ها ها ها! إنها ليست وظيفة قياسية على الإطلاق - إنها ماكرو مخصص. نعم ، نحن الاشرار ونحن في حيرة من أمرك.

الشكل 2. القراء يونيكورن مربكة حول isspace.


الشكل 2. القراء يونيكورن مربكة حول isspace .

ليس لنا ولا نحن وحدنا اللوم ، بالطبع. يكمن الخطأ في كل هذا الارتباك في مؤلفي مشروع غنو ميدنايت كوماندر ، الذين قاموا بتنفيذ تطبيقهم الخاص للإصدار في ملف charset.h:

 #ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t') 

مع هذا الماكرو ، أربك المؤلفون المطورين الآخرين. تمت كتابة التعليمات البرمجية وفقًا لافتراض أن isspace هي وظيفة قياسية ، والتي تعتبر إرجاع حرف (0x0d ، '\ r') حرف مسافة بيضاء.

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

 for (c = getc (InFile); ((c)==' ' || (c) == '\t') && ('\n' != c); c = getc (InFile)) 

تعبير ('\ n'! = C) الفرعي ليس ضروريًا (لا لزوم له) لأنه سيتم تقييمه دائمًا إلى صواب. هذا ما يحذرك PVS-Studio من خلال إخراج التحذير:

V560 جزء من التعبير الشرطي صحيح دائمًا: ('\ n'! = C). params.c 136.

لتوضيح ذلك ، دعنا نتفحص 3 نتائج محتملة:

  • تم الوصول إلى نهاية الملف. EOF ليس حرف مسافة أو علامة تبويب. لا يتم تقييم التعبير الفرعي ('\ n'! = C) بسبب تقييم الدائرة القصيرة . تنتهي الحلقة.
  • لقد قرأت الوظيفة بعض الأحرف التي لا تمثل مساحة أو علامة تبويب. لا يتم تقييم التعبير الفرعي ('\ n'! = C) بسبب تقييم الدائرة القصيرة. تنتهي الحلقة.
  • لقد قرأت الوظيفة مسافة أو حرف علامة تبويب أفقية. يتم تقييم ('\ n'! = C) التعبير الفرعي ، ولكن النتيجة صحيحة دائمًا.

بمعنى آخر ، الكود أعلاه مكافئ لما يلي:

 for (c = getc (InFile); c==' ' || c == '\t'; c = getc (InFile)) 

لقد وجدنا أنه لا يعمل بالطريقة المطلوبة. الآن دعونا نرى ما هي الآثار المترتبة.

توقع مطور ، الذي كتب نداء isspace في نص الدالة EatWhitespace ، أن يتم استدعاء الوظيفة القياسية. لهذا السبب أضافوا الشرط الذي يمنع حرف LF ('\ n') من أن يعامل كحرف بيضاء.

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

الأمر الأكثر أهمية هو أنهم أرادوا تخطي حرف الإرجاع (0x0d ، \ r ') أيضًا. لا يحدث ذلك - تنتهي الحلقة عند مواجهة هذه الشخصية. سينتهي البرنامج بالتصرف بشكل غير متوقع إذا تم تمثيل الخطوط الجديدة بواسطة تسلسل CR + LF ، وهو النوع المستخدم في بعض الأنظمة غير التابعة لنظام UNIX مثل Microsoft Windows.

لمزيد من التفاصيل حول الأسباب التاريخية لاستخدام LF أو CR + LF كأحرف سطر جديد ، راجع صفحة Wikipedia " Newline ".

تم تصميم وظيفة EatWhitespace لمعالجة الملفات بالطريقة نفسها ، سواء كانت تستخدم LF أو CR + LF كأحرف جديدة. لكنه فشل في حالة CR + LF. بمعنى آخر ، إذا كان الملف الخاص بك من عالم Windows ، فأنت في مشكلة :).

على الرغم من أن هذا قد لا يكون خللًا خطيرًا ، خاصةً بالنظر إلى استخدام GNU Midnight Commander في أنظمة التشغيل المشابهة لنظام التشغيل UNIX ، حيث يتم استخدام LF (0x0a ، '\ n') كحرف جديد ، إلا أن تفاهات مثل تلك لا تزال تميل إلى أن تؤدي إلى إزعاج مشاكل في توافق البيانات المعدة على نظامي Linux و Windows.

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

تجاوز الوظائف القياسية هو ممارسة سيئة. بالمناسبة ، ناقشنا حالة مماثلة من الماكرو #define sprintf std :: printf في المقالة الأخيرة " تقدير تحليل الشفرة الثابتة ".

كان الحل الأفضل هو إعطاء الماكرو اسمًا فريدًا ، على سبيل المثال ، is_space_or_tab . هذا من شأنه أن يساعد على تجنب كل الارتباك.

ربما كانت وظيفة isspace القياسية بطيئة للغاية وأنشأ المبرمج إصدارًا أسرع ، يكفي لاحتياجاتهم. لكنهم ما زالوا لا ينبغي عليهم فعل ذلك بهذه الطريقة. قد يكون الحل الأكثر أمانًا هو تحديد isspace بحيث تحصل على رمز غير قابل للترجمة ، بينما يمكن تنفيذ الوظيفة المطلوبة على أنها ماكرو باسم فريد.

شكرا للقراءة. لا تتردد في تنزيل PVS-Studio وجربه مع مشاريعك. كتذكير ، نحن ندعم جافا أيضًا.

Source: https://habr.com/ru/post/ar439372/


All Articles