إعادة فحص ديناميكيات لعبة نيوتن مع محلل ثابت PVS-Studio

الصورة 1

اكتشفت مؤخرًا على الإنترنت محرك الفيزياء Newton Game Dynamics. مع العلم أن هذه المشاريع عادة ما تحتوي على كمية كبيرة من التعليمات البرمجية المعقدة ، اعتقدت أنه سيكون من المثير للاهتمام التحقق من ذلك مع محلل ثابت PVS-Studio. وقد أثار حماسي كذلك حقيقة أن زميلي أندريه كاربوف اختبر بالفعل هذا المشروع في عام 2014 ، مما يعني أنه فرصة جيدة أيضًا لإثبات تطور محللنا على مدى السنوات الست الماضية. تجدر الإشارة أيضًا إلى أنه في وقت كتابة هذا التقرير ، كان أحدث إصدار من Newton Game Dynamics مؤرخًا في 27 فبراير 2020 ، أي أن هذا المشروع يتطور بنشاط أيضًا على مدى السنوات الست الماضية. وبالتالي ، آمل أنه بالإضافة إلى فريقنا ، ستكون هذه المقالة مفيدة لمطوري المحرك ، الذين سيكونون قادرين على التخلص من بعض الأخطاء وإصلاح التعليمات البرمجية الخاصة بهم.

إخراج محلل


في عام 2014 ، أصدر PVS-Studio:

  • 48 تنبيه من المستوى الأول ؛
  • 79 المستوى الثاني ؛
  • 261 المستوى الثالث.

بالنسبة لعام 2020 ، ومع ذلك:

  • 124 تحذيرات من المستوى الأول ؛
  • 272 المستوى الثاني ؛
  • 787 من المستوى الثالث (من بينها أيضا مثيرة للاهتمام).

هناك تحذيرات أكثر إثارة للاهتمام من مقالة أندري ، لكننا سندرسها بمزيد من التفصيل.

وصف التحذيرات


تحذير N1

V519 يتم تعيين المتغير 'tmp [i] [2]' مرتين على التوالي. ربما هذا خطأ. خطوط التحقق: 468 ، 469. dgCollisionConvexHull.cpp 469

bool dgCollisionConvexHull::Create (dgInt32 count,....)
{
  ....
  dgStack<dgVector> tmp(3 * count);
  for (dgInt32 i = 0; i < count; i ++) 
  {
    tmp[i][0] = dgFloat32 (buffer[i*3 + 0]);
    tmp[i][1] = dgFloat32 (buffer[i*3 + 1]);
    tmp[i][2] = dgFloat32 (buffer[i*3 + 2]);
    tmp[i][2] = dgFloat32 (0.0f);
  }
  ....
}

تتم تهيئة عنصر الصفيف tmp [i] [2] مرتين على التوالي. في معظم الأحيان ، يتحدث مثل هذا الرمز عن النسخ واللصق. يمكنك إصلاح ذلك عن طريق إزالة التهيئة غير الضرورية إذا لم تكن هناك حاجة إليها ، أو عن طريق استبدال فهرس الصفيف بآخر لاحق - وهذا يعتمد بالفعل على قيمة متغير العدد . علاوة على ذلك ، أود أن أصف تحذيرًا آخر V519 ، والذي لم يكن لدى أندريه في المقالة ، ولكن لديه قاعدة بيانات الأخطاء الخاصة بنا :

V519 يتم تعيين قيم الكائن `` الرطب '' مرتين على التوالي. ربما هذا خطأ. 404- صناعة الفيبرجلاس

void dgBody::AddBuoyancyForce (....)
{
  ....
  damp = (m_omega % m_omega) * dgFloat32 (10.0f) *
        fluidAngularViscousity; 
  damp = GetMax (GetMin ((m_omega % m_omega) * 
       dgFloat32 (1000.0f) * 
       fluidAngularViscousity, dgFloat32(0.25f)), 
       dgFloat32(2.0f));
  ....
}

