PVS- ستوديو نظرت إلى محرك الرصاصة الفداء الأحمر الميت

صورة 4

في الوقت الحاضر ، ليست هناك حاجة لتطبيق فيزياء الكائنات من البداية لتطوير اللعبة لأن هناك الكثير من المكتبات لهذا الغرض. تم استخدام Bullet بنشاط في العديد من ألعاب AAA ، ومشاريع الواقع الافتراضي ، والمحاكاة المختلفة ، والتعلم الآلي. ولا يزال يستخدم ، على سبيل المثال ، أحد محركات Red Reddemption Red و Red Dead Redemption 2. فلماذا لا تتحقق من Bullet باستخدام PVS-Studio لترى ما يمكن أن يكتشفه التحليل الثابت للأخطاء في مشروع محاكاة الفيزياء واسع النطاق.

يتم توزيع هذه المكتبة بحرية ، بحيث يمكن للجميع استخدامها في مشاريعهم الخاصة إذا رغبوا في ذلك. بالإضافة إلى Red Dead Redemption ، يتم استخدام محرك الفيزياء هذا أيضًا في صناعة الأفلام لإنشاء مؤثرات خاصة. على سبيل المثال ، تم استخدامه في تصوير فيلم Sherlock Holmes الخاص بـ Guy Ritchie لحساب الاصطدامات.

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

الاحماء


مثال 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.
الصورة 1

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

نسخ لصق


مثال 2:

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

لا يتم استخدام المعلمة V751 "halfExtentsZ" داخل جسم الوظيفة. TinyRenderer.cpp 375

 void TinyRenderObjectData::createCube(float halfExtentsX, float halfExtentsY, float halfExtentsZ, ....) { .... m_model->addVertex(halfExtentsX * cube_vertices_textured[i * 9], halfExtentsY * cube_vertices_textured[i * 9 + 1], halfExtentsY * cube_vertices_textured[i * 9 + 2], cube_vertices_textured[i * 9 + 4], ....); .... } 

مثال 3:

اكتشف المحلل أيضًا الجزء المثير للاهتمام التالي وسأظهره أولاً في النموذج الأولي.

صورة 11

انظر هذا الخط looooooooooong؟

صورة 12

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

قام المحلل بإنشاء التحذيرات التالية على هذا الخط.

V501 هناك تعبيرات فرعية مماثلة 'rotmat.Column1 (). Norm () <1.0001' إلى اليسار وإلى يمين عامل التشغيل '&&'. LinearR4.cpp 351

V501 هناك تعبيرات فرعية متطابقة '0.9999 <rotmat.Column1 (). Norm ()' إلى اليسار وإلى يمين المشغل '&&'. LinearR4.cpp 351

إذا كتبنا كل ذلك في نموذج "جدولي" واضح ، يمكننا أن نرى أن جميع عمليات الفحص نفسها تنطبق على العمود 1 . توضح المقارنات الأخيرة اثنين أن هناك Column1 و Column2 . على الأرجح ، يجب أن تحقق المقارنات الثالثة والرابعة قيمة العمود 2 .

  Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() && Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() &&(Column1() ^ Column2()) < 0.001 && (Column1() ^ Column2()) > -0.001 

في هذا النموذج ، تصبح المقارنات نفسها أكثر وضوحًا.

مثال 4:

خطأ من نفس النوع:

V501 هناك تعبيرات فرعية متطابقة 'cs.m_fJacCoeffInv [0] == 0' إلى اليسار وإلى يمين عامل التشغيل '&&'. b3CpuRigidBodyPipeline.cpp 169

 float m_fJacCoeffInv[2]; static inline void b3SolveFriction(b3ContactConstraint4& cs, ....) { if (cs.m_fJacCoeffInv[0] == 0 && cs.m_fJacCoeffInv[0] == 0) { return; } .... } 

في هذه الحالة ، يتم التحقق من عنصر الصفيف نفسه مرتين. على الأرجح ، يجب أن يكون الشرط كما يلي: cs.m_fJacCoeffInv [0] == 0 && cs.m_fJacCoeffInv [1] == 0 . هذا مثال كلاسيكي لخطأ النسخ.

مثال 5:

اكتشف أيضًا أن هناك عيبًا كهذا:

V517 استخدام نمط "if (A) {...} آخر إذا تم اكتشاف (A) {...}". هناك احتمال لوجود خطأ منطقي. خطوط الفحص: 79 ، 112. main.cpp 79

 int main(int argc, char* argv[]) { .... while (serviceResult > 0) { serviceResult = enet_host_service(client, &event, 0); if (serviceResult > 0) { .... } else if (serviceResult > 0) { puts("Error with servicing the client"); exit(EXIT_FAILURE); } .... } .... } 

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

