أفضل 10 أخطاء في مشاريع C ++ لعام 2019

الصورة 7

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

المركز العاشر: "ما هو نظام التشغيل لدينا؟"


V1040 خطأ مطبعي محتمل في الهجاء لاسم ماكرو محدد مسبقًا. يشبه الماكرو '__MINGW32_' '__MINGW32__'. winapi.h 4112

#if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32_) #define __UNICODE_STRING_DEFINED #endif 

هنا ، تم إجراء خطأ مطبعي في اسم الماكرو __MINGW32 _ (يعلن MINGW32 __MINGW32__). في أماكن أخرى من المشروع ، تتم كتابة التحقق بشكل صحيح:

الصورة 3


هذا ، بالمناسبة ، لم يكن الخطأ الأول فقط في مقال " CMake: الحالة عندما يكون المشروع لا يغتفر لجودة الكود " ، ولكن بشكل عام الخطأ الحقيقي الأول الذي وجدته تشخيصات V1040 في مشروع مفتوح حقيقي (19 أغسطس 2019).

المركز التاسع: "من هو الأول؟"


V502 ربما يعمل المشغل '؟:' بطريقة مختلفة عما كان متوقعًا. لدى المشغل '؟:' أولوية أقل من المشغل '=='. mir_parser.cpp 884

 enum Opcode : uint8 { kOpUndef, .... OP_intrinsiccall, OP_intrinsiccallassigned, .... kOpLast, }; bool MIRParser::ParseStmtIntrinsiccall(StmtNodePtr &stmt, bool isAssigned) { Opcode o = !isAssigned ? (....) : (....); auto *intrnCallNode = mod.CurFuncCodeMemPool()->New<IntrinsiccallNode>(....); lexer.NextToken(); if (o == !isAssigned ? OP_intrinsiccall : OP_intrinsiccallassigned) { intrnCallNode->SetIntrinsic(GetIntrinsicID(lexer.GetTokenKind())); } else { intrnCallNode->SetIntrinsic(static_cast<MIRIntrinsicID>(....)); } .... } 

نحن مهتمون بالجزء التالي من هذا الرمز:

 if (o == !isAssigned ? OP_intrinsiccall : OP_intrinsiccallassigned) { .... } 

المشغل '==' له أولوية أعلى من المشغل الثلاثي (؟ :). لهذا السبب ، لا يتم تقييم التعبير الشرطي بشكل صحيح. الكود المكتوب مكافئ لما يلي:

 if ((o == !isAssigned) ? OP_intrinsiccall : OP_intrinsiccallassigned) { .... } 

مع الأخذ في الاعتبار حقيقة أن الثوابت OP_intrinsiccall و OP_intrinsiccallassigned لها قيم غير صفرية ، فإن هذا الشرط يرجع دائمًا القيمة الحقيقية. نص الفرع الآخر هو رمز غير قابل للوصول.

لقد جاء هذا الخطأ إلى القمة من مقال " التحقق من رمز مترجم Ark الذي فتحته Huawei مؤخرًا ."

المركز الثامن: "خطر عمليات البت"


V1046 الاستخدام غير الآمن لأنواع bool و "int" معًا في العملية '& ='. GSLMultiRootFinder.h 175

 int AddFunction(const ROOT::Math::IMultiGenFunction & func) { ROOT::Math::IMultiGenFunction * f = func.Clone(); if (!f) return 0; fFunctions.push_back(f); return fFunctions.size(); } template<class FuncIterator> bool SetFunctionList( FuncIterator begin, FuncIterator end) { bool ret = true; for (FuncIterator itr = begin; itr != end; ++itr) { const ROOT::Math::IMultiGenFunction * f = *itr; ret &= AddFunction(*f); } return ret; } 

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

ومع ذلك ، في الواقع ، يمكن أن ترجع الدالة SetFunctionList false حتى بالنسبة للتكرارات الصالحة. دعونا نلقي نظرة على الموقف ، حيث تقوم دالة AddFunction بإرجاع عدد التكرارات الصالحة في قائمة fFunctions . أي عند إضافة مكررات غير صفرية ، سيزداد حجم هذه القائمة بالتتابع: 1 ، 2 ، 3 ، 4 ، إلخ. هذا هو المكان الذي يبدأ فيه الخطأ في الكود في إظهار نفسه:

 ret &= AddFunction(*f); 

لأن نظرًا لأن الدالة تُرجع نتيجة type int ، وليس منطقيًا ، فإن العملية '& =' بالأرقام الزوجية سوف تعطي القيمة خاطئة . بعد كل شيء ، فإن أقل قليلا من الأرقام الزوجية سيكون دائما صفر. لذلك ، فإن مثل هذا الخطأ غير الواضح سوف يفسد نتيجة دالة SetFunctionsList حتى للوسائط الصحيحة.

إذا كنت تقرأ الشفرة بعناية من المثال (وقرأتها بعناية ، أليس كذلك؟) ، فقد تلاحظ أن هذا رمز من مشروع ROOT. بالطبع ، قمنا باختباره: " تحليل كود ROOT - إطار لتحليل بيانات البحث العلمي ."