أعترف ، لم أجد هذا الخطأ في سجل المحلل. أيضا، لم أجد في AddBuoyancyForce وظيفة في dgbody.cpp . هذا أمر طبيعي: إذا كانت الأمثلة الجديدة للأخطاء التي تم العثور عليها باستخدام تحذيرات محللنا هي مؤشر على تطور PVS-Studio ، فإن غياب الأخطاء التي تم العثور عليها سابقًا في المشروع هو مؤشر على تطور المشروع.

لا

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

أصدر المحلل تحذيرين لجزء التعليمات البرمجية هذا:

V621 فكر في فحص عامل التشغيل 'for'. من الممكن أن يتم تنفيذ الحلقة بشكل غير صحيح أو لن يتم تنفيذها على الإطلاق. MultiBodyCar.cpp 942

V654 الشرط 'i <count' للحلقة دائمًا خطأ. MultiBodyCar.cpp 942

void MultibodyBodyCar(DemoEntityManager* const scene)
{
  ....
  int count = 10;
  count = 0;
  for (int i = 0; i < count; i++) 
  {
    for (int j = 0; j < count; j++) 
    {
      dMatrix offset(location);
      offset.m_posit += dVector (j * 5.0f + 4.0f, 0.0f, i * 5.0f, 0.0f);
      //manager->CreateSportCar(offset, viperModel.GetData());
      manager->CreateOffRoadCar(offset, monsterTruck.GetData());
    }
  }
  ....
}

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

V519 يتم تعيين قيم المتغير "ret" مرتين على التوالي. ربما يكون هذا خطأً ، تحقق من السطور: 325 ، 326. dString.cpp 326

void dString::LoadFile (FILE* const file)
{
  ....
  size_t ret = fread(m_string, 1, size, file);
  ret = 0;
  ....
}

V519 يتم تعيين قيم المتغير "ret" مرتين على التوالي ، وربما يكون هذا خطأ. خطوط التحقق: 1222 ، 1223. DemoEntityManager.cpp 1223

void DemoEntityManager::DeserializeFile (....)
{
  ....
  size_t ret = fread(buffer, size, 1, (FILE*) serializeHandle);
  ret = 0;
  ....
}

V560 دائمًا ما يكون جزء من التعبير الشرطي صحيحًا: (العدد <10). 726 د

bool dCholeskyWithRegularizer(....)
{
  ....
  int count = 0;
  while (!pass && (count < 10))
  {
    ....
  }
  ....
} 

V654 الشرط 'ptr! = Edge' للحلقة دائمًا خطأ. جابر بن محمد الجابري 1571

void dgPolyhedra::Triangulate (....)
{
  ....
  ptr = edge;
  ....
  while (ptr != edge);
  ....
}

يتم دائمًا إعادة كتابة "عدد" المعلمة V763 في نص الوظيفة قبل استخدامها. ConvexCast.cpp 31

StupidComplexOfConvexShapes (...., int count)
{
  count = 40;
  //count = 1;
  ....
}

V547 تعبير 'axisCount' خطأ دائمًا. MultiBodyCar.cpp 650

void UpdateDriverInput(dVehicle* const vehicle, dFloat timestep) 
{
  ....
  int axisCount = scene->GetJoystickAxis(axis);
  axisCount = 0;
  if (axisCount)
  {
    ....
  }
  ....
}

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

تحذير N2

V769 يساوي مؤشر "النتيجة" في تعبير "النتيجة + i" قيمة nullptr. القيمة الناتجة لا معنى لها ولا يجب استخدامها. 286

GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)
{
  GLFWvidmode* result = NULL;
  ....
  for (i = 0;  i < *count;  i++)
    {
    if (_glfwCompareVideoModes(result + i, &mode) == 0)
      break;
    }
}

المشكلة هي أن النتيجة لم تتغير منذ التهيئة. المؤشر الناتج لن يكون له معنى ؛ لا يمكن استخدامه.

تحذير N3 ، N4 ، N5

V778 تم العثور على جزأين رمز مشابه. ربما ، هذا خطأ مطبعي ويجب استخدام متغير "m_colorChannel" بدلاً من "m_binormalChannel". محمد عبدالله محمد الجابري 1887