يوجد تحذير مماثل للمحلل ، ولكن لا يوجد أي فائدة من النظر إليه عن كثب في هذه المقالة.

V517 استخدام نمط "if (A) {...} آخر إذا تم اكتشاف (A) {...}". هناك احتمال لوجود خطأ منطقي. خطوط الفحص: 151 ، 190. PhysicsClientUDP.cpp 151

فوق الجزء العلوي: تجاوز حدود الصفيف


مثال 6:

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

هنا ، في حالة الحلقة ، الحد الأقصى لمتغير dofIndex هو 128 و dof هو 4 شامل. لكن m_desiredState يحتوي أيضًا على 128 عنصرًا فقط. نتيجة لذلك ، قد يتسبب فهرس [dofIndex + dof] في تجاوز صفيف.

V557 تجاوز الصفيف هو ممكن. قد تصل قيمة مؤشر "dofIndex + dof" إلى 130. PhysicsClientC_API.cpp 968

 #define MAX_DEGREE_OF_FREEDOM 128 double m_desiredState[MAX_DEGREE_OF_FREEDOM]; B3_SHARED_API int b3JointControl(int dofIndex, double* forces, int dofCount, ....) { .... if ( (dofIndex >= 0) && (dofIndex < MAX_DEGREE_OF_FREEDOM ) && dofCount >= 0 && dofCount <= 4) { for (int dof = 0; dof < dofCount; dof++) { command->m_sendState.m_desiredState[dofIndex+dof] = forces[dof]; .... } } .... } 

مثال 7:

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

V557 تجاوز الصفيف هو ممكن. قد تصل قيمة مؤشر 'len' إلى 1024. PhysicsClientC_API.cpp 5223

 #define MAX_FILENAME_LENGTH MAX_URDF_FILENAME_LENGTH 1024 struct b3Profile { char m_name[MAX_FILENAME_LENGTH]; int m_durationInMicroSeconds; }; int len = strlen(name); if (len >= 0 && len < (MAX_FILENAME_LENGTH + 1)) { command->m_type = CMD_PROFILE_TIMING; strcpy(command->m_profile.m_name, name); command->m_profile.m_name[len] = 0; } 

قياسه مرة واحدة ، وقطع عليه سبع مرات


مثال 8:

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

V807 انخفاض الأداء. حاول إنشاء مؤشر لتجنب استخدام تعبير 'm_app-> m_renderer-> getActiveCamera () بشكل متكرر. InverseKinematicsExample.cpp 315

 virtual void resetCamera() { .... if (....) { m_app->m_renderer->getActiveCamera()->setCameraDistance(dist); m_app->m_renderer->getActiveCamera()->setCameraPitch(pitch); m_app->m_renderer->getActiveCamera()->setCameraYaw(yaw); m_app->m_renderer->getActiveCamera()->setCameraPosition(....); } } 

يتم استخدام سلسلة الاتصال نفسها عدة مرات هنا ويمكن استبدالها بمؤشر واحد.

مثال 9:

V810 انخفاض الأداء. تم استدعاء الدالة 'btCos (euler_out.pitch)' عدة مرات باستخدام وسيطات متطابقة. ربما يجب حفظ النتيجة في متغير مؤقت ، والذي يمكن استخدامه أثناء استدعاء وظيفة "btAtan2". btMatrix3x3.h 576

V810 انخفاض الأداء. تم استدعاء الدالة 'btCos (euler_out2.pitch)' عدة مرات باستخدام وسيطات متطابقة. ربما يجب حفظ النتيجة في متغير مؤقت ، والذي يمكن استخدامه أثناء استدعاء وظيفة "btAtan2". btMatrix3x3.h 578

 void getEulerZYX(....) const { .... if (....) { .... } else { .... euler_out.roll = btAtan2(m_el[2].y() / btCos(euler_out.pitch), m_el[2].z() / btCos(euler_out.pitch)); euler_out2.roll = btAtan2(m_el[2].y() / btCos(euler_out2.pitch), m_el[2].z() / btCos(euler_out2.pitch)); euler_out.yaw = btAtan2(m_el[1].x() / btCos(euler_out.pitch), m_el[0].x() / btCos(euler_out.pitch)); euler_out2.yaw = btAtan2(m_el[1].x() / btCos(euler_out2.pitch), m_el[0].x() / btCos(euler_out2.pitch)); } .... } 

في هذه الحالة ، يمكنك إنشاء متغيرين وحفظ القيم التي يتم إرجاعها بواسطة الدالة btCos لـ euler_out.pitch و euler_out2.pitch فيها بدلاً من استدعاء الوظيفة أربع مرات لكل وسيطة.

