كارثة الأيام المظلمة المقبلة ، تحليل ثابت والخبز

الصورة 10

على الأرجح ، من عنوان المقال الذي خمنته بالفعل أن التركيز ينصب على الأخطاء في شفرة المصدر. لكن هذا ليس الشيء الوحيد الذي سيتم مناقشته في هذه المقالة. إذا كنت بجانب C ++ وأخطاء في رمز شخص آخر ، فأنت تنجذب إلى ألعاب غير عادية وتهتم بمعرفة ماهية هذه "الخبز" وماذا يأكلون معهم ، مرحبًا بك في cat!

في بحثي عن ألعاب غير عادية ، صادفت لعبة Cataclysm Dark Days Ahead ، والتي تختلف عن غيرها من الرسومات غير العادية: يتم تنفيذها باستخدام أحرف ASCII متعددة الألوان على خلفية سوداء.

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

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

حتى الآن ، تم اختبار الكثير من الألعاب باستخدام PVS-Studio. على سبيل المثال ، يمكنك قراءة مقالتنا الأخرى ، " التحليل الثابت في صناعة ألعاب الفيديو: أهم 10 أخطاء برمجية ".

منطق


مثال 1:

المثال التالي هو خطأ نسخة نموذجية.

V501 هناك تعبيرات فرعية مماثلة إلى اليسار وإلى يمين "||" عامل التشغيل: rng (2 ، 7) <abs (z) || rng (2 ، 7) <abs (z) overmap.cpp 1503

bool overmap::generate_sub( const int z ) { .... if( rng( 2, 7 ) < abs( z ) || rng( 2, 7 ) < abs( z ) ) { .... } .... } 

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

تحذير مماثل:
  • V501 هناك تعبيرات فرعية متطابقة 'one_in (100000 / to_turns <int> (dur))' إلى اليسار وإلى يمين عامل التشغيل '&&'. player_hardcoded_effects.cpp 547

صورة 9

مثال 2:

V728 يمكن تبسيط عملية التحقق المفرطة. The '(A && B) || تعبير (! A &&! B) 'يعادل التعبير' bool (A) == bool (B) '. inventory_ui.cpp 199

 bool inventory_selector_preset::sort_compare( .... ) const { .... const bool left_fav = g->u.inv.assigned.count( lhs.location->invlet ); const bool right_fav = g->u.inv.assigned.count( rhs.location->invlet ); if( ( left_fav && right_fav ) || ( !left_fav && !right_fav ) ) { return .... } .... } 

لا يوجد خطأ في الحالة ، لكنه معقد بشكل غير ضروري. يجدر أخذ الأمر بالشفقة على أولئك الذين يتعين عليهم تفكيك هذا الشرط ، ومن السهل الكتابة إذا (left_fav == right_fav) .

تحذير مماثل:

  • V728 يمكن تبسيط عملية التحقق المفرطة. The '(A &&! B) || تعبير (! A && B) 'يعادل التعبير' bool (A)! = Bool (B) '. iuse_actor.cpp 2653

تراجع أنا


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

الصورة 8

Mikrooptimizatsii


مثال 3:

لا تشير المجموعة التالية من تحذيرات المحلل إلى وجود خطأ ، ولكن تشير إلى احتمال إجراء microoptimization لرمز البرنامج.

V801 انخفاض الأداء. من الأفضل إعادة تعريف وسيطة الدالة الثانية كمرجع. فكر في استبدال "const ... type" بـ "const ... & type". map.cpp 4644

 template <typename Stack> std::list<item> use_amount_stack( Stack stack, const itype_id type ) { std::list<item> ret; for( auto a = stack.begin(); a != stack.end() && quantity > 0; ) { if( a->use_amount( type, ret ) ) { a = stack.erase( a ); } else { ++a; } } return ret; } 

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

تحذيرات مماثلة:

  • V801 انخفاض الأداء. من الأفضل إعادة تعريف الوسيطة الدالة الثالثة كمرجع. فكر في استبدال "const ... evt_filter" بـ "const ... & evt_filter". input.cpp 691
  • V801 انخفاض الأداء. من الأفضل إعادة تعريف وسيطة الدالة الخامسة كمرجع. فكر في استبدال "const ... color" بـ "const ... & color". output.h 207
  • في المجموع ، ولدت محلل 32 من هذه التحذيرات.

مثال 4:

V813 انخفاض الأداء. ربما يجب تقديم وسيطة 'str' كمرجع ثابت. catacharset.cpp 256

 std::string base64_encode( std::string str ) { if( str.length() > 0 && str[0] == '#' ) { return str; } int input_length = str.length(); std::string encoded_data( output_length, '\0' ); .... for( int i = 0, j = 0; i < input_length; ) { .... } for( int i = 0; i < mod_table[input_length % 3]; i++ ) { encoded_data[output_length - 1 - i] = '='; } return "#" + encoded_data; } 

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

هذا التحذير لم يكن وحيدًا ، فهناك 26 حالة في المجموع.

الصورة 7