void dgMeshEffect::EndBuildFace ()
{
  ....
  if (m_attrib.m_binormalChannel.m_count) <=
  {
    attibutes.m_binormalChannel.
      PushBack(m_attrib.m_binormalChannel[m_constructionIndex + i]);
  }
  if (m_attrib.m_binormalChannel.m_count) <= 
  {
    attibutes.m_colorChannel.
      PushBack(m_attrib.m_colorChannel[m_constructionIndex + i]);
  }
}

يبدو أن المبرمج نسخ شرطين. الثاني يجب أن يبدو مثل هذا:

if (m_attrib.m_colorChannel.m_count) <= 
{
  attibutes.m_colorChannel.
  PushBack(m_attrib.m_colorChannel[m_constructionIndex + i]);
}

تم العثور على خطأ آخر مشابه جدًا أيضًا:

V524 ومن الغريب أن نص دالة 'EnabledAxis1' يكافئ تمامًا نص وظيفة 'EnabledAxis0'. dCustomDoubleHingeActuator.cpp 88

void dCustomDoubleHingeActuator::EnabledAxis0(bool state)
{
  m_axis0Enable = state;  <=
}
void dCustomDoubleHingeActuator::EnabledAxis1(bool state)
{
  m_axis0Enable = state;  <=
}

هنا يجب إصلاح الرمز على النحو التالي:

void dCustomDoubleHingeActuator::EnabledAxis1(bool state)
{
  m_axis1Enable = state;
}

نسخ ولصق آخر:

V525 يحتوي الرمز على مجموعة كتل مماثلة. تحقق من العناصر "m_x" و "m_y" و "m_y" في السطور 73 و 74 و 75. dWoodFracture.cpp 73

WoodVoronoidEffect(....)
{
  ....
  for (int i = 0; i < count; i ++) 
  {
    dFloat x = dGaussianRandom(size.m_x * 0.1f);
    dFloat y = dGaussianRandom(size.m_y * 0.1f);  <=
    dFloat z = dGaussianRandom(size.m_y * 0.1f);  <=
  ....
  }
  ....
}

على الأرجح ، يجب أن تبدو تهيئة المتغير z على النحو التالي :

dFloat z = dGaussianRandom(size.m_z * 0.1f); 

تحذيرات N6 ، N7

في Newton Game Dynamics ، كما هو الحال في أي مشروع كبير C أو C ++ ، كانت هناك بعض التحذيرات حول العمل غير الآمن مع المؤشرات. غالبًا ما يصعب العثور على مثل هذه الأخطاء وتصحيحها ؛ فهي تتسبب في تعطل البرنامج ؛ وبشكل عام ، فهي خطيرة للغاية ولا يمكن التنبؤ بها. لحسن الحظ ، فإن محللنا قادر على اكتشاف العديد من هذه الأخطاء. يبدو من الواضح أنه من الأفضل كتابة شيك مرة واحدة وعدم أخذ حمام بخار ، بدلاً من قضاء الكثير من الوقت في إعادة إظهار المشكلة ، والعثور على مكان المشكلة في الشفرة وتصحيحها. فيما يلي بعض التحذيرات:

V522 قد يكون هناك إشارة إلى "وجه" مؤشر صفري محتمل. 351

DG_INLINE dgMinkFace* dgContactSolver::AddFace(dgInt32 v0,dgInt32 v1,
                                               dgInt32 v2)
{
  dgMinkFace* const face = NewFace();
  face->m_mark = 0; 
  ....
}

تعريف وظيفة NewFace صغير ، لذا سأعطيها بالكامل:

DG_INLINE dgMinkFace* dgContactSolver::NewFace()
{
  dgMinkFace* face = (dgMinkFace*)m_freeFace;
  if (m_freeFace) 
  {
    m_freeFace = m_freeFace->m_next;
  } else 
  {
    face = &m_facePool[m_faceIndex];
    m_faceIndex++;
    if (m_faceIndex >= DG_CONVEX_MINK_MAX_FACES) 
    {
      return NULL;
    }
  }
#ifdef _DEBUG
    memset(face, 0, sizeof (dgMinkFace));
#endif
  return face;
}