المركز السابع: "الارتباك في المتغيرات"


V1001 [CWE-563] تم تعيين متغير "الوضع" ولكن لا يتم استخدامه في نهاية الوظيفة. SIModeRegister.cpp 48

 struct Status { unsigned Mask; unsigned Mode; Status() : Mask(0), Mode(0){}; Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) { Mode &= Mask; }; .... }; 

من الخطورة جدًا إعطاء وسيطات الوظيفة نفس أسماء أعضاء الفصل. من السهل جدا الحصول على الخلط. أمامنا هو مجرد مثل هذه الحالة. هذا التعبير لا معنى له:

 Mode &= Mask; 

تتغير حجة الوظيفة. وهذا كل شيء. هذه الحجة لم تعد تستخدم. على الأرجح ، كان من الضروري أن تكتب مثل هذا:

 Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) { this->Mode &= Mask; }; 

وهذا خطأ من LLVM . لدينا تقليد من وقت لآخر لتحليل هذا المشروع. هذا العام لدينا أيضا مقال عن التحقق.

المركز السادس: "C ++ لها قوانينها الخاصة"


يظهر الخطأ التالي في الكود بسبب حقيقة أن قواعد C ++ لا تتوافق دائمًا مع القواعد الرياضية أو "الفطرة السليمة". إشعار لنفسك حيث الخطأ في مقتطف صغير من التعليمات البرمجية؟

V709 تم العثور على مقارنة مشبوهة: 'f0 == f1 == m_fractureBodies.size ()'. تذكر أن 'a == b == c' لا تساوي 'a == b && b == c'. btFractureDynamicsWorld.cpp 483

 btAlignedObjectArray<btFractureBody*> m_fractureBodies; void btFractureDynamicsWorld::fractureCallback() { for (int i = 0; i < numManifolds; i++) { .... int f0 = m_fractureBodies.findLinearSearch(....); int f1 = m_fractureBodies.findLinearSearch(....); if (f0 == f1 == m_fractureBodies.size()) continue; .... } .... } 

يبدو أن الشرط يتحقق من أن f0 تساوي f1 وتساوي عدد العناصر في m_fractureBodies . يبدو أن هذه المقارنة يجب أن تتحقق مما إذا كانت f0 و f1 في نهاية صفيف m_fractureBodies ، لأنها تحتوي على موضع الكائن الذي تم العثور عليه بواسطة طريقة findLinearSearch () . ومع ذلك ، في الواقع ، يتحول هذا التعبير إلى فحص لمعرفة ما إذا كانت f0 و f1 متساوية ، ثم إلى التحقق لمعرفة ما إذا كان m_fractureBodies.size () يساوي نتيجة f0 == f1 . نتيجة لذلك ، تتم مقارنة المعامل الثالث هنا بـ 0 أو 1.

خطأ جميل! ولحسن الحظ ، نادر جدًا. حتى الآن التقينا بها فقط في ثلاثة مشاريع مفتوحة ، ومن المثير للاهتمام أن جميعها كانت مجرد محركات ألعاب. بالمناسبة ، ليس هذا هو الخطأ الوحيد الذي وجدناه في Bullet. الأكثر إثارة للاهتمام حصلت في مقالتنا " PVS-Studio نظرت إلى Red Dead Redemption - Bullet engine ".

المركز الخامس: "ما هي نهاية الخط؟"


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

لا ينبغي مقارنة V739 EOF بقيمة من النوع "char". يجب أن يكون "ch" من النوع "int". json.cpp 762

 void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... } 

هذا أحد تلك الأخطاء التي يصعب ملاحظتها إذا كنت لا تعرف أن EOF يُعرَّف بأنه -1. وفقًا لذلك ، إذا حاولت مقارنتها بمتغير من النوع char موقَّع ، فالشرط دائمًا ما يكون خاطئًا . الاستثناء الوحيد هو إذا كان رمز الحرف هو 0xFF (255). عند المقارنة ، سيتحول هذا الرمز إلى -1 وستكون الحالة صحيحة.

هناك الكثير من الأخطاء المتعلقة بالألعاب في هذا الجزء العلوي: من المحركات إلى الألعاب المفتوحة. كما كنت قد خمنت ، جاء هذا المكان لنا أيضا من هذا المجال. يمكنك رؤية المزيد من الأخطاء في المقالة " كارثة الأيام المظلمة المقبلة والتحليل الثابت والخبز ".

المركز الرابع: "سحر الرقم"


V624 ربما يكون هناك خطأ مطبعي في ثابت "3.141592538". حاول استخدام ثابت M_PI من <math.h>. PhysicsClientC_API.cpp 4109

 B3_SHARED_API void b3ComputeProjectionMatrixFOV(float fov, ....) { float yScale = 1.0 / tan((3.141592538 / 180.0) * fov / 2); .... } 


خطأ مطبعي صغير في الرقم Pi (3.141592653 ...) ، مفقودًا الرقم "6" في الموضع السابع في الجزء الكسري.