تحذيرات مماثلة:

  • V813 انخفاض الأداء. ربما يجب تقديم حجة "الرسالة" كمرجع ثابت. json.cpp 1452
  • V813 انخفاض الأداء. ربما يجب تقديم حجة '' كمرجع ثابت. catacharset.cpp 218
  • وهلم جرا ...

تراجع الثاني


لا تزال بعض ألعاب roguelike الكلاسيكية قيد التطوير. إذا ذهبت إلى مستودعات GitHub Cataclysm DDA أو NetHack ، يمكنك أن ترى أن التغييرات يتم إجراؤها بنشاط كل يوم. تعد NetHack بشكل عام أقدم لعبة لا تزال قيد التطوير: تم إصدارها في يوليو 1987 ، ويعود تاريخ آخر إصدار إلى عام 2018.

ومع ذلك ، واحدة من الألعاب الشهيرة في وقت لاحق من هذا النوع هي Dwarf Fortress ، التي تم تطويرها منذ عام 2002 وأصدرت لأول مرة في عام 2006. "الخسارة ممتعة" هو شعار اللعبة الذي يعكس جوهرها بدقة ، لأنه من المستحيل الفوز بها. حازت هذه اللعبة في عام 2007 على لقب أفضل لعبة roguelike لهذا العام نتيجة التصويت ، والتي تقام سنويًا على موقع ASCII GAMES.

صورة 6

بالمناسبة ، قد يكون المهتمون بهذه اللعبة مهتمين بالأخبار التالية. سيتم إطلاق Fort Dwarf Fortress على Steam مع رسومات محسّنة 32 بت. مع صورة محدّثة يعمل عليها مشرفان من ذوي الخبرة ، سيتلقى الإصدار المتميز من Dwarf Fortress مقطوعات موسيقية إضافية ودعمًا لـ Steam Workshop. ولكن إذا كان هناك أي شيء ، فسيكون بمقدور مالكي النسخة المدفوعة من Dwarf Fortress تغيير الرسومات المحدثة إلى النموذج السابق في ASCII. مزيد من التفاصيل .

تجاوز عامل التشغيل


أمثلة 5 ، 6:

كان هناك أيضا زوج مثير للاهتمام من التحذيرات المماثلة.