تقوم إحدى وظائف نقاط الخروج NewFace بإرجاع القيمة الفارغة (NULL) ، في حالة حدوث إعادة مرجعية للمؤشر فارغة ، وسيكون هناك سلوك غير محدد للبرنامج.

هناك أيضًا تحذير مماثل لقطعة رمز أكثر خطورة:

V522 قد يكون هناك إحالة مرجعية لمؤشر خالٍ محتمل "محيط". عوض عوض علي العاصي 2541

bool dgPolyhedra::PolygonizeFace(....)
{
  ....
  dgEdge* const perimeter = flatFace.AddHalfEdge
                           (edge1->m_next->m_incidentVertex,
                            edge1->m_incidentVertex);
  perimeter->m_twin = edge1;
  ....
}

هنا تعريف AddHalfEdge :

dgEdge* dgPolyhedra::AddHalfEdge (dgInt32 v0, dgInt32 v1)
{
  if (v0 != v1) 
  {
    dgPairKey pairKey (v0, v1);
    dgEdge tmpEdge (v0, -1);
    dgTreeNode* node = Insert (tmpEdge, pairKey.GetVal()); 
    return node ? &node->GetInfo() : NULL;
  } else 
  {
    return NULL;
  }
}

هنا ، NULL هي القيمة المرتجعة عند نقطتين من نقاط الخروج الثلاث المحتملة للدالة.

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

تحذير N8

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

char* const pBits = new char [width * height * 4];
if(pBits == NULL) 
{
  fclose(pFile);
  return 0;
}

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

تحذيرات N9 و N10 و N11

  • V764 ترتيب غير صحيح للوسيطات التي تم تمريرها إلى دالة "CreateWheel": "الارتفاع" و "نصف القطر". StandardJoints.cpp 791
  • V764 ترتيب غير صحيح للوسيطات التي تم تمريرها إلى دالة "CreateWheel": "الارتفاع" و "نصف القطر". StandardJoints.cpp 833
  • V764 ترتيب غير صحيح للوسيطات التي تم تمريرها إلى دالة "CreateWheel": "الارتفاع" و "نصف القطر". StandardJoints.cpp 884

هكذا تبدو مكالمات الوظيفة:

NewtonBody* const wheel = CreateWheel (scene, origin, height, radius);

هكذا يبدو إعلان الوظيفة:

static NewtonBody* CreateWheel (DemoEntityManager* const scene,
  const dVector& location, dFloat radius, dFloat height)

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

تحذيرات N12 ، N13

أصدر المحلل تحذيرات

لطريقتين متشابهتين بأسماء مختلفة: V621 فكر في فحص عامل 'for'. من الممكن أن يتم تنفيذ الحلقة بشكل غير صحيح أو لن يتم تنفيذها على الإطلاق. dgCollisionUserMesh.cpp 161

V621 فكر في فحص عامل التشغيل "for". من الممكن أن يتم تنفيذ الحلقة بشكل غير صحيح أو لن يتم تنفيذها على الإطلاق. 236- د

void dgCollisionUserMesh::GetCollidingFacesContinue
    (dgPolygonMeshDesc* const data) const
{
  ....
  data->m_faceCount = 0; <=
  data->m_userData = m_userData;
  data->m_separationDistance = dgFloat32(0.0f);
  m_collideCallback(&data->m_p0, NULL);
  dgInt32 faceCount0 = 0;
  dgInt32 faceIndexCount0 = 0;
  dgInt32 faceIndexCount1 = 0;
  dgInt32 stride = data->m_vertexStrideInBytes / sizeof(dgFloat32);
  dgFloat32* const vertex = data->m_vertex;
  dgInt32* const address = data->m_meshData.m_globalFaceIndexStart;
  dgFloat32* const hitDistance = data->m_meshData.m_globalHitDistance;
  const dgInt32* const srcIndices = data->m_faceVertexIndex;
  dgInt32* const dstIndices = data->m_globalFaceVertexIndex;
  dgInt32* const faceIndexCountArray = data->m_faceIndexCount;
  for (dgInt32 i = 0; (i < data->m_faceCount)&&
       (faceIndexCount0 < (DG_MAX_COLLIDING_INDICES - 32));
       i++)
  {
    ....
  }
  ....
}
void dgCollisionUserMesh::GetCollidingFacesDescrete
    (dgPolygonMeshDesc* const data) const
{
  ....
  data->m_faceCount = 0; <=  
  data->m_userData = m_userData;
  data->m_separationDistance = dgFloat32(0.0f);
  m_collideCallback(&data->m_p0, NULL);
  dgInt32 faceCount0 = 0;
  dgInt32 faceIndexCount0 = 0;
  dgInt32 faceIndexCount1 = 0;
  dgInt32 stride = data->m_vertexStrideInBytes / sizeof(dgFloat32);
  dgFloat32* const vertex = data->m_vertex;
  dgInt32* const address = data->m_meshData.m_globalFaceIndexStart;
  dgFloat32* const hitDistance = data->m_meshData.m_globalHitDistance;
  const dgInt32* const srcIndices = data->m_faceVertexIndex;
  dgInt32* const dstIndices = data->m_globalFaceVertexIndex;
  dgInt32* const faceIndexCountArray = data->m_faceIndexCount;
  for (dgInt32 i = 0; (i < data->m_faceCount)&&
       (faceIndexCount0 < (DG_MAX_COLLIDING_INDICES - 32));
       i++)
  {
    ....
  }
  ....
}

