سنة أخرى تقترب من نهايتها ، وهذا هو الوقت المثالي لجعل نفسك فنجان من القهوة وإعادة قراءة مراجعات الأخطاء التي تم جمعها عبر مشاريع مفتوحة المصدر خلال هذا العام. هذا سيستغرق بعض الوقت ، بالطبع ، لذلك قمنا بإعداد هذه المقالة لتسهيل الأمر عليك. اليوم سوف نتذكر البقع الداكنة الأكثر إثارة للاهتمام التي واجهناها في مشاريع C / C ++ مفتوحة المصدر في عام 2019.
لا. 10. ما هو نظام التشغيل الذي نعمل عليه؟
V1040 خطأ مطبعي محتمل في الهجاء لاسم ماكرو محدد مسبقًا. يشبه الماكرو '__MINGW32_' '__MINGW32__'. winapi.h 4112
#if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32_) #define __UNICODE_STRING_DEFINED #endif
يوجد خطأ مطبعي في اسم
__MINGW32 _ الماكرو (تم إعلان MINGW32 بالفعل بواسطة __MINGW32__). في مكان آخر من المشروع ، تتم كتابة الشيك بشكل صحيح:
بالمناسبة ، لم يكن هذا الخطأ هو أول من تم وصفه في مقال "
CMake: الحالة التي تكون فيها جودة المشروع لا تُغتفر " وإنما هو أول خطأ حقيقي وجده تشخيص V1040 في مشروع حقيقي مفتوح المصدر (19 أغسطس ، 2019).
لا. 9. من هو الأول؟
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 غير خالية ، فإن الشرط سيعود
صحيحًا طوال الوقت ، مما يعني أن نص الفرع
الآخر هو رمز غير قابل للوصول.
تم وصف هذا الخطأ في مقال "
التحقق من مصدر المترجم الذي تم إنشاؤه مؤخرًا بواسطة Huawei ."
لا. 8. عمليات bitwise الخطرة
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 بإرجاع عدد
التكرارات الصالحة في قائمة
الوظائف . بمعنى أن إضافة برامج التكرار غير الفارغة ستؤدي إلى زيادة حجم القائمة بشكل متزايد: 1 و 2 و 3 و 4 وما إلى ذلك. هذا هو المكان الذي يلعب فيه الخطأ:
ret &= AddFunction(*f);
نظرًا لأن الدالة تُرجع قيمة type
int بدلاً من
bool ، ستُرجع العملية '& ='
خطأ للقيم الزوجية لأنه يتم دائمًا تعيين أقل عدد من الأرقام الزوجية على الصفر. هذا هو كيف يمكن
لخطأ واحد دقيق كسر قيمة الإرجاع
SetFunctionsList حتى عندما تكون الوسائط الخاصة به صالحة.
إذا كنت تقرأ المقتطف بعناية (وكنت كذلك ، أليس كذلك؟) ، لربما لاحظت أنه جاء من المشروع ROOT. نعم ، لقد فحصناها أيضًا: "
تحليل كود ROOT ، إطار تحليل البيانات العلمية ".
لا. 7. متغيرات مختلطة
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 . لدينا تقليد للتحقق من هذا المشروع بين الحين والآخر. هذا العام قمنا
بفحصه مرة أخرى.
لا. 6. 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 Looked the Red Dead Redemption's Bullet Engine ".
لا. 5. ما في نهاية السطر؟
هذا واحد سهل إذا كنت تعرف التفاصيل صعبة واحدة.
لا ينبغي مقارنة
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). عند مقارنتها بـ
EOF ، ستتحول هذه الشخصية إلى -1 ، مما يجعل الحالة صحيحة.
تم العثور على الكثير من الأخطاء في أفضل 10 إصدارات هذا العام في برامج ألعاب الكمبيوتر: محركات أو ألعاب مفتوحة المصدر. كما خمنت بالفعل ، جاء هذا واحد من تلك المنطقة أيضا. تم وصف المزيد من الأخطاء في مقال "
كارثة الأيام المظلمة المقبلة: التحليل الثابت وألعاب Roguelike ".
لا. 4. السحر ثابت بي
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" مفقود في المكان العشري السابع.
بالكاد يتسبب الرقم العشري غير الصحيح المليون في أي ضرر ملحوظ ، ولكن لا يزال من الأفضل استخدام الثوابت الموجودة من المكتبات ، التي يتم ضمان صحتها. على سبيل المثال ، يتم تمثيل رقم Pi بواسطة M_PI الثابت من math.h.
لقد قرأت بالفعل عن هذا الخطأ في المقالة "
PVS-Studio Looked the Red Dead Redemption's Bullet Engine " ، حيث تم وضعها في المركز السادس. إذا لم تقرأها بعد ، فهذه هي فرصتك الأخيرة.
تحويل صغير
نحن نقترب من أفضل 3 البق الأكثر إثارة للاهتمام. كما لاحظت على الأرجح ، أقوم بتصنيف الأخطاء ليس حسب تأثيرها ، لكن بالجهد الذي يتطلبه المراجع البشري للعثور عليه. بعد كل شيء ، تتمثل ميزة التحليل الثابت عبر مراجعات الكود في الأساس في عدم قدرة أدوات البرامج على التعب أو نسيان الأشياء. :)
الآن ، دعونا نرى ما لدينا في أعلى 3 لدينا.
لا. 3. استثناء بعيد المنال
يجب أن تكون فئات
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 القديمة الجيدة ، والتي
فحصناها أيضًا في وقت سابق من هذا العام.
لا. 2. علامات 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 ++ الكثير في حد ذاته ، لذلك دعونا نلقي نظرة على الكود المسبق الذي تم إنشاؤه من المقتطف أعلاه:
عثر المحلل على علامة
<div> غير مغلقة. هناك العديد من أجزاء كود html هنا ، لذلك يحتاج المؤلفون إلى مراجعتها.
فوجئت يمكننا تشخيص هذا النوع من الأخطاء؟ لقد تأثرت أيضًا عندما رأيت ذلك للمرة الأولى. لذلك ، نعم ، نحن نعرف شيئًا ما عن تحليل كود html. حسنًا ، فقط إذا كان ضمن رمز C ++. :)
لا يتم وضع هذا الخطأ في المرتبة الثانية فحسب ، بل إنه أيضًا آلة حاسبة ثانية في قائمة أفضل 10 قوائم لدينا. لمعرفة الأخطاء الأخرى التي وجدناها في هذا المشروع ، راجع مقالة "
المتابعة على خطى الآلات الحاسبة: SpeedCrunch ".
لا. 1. وظائف قياسية بعيدة المنال
وهنا علة وضعت لأول مرة. هذا واحد هو علة غريبة بشكل مثير للإعجاب ، والتي تمكنت من جعله من خلال مراجعة التعليمات البرمجية.
حاول أن تجدها بنفسك:
static int EatWhitespace (FILE * InFile) { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); }
لنرى الآن ما يقوله المحلل:
V560 جزء من التعبير الشرطي صحيح دائمًا: ('\ n'! = C). params.c 136.
غريب ، أليس كذلك؟ دعونا نلقي نظرة على بعض المواقع الغريبة الأخرى ولكن في ملف مختلف (charset.h):
#ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t')
حسنًا ، هذا غريب حقًا ... لذا ، إذا كان المتغير
c يساوي
'\ n' ، فسوف تتكرر وظيفة isspace
(ج) التي تبدو غير ضارة ، وبالتالي تمنع تنفيذ الجزء الثاني من الفحص بسبب تقييم دائرة قصر. وإذا تم تنفيذ
isspace (c) ، فسيكون المتغير
c مساويًا لـ
'' أو
'\ t' ، ومن الواضح أنه لا يساوي
'\ n' .
يمكنك القول أن هذا الماكرو مشابه لـ
#define true false ورمز مثل هذا لن يجعله أبدًا من خلال مراجعة الكود. لكن هذا القصاص بالذات فعل - وكان جالسًا في المستودع في انتظار اكتشافه.
للحصول على مزيد من التفاصيل حول هذا الخطأ ، راجع المقال "هل
تريد أن تلعب محققًا؟ ابحث عن الخطأ في وظيفة من قائد منتصف الليل ".
استنتاج
لقد وجدنا الكثير من الأخطاء خلال هذا العام. كانت تلك الأخطاء الشائعة في نسخ اللصق والثوابت غير الدقيقة والعلامات غير المغلقة والكثير من العيوب الأخرى. لكن محللنا يتطور
ويتعلم تشخيص المزيد والمزيد من أنواع المشكلات ، لذلك نحن بالتأكيد لن نتباطأ وسنقوم بنشر مقالات جديدة حول الأخطاء الموجودة في المشاريع بنفس الطريقة المعتادة.
فقط في حالة عدم قراءة مقالاتنا من قبل ، تم العثور على كل هذه الأخطاء باستخدام محلل ثابت PVS-Studio ، الذي نرحب به
لتنزيله وتجربته في مشاريعك الخاصة. يكتشف الأخطاء في البرامج المكتوبة بلغات C و C ++ و C # و Java.
لقد وصلت أخيرًا إلى خط النهاية! إذا فاتتك المستوىان الأولان ، أقترح أن تغتنم الفرصة وأن تكمل هذه المستويات معنا:
C # و
Java .