صورة 4
ربما لن يؤدي الخطأ في المكان العشري العشري إلى عواقب ملموسة ، ولكن لا يزال يتعين عليك استخدام ثوابت المكتبة الحالية بدون أخطاء مطبعية. بالنسبة لـ Pi ، على سبيل المثال ، يوجد ثابت M_PI من رأس math.h.

هذا الخطأ هو من مقال " نظرت PVS-Studio في Red Red Redemption - Bullet engine " ، المألوف لنا بالفعل في المركز السادس. إذا لم تقم بإيقاف تشغيله في وقت لاحق ، فهذه هي الفرصة الأخيرة.

استطراد خفيف


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

والآن ، أوجه انتباهكم إلى الثلاثة الأوائل.

الصورة 8


المركز الثالث: "استثناء بعيد المنال"


يجب أن تكون فئات V702 مستمدة دائمًا من std :: استثناء (وما شابه ) كـ "عام" (لم يتم تحديد أي كلمة رئيسية ، لذلك يقوم المحول البرمجي بتعيينها إلى "خاص"). CalcManager CalcException.h 4

 class CalcException : std::exception { public: CalcException(HRESULT hr) { m_hr = hr; } HRESULT GetException() { return m_hr; } private: HRESULT m_hr; }; 

اكتشف المحلل فئة موروثة من الفئة std :: استثناء من خلال المعدل الخاص (المعدل الافتراضي إذا لم يتم تحديد أي شيء). المشكلة في هذا الرمز هي أنه عند محاولة التقاط الاستثناء العام std :: استثناء ، سيتم تخطي استثناء من النوع CalcException . يحدث هذا السلوك لأن الميراث الخاص يحول دون تحويل النوع الضمني.

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

المركز الثاني: علامات HTML غير المغلقة


V735 ربما HTML غير صحيح. تمت مصادفة علامة الإغلاق "</body>" ، بينما كانت علامة "</div>" متوقعة. book.cpp 127

 static QString makeAlgebraLogBaseConversionPage() { return BEGIN INDEX_LINK TITLE(Book::tr("Logarithmic Base Conversion")) FORMULA(y = log(x) / log(a), log<sub>a</sub>x = log(x) / log(a)) END; } 

كما يحدث غالبًا مع شفرة C / C ++ ، لا يوجد شيء واضح من المصدر ، لذلك دعونا ننتقل إلى الكود المسبق المعالجة لهذه الشريحة:

صورة 6


اكتشف المحلل علامة <div> غير مغلقة. هناك العديد من أجزاء كود html في هذا الملف ، والآن يجب فحصها من قِبل المطورين.

فوجئت أننا يمكن أن تحقق وكذا؟ عندما رأيت هذا لأول مرة ، أعجبت. لذلك نحن نحلل قليلا من كود HTML. صحيح ، فقط في رمز C ++. :)

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

المركز الأول: "الميزات القياسية المراوغة"


لذلك وصلنا إلى المقام الأول. مشكلة غريبة مثيرة للإعجاب والتي مرت بمراجعة الكود.

حاول اكتشافه بنفسك:

 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 */ 

الآن دعونا نرى ما يقسم المحلل:

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

غريب ، أليس كذلك؟ دعونا ننظر إلى شيء مثير للاهتمام في نفس المشروع ، ولكن في ملف مختلف (charset.h):

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

لذلك ، وهذا أمر غريب بالفعل ... اتضح أنه إذا كان المتغير c يساوي '\ n' ، فإن وظيفة الإصدار الأول (c) غير ضارة تمامًا ستعود خطأ ولن يتم تنفيذ الجزء الثاني من هذا الاختبار بسبب تقييم الدائرة القصيرة. إذا تم تنفيذ isspace (c) ، فسيكون المتغير c إما '' أو '\ t' ، ومن الواضح أن هذا لا يساوي '\ n' .

بالطبع ، يمكنك القول أن هذا الماكرو يشبه # تعريف الخطأ الحقيقي ، وأن مثل هذا الرمز لن يجتاز مراجعة الرمز. ومع ذلك ، اجتاز هذا الرمز المراجعة وكان ينتظرنا بأمان في مستودع المشروع.

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

استنتاج


صورة 9


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

إذا قرأ شخص ما مقالاتنا لأول مرة ، فقط في حالة ، سنوضح أنه تم العثور على كل هذه الأخطاء باستخدام محلل الشفرة الثابتة PVS-Studio ، الذي نقترح تنزيله وتجربته. يمكن للمحلل اكتشاف الأخطاء في كود البرامج المكتوبة باللغات: C و C ++ و C # و Java.

لذلك وصلنا إلى النهاية! إذا فاتتك المستوىان الأولان ، أقترح ألا تفوتك الفرصة وتصفحهما معنا: C # و Java .



إذا كنت ترغب في مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فالرجاء استخدام الرابط الخاص بالترجمة: Maxim Zvyagintsev. تم العثور على أفضل 10 أخطاء في مشاريع C ++ في عام 2019 .

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


All Articles