V690 تطبق فئة 'JsonObject' مُنشئ نسخ ، لكنها تفتقر إلى عامل التشغيل '='. من الخطير استخدام مثل هذه الفئة. json.h 647

 class JsonObject { private: .... JsonIn *jsin; .... public: JsonObject( JsonIn &jsin ); JsonObject( const JsonObject &jsobj ); JsonObject() : positions(), start( 0 ), end( 0 ), jsin( NULL ) {} ~JsonObject() { finish(); } void finish(); // moves the stream to the end of the object .... void JsonObject::finish() { .... } .... } 

تحتوي هذه الفئة على مُنشئ نسخ و destructor ، ومع ذلك ، فهي لا تفرط في عامل التشغيل. المشكلة هنا هي أن مشغل المهمة الذي تم إنشاؤه تلقائيًا يمكنه فقط تعيين مؤشر إلى JsonIn . نتيجة لذلك ، يشير كلا الكائنين من فئة JsonObject إلى نفس JsonIn . لا يُعرف ما إذا كان مثل هذا الموقف قد ينشأ في مكان ما الآن ، ولكن ، على أي حال ، فإن هذا أمر شائع أن يتقدم شخص ما عاجلاً أم آجلاً.

مشكلة مماثلة موجودة في الفصل التالي.

V690 تطبق فئة "JsonArray" مُنشئ نسخ ، لكنها تفتقر إلى عامل التشغيل "=". من الخطير استخدام مثل هذه الفئة. json.h 820

 class JsonArray { private: .... JsonIn *jsin; .... public: JsonArray( JsonIn &jsin ); JsonArray( const JsonArray &jsarr ); JsonArray() : positions(), ...., jsin( NULL ) {}; ~JsonArray() { finish(); } void finish(); // move the stream position to the end of the array void JsonArray::finish() { .... } } 

لمزيد من المعلومات حول خطر نقص التحميل الزائد لمشغل المهمة لفئة معقدة ، راجع مقالة " قانون الاثنين الكبار " (أو ترجمة هذه المقالة " C ++: The Big Two Law ").

أمثلة 7 ، 8:

مثال آخر يتعلق بمشغل المهمة المثقل ، لكننا نتحدث هذه المرة عن تنفيذه المحدد.

V794 يجب حماية مشغل المهمة من حالة "هذا == & غيرها". mattack_common.h 49

 class StringRef { public: .... private: friend struct StringRefTestAccess; char const* m_start; size_type m_size; char* m_data = nullptr; .... auto operator = ( StringRef const &other ) noexcept -> StringRef& { delete[] m_data; m_data = nullptr; m_start = other.m_start; m_size = other.m_size; return *this; } 

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

مثال مشابه على التحميل الزائد لمشغل مهمة خاطئ مع تأثير جانبي مثير للاهتمام:

V794 يجب حماية مشغل التخصيص من حالة "this == & rhs". player_activity.cpp 38

 player_activity &player_activity::operator=( const player_activity &rhs ) { type = rhs.type; .... targets.clear(); targets.reserve( rhs.targets.size() ); std::transform( rhs.targets.begin(), rhs.targets.end(), std::back_inserter( targets ), []( const item_location & e ) { return e.clone(); } ); return *this; } 

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

الصورة 16

تراجع الثالث


في عام 2008 ، اكتسب الخبز تعريفًا رسميًا ، والذي أطلق عليه اسم الملحمة "Berlin Interpretation". وفقًا لهذا التعريف ، فإن الميزات الرئيسية لهذه الألعاب هي:

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

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

الوضع المعتاد في Cataclysm DDA: متجمد وجائع حتى الموت ، أنت تعذب بالعطش ، وبالفعل لديك ستة مخالب بدلاً من الأرجل.

صورة 15

تفاصيل مهمة


مثال 9:

V1028 تجاوز السعة المحتملة. ضع في الاعتبار صب معاملات عامل التشغيل "start + أكبر" للنوع "size_t" ، وليس النتيجة. worldfactory.cpp 638

 void worldfactory::draw_mod_list( int &start, .... ) { .... int larger = ....; unsigned int iNum = ....; .... for( .... ) { if( iNum >= static_cast<size_t>( start ) && iNum < static_cast<size_t>( start + larger ) ) { .... } .... } .... } 

يبدو أن المبرمج أراد تجنب الفائض. لكن إحضار نتيجة الإضافة في هذه الحالة لا طائل منه ، لأن التدفق الزائد سيحدث عند إضافة الأرقام ، وسيتم تنفيذ التوسيع على نتيجة لا معنى لها. لتفادي هذا الموقف ، تحتاج إلى إرسال وسيطة واحدة فقط إلى نوع أكبر: (static_cast <size_t> (start) + أكبر) .

مثال 10:

V530 يجب استخدام قيمة الإرجاع "حجم" الدالة. worldfactory.cpp 1340

 bool worldfactory::world_need_lua_build( std::string world_name ) { #ifndef LUA .... #endif // Prevent unused var error when LUA and RELEASE enabled. world_name.size(); return false; } 

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

مثال 11:

V812 انخفاض الأداء. الاستخدام غير الفعال لوظيفة "العد". يمكن الاستعاضة عنها بالدعوة إلى وظيفة "find". player.cpp 9600

 bool player::read( int inventory_position, const bool continuous ) { .... player_activity activity; if( !continuous || !std::all_of( learners.begin(), learners.end(), [&]( std::pair<npc *, std::string> elem ) { return std::count( activity.values.begin(), activity.values.end(), elem.first->getID() ) != 0; } ) { .... } .... } 

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

مثال 12:

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

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

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

الصورة 3

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

مثال 13:

الخطأ الصغير التالي قد يصبح ذات يوم حرجًا. لا عجب أنه في قائمة CWE مثل CWE-834 . وكان هناك خمسة منهم بالمناسبة.

حلقة V663 اللانهائية ممكنة. شرط "cin.eof ()" غير كاف للكسر من الحلقة. ضع في اعتبارك إضافة استدعاء دالة 'cin.fail () إلى التعبير الشرطي. action.cpp 46

 void parse_keymap( std::istream &keymap_txt, .... ) { while( !keymap_txt.eof() ) { .... } } 

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

 while( !keymap_txt.eof() ) { if(keymap_txt.fail()) { keymap_txt.clear(); keymap_txt.ignore(numeric_limits<streamsize>::max(),'\n'); break; } .... } 

مطلوب keymap_txt.clear () لإزالة حالة الخطأ (علامة) من الدفق في حالة حدوث خطأ في القراءة من الملف ، وإلا فسيكون من المستحيل قراءة النص. keymap_txt.ignore مع numeric_limits <streamsize> :: max () المعلمات ويسمح لك حرف التحكم في تغذية السطر بتخطي بقية السطر.

هناك طريقة أبسط بكثير لإيقاف القراءة:

 while( !keymap_txt ) { .... } 

عند استخدامه في سياق المنطق ، فإنه يحول نفسه إلى قيمة مكافئة للقيمة true حتى يتم الوصول إلى EOF .

تراجع الرابع


الآن الألعاب الأكثر شعبية هي تلك التي تجمع بين علامات ألعاب roguelike والأنواع الأخرى: منهاج الألعاب ، والاستراتيجيات ، وما إلى ذلك. أصبحت هذه الألعاب تسمى roguelike-like أو roguelite. وتشمل هذه الألعاب ألقاب شهيرة مثل "لا تجويع" و "Binding of Isaac" و "FTL: Faster Than Light" و "Darkest Dungeon" و "Diablo".

على الرغم من أنه في بعض الأحيان يكون الفرق بين roguelike و roguelite صغيرًا لدرجة أنه ليس من الواضح النوع الذي تنتمي إليه اللعبة. يعتقد أحدهم أن Dwarf Fortress لم تعد لعبة roguelike ، ولكن بالنسبة لشخص ما ، Diablo هو الخبز التقليدي.

الصورة 1

استنتاج


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

الصورة 2

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

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


All Articles