المشكلة في هذا الجزء من الشرط هي: i <data-> m_faceCount. نظرًا لأن البيانات> m_faceCount يتم تعيينها 0 ، فلن يتم تنفيذ هذه الحلقة مرة واحدة. ربما ، عند كتابة هذا الجزء من التعليمات البرمجية ، نسي المبرمج إعادة تهيئة حقل m_faceCount ، ثم نسخ نص الطريقة ببساطة.

تحذير N14 ، N15

أصدر المحلل تحذيرين لسطور مماثلة من التعليمات البرمجية بالتتابع:

V630 يتم استخدام وظيفة "_alloca" لتخصيص ذاكرة لمجموعة من الكائنات التي هي فئات تحتوي على مُنشئات. dgSkeletonContainer.cpp 1341

V630 يتم استخدام الوظيفة "_alloca" لتخصيص ذاكرة لمجموعة من الكائنات التي هي فئات تحتوي على مُنشئات. 1342

#define alloca _alloca
....
#define dAlloca(type,size) (type*) alloca ((size) * sizeof (type))
....
dgSpatialMatrix::dgSpatialMatrix();
dgSpatialMatrix::dgSpatialMatrix(dgFloat32 val);
....
dgSpatialMatrix* const bodyMassArray = dgAlloca(dgSpatialMatrix,
                                                m_nodeCount);
dgSpatialMatrix* const jointMassArray = dgAlloca(dgSpatialMatrix,
                                                 m_nodeCount); 

تكمن مشكلة جزء التعليمات البرمجية هذا في أنها تعمل مع الذاكرة المخصصة كمصفوفة من الكائنات التي تحتوي على مُنشئ أو مُدمِّر. مع هذا التخصيص للذاكرة للفئة ، لن يتم استدعاء المنشئ. عند تحرير الذاكرة ، لن يتم استدعاء المدمر. هذا أمر مريب للغاية. يمكن أن يؤدي هذا الرمز إلى العمل مع متغيرات غير مهيأة وأخطاء أخرى. بالإضافة إلى ذلك ، مقارنةً بالطريقة التي تستخدم malloc / free ، فإن هذا الرمز سيء لأنه إذا حاولت تخصيص ذاكرة أكبر مما يمكن للجهاز توفيره ، فلن تتلقى رسالة خطأ واضحة. بدلاً من ذلك ، تحصل على خطأ تجزئة عند محاولة الوصول إلى هذه الذاكرة. إليك بعض رسائل المحلل:
  • V630 The '_alloca' function is used to allocate memory for an array of objects which are classes containing constructors. dVehicleSolver.cpp 498
  • V630 The '_alloca' function is used to allocate memory for an array of objects which are classes containing constructors. dVehicleSolver.cpp 499
  • V630 The '_alloca' function is used to allocate memory for an array of objects which are classes containing constructors. dVehicleSolver.cpp 1144
  • 10 .


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



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

All Articles