تسرب


مثال 10:

تم اكتشاف الكثير من الأخطاء من النوع التالي في المشروع:

تم إنهاء نطاق الرؤية V773 لمؤشر 'المستورد' دون تحرير الذاكرة. تسرب الذاكرة ممكن. SerializeSetup.cpp 94

 void SerializeSetup::initPhysics() { .... btBulletWorldImporter* importer = new btBulletWorldImporter(m_dynamicsWorld); .... fclose(file); m_guiHelper->autogenerateGraphicsObjects(m_dynamicsWorld); } 

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

يعيش C ++ حسب الكود الخاص به


مثال 11:

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

 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; .... } .... } 

يقوم المحلل بإنشاء التحذير التالي:

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

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

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

مثال 12:

عند العمل باستخدام سلاسل ، من الأفضل استخدام الميزات التي توفرها فئة السلسلة . لذا ، في الحالتين التاليتين ، من الأفضل استبدال strlen (MyStr.c_str ()) و val = "" بـ MyStr.length () و val.clear () ، على التوالي.

V806 انخفاض الأداء. يمكن إعادة كتابة تعبير النوع strlen (MyStr.c_str ()) كـ MyStr.length (). RobotLoggingUtil.cpp 213

 FILE* createMinitaurLogFile(const char* fileName, std::string& structTypes, ....) { FILE* f = fopen(fileName, "wb"); if (f) { .... fwrite(structTypes.c_str(), strlen(structTypes.c_str()), 1, f); .... } .... } 

V815 انخفاض الأداء. فكّر في استبدال التعبير 'val = ""' بـ "val.clear () '. b3CommandLineArgs.h 40

 void addArgs(int argc, char **argv) { .... std::string val; .... val = ""; .... } 

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

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

أخطاء وجدت من قبلنا


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

لم يكن هناك الكثير من طلبات السحب في المستودع والعديد منها مرتبط بالمنطق الداخلي للمحرك. ولكن كانت هناك أيضًا أخطاء يمكن للمحلل اكتشافها.

مثال 13:

 char m_deviceExtensions[B3_MAX_STRING_LENGTH]; void b3OpenCLUtils_printDeviceInfo(cl_device_id device) { b3OpenCLDeviceInfo info; b3OpenCLUtils::getDeviceInfo(device, &info); .... if (info.m_deviceExtensions != 0) { .... } } 

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

V600 النظر في فحص الشرط. مؤشر 'info.m_deviceExtensions' دائمًا لا يساوي NULL. b3OpenCLUtils.cpp 551

مثال 14:

يمكنك معرفة ما هي المشكلة مع الوظيفة التالية؟

 inline void Matrix4x4::SetIdentity() { m12 = m13 = m14 = m21 = m23 = m24 = m13 = m23 = m41 = m42 = m43 = 0.0; m11 = m22 = m33 = m44 = 1.0; } 

يقوم المحلل بإنشاء التحذيرات التالية:

V570 يتم تعيين نفس القيمة مرتين للمتغير "m23". LinearR4.h 627

V570 يتم تعيين نفس القيمة مرتين للمتغير "m13". LinearR4.h 627

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

 m12 = m13 = m14 = m21 = m23 = m24 = m31 = m32 = m34 = m41 = m42 = m43 = 0.0; 

مثال 15:

الخطأ التالي في أحد شروط الدالة btSoftBody :: addAeroForceToNode () أدى إلى خطأ واضح. وفقًا للتعليق في طلب السحب ، تم تطبيق القوات على الأشياء من الجانب الخطأ.

 struct eAeroModel { enum _ { V_Point, V_TwoSided, .... END }; }; void btSoftBody::addAeroForceToNode(....) { .... if (....) { if (btSoftBody::eAeroModel::V_TwoSided) { .... } .... } .... } 

يمكن لـ PVS-Studio أيضًا العثور على هذا الخطأ وإنشاء التحذير التالي:

V768 يتم استخدام ثابت التعداد 'V_TwoSided' كمتغير من نوع Boolean. btSoftBody.cpp 542

الاختيار الثابت يشبه هذا:

 if (m_cfg.aeromodel == btSoftBody::eAeroModel::V_TwoSided) { .... } 

بدلاً من تكافؤ خاصية كائن بواحد من العدادات ، تم التحقق من العداد V_TwoSided نفسه.

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

استنتاج


صورة 6

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

تابعنا واشترك في حساباتنا وقنوات التواصل الاجتماعي: Instagram و Twitter و Facebook و Telegram . يسعدنا أن نكون معك أينما كنت وأن نبقيك على تواصل دائم.

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


All Articles