
يتم إنشاء التطبيقات الحديثة من مكتبات تابعة لجهات خارجية مثل وحدات البناء. هذا أمر طبيعي والخيار الوحيد لإكمال المشروع في فترة زمنية معقولة وبميزانية معقولة. ومع ذلك ، فإن أخذ كل الطوب دون تمييز قد لا يكون فكرة جيدة. إذا كان هناك العديد من الخيارات ، فمن المفيد أن تأخذ الوقت الكافي لتحليل المكتبات المفتوحة من أجل اختيار أعلى جودة.
مجموعة "مكتبات C ++ رائعة للرأس فقط"
بدأت قصة هذه الكتابة مع بودكاست Cppcast " Cross Platform Mobile Telephony ". من خلاله ، علمت بوجود قائمة " awesome-hpp " ، والتي تسرد عددًا كبيرًا من مكتبات C ++ المفتوحة ، والتي تتكون فقط من ملفات الرأس.
هذه القائمة اهتمت بي لسببين. أولاً ، إنها فرصة لتجديد قاعدة المشاريع لاختبار محلل PVS-Studio الخاص بنا على الكود الحديث. تمت كتابة العديد من المشاريع في C ++ 11 و C ++ 14 و C ++ 17. ثانيًا ، إنها فرصة لكتابة مقال حول التحقق من هذه المشاريع.
المشاريع صغيرة ، لذلك هناك عدد قليل من الأخطاء في كل منها على حدة. بالإضافة إلى ذلك ، هناك القليل من التحذيرات ، لأن يمكن اكتشاف بعض الأخطاء فقط إذا تم إنشاء فئات أو وظائف القالب في رمز مخصص. حتى يتم استخدام هذه الفئات والوظائف ، غالبًا ما يكون من المستحيل معرفة ما إذا كان هناك خطأ أم لا. ومع ذلك ، في المجموع ، كان هناك الكثير من الأخطاء ، وسأكتب عنها في المقالة التالية. هذه المقالة ليست عن الأخطاء ، ولكن عن التحذير.
لماذا تحلل
باستخدام مكتبات الجهات الخارجية ، فأنت تثق بهم دون قيد أو شرط للقيام ببعض الأعمال والحسابات. يكمن الخطر في أن المبرمجين في بعض الأحيان يختارون مكتبة دون التفكير في أن الأخطاء قد لا تحتوي فقط على التعليمات البرمجية الخاصة بهم ، ولكن أيضًا على رمز المكتبة نفسها. نتيجة لذلك ، هناك أخطاء غير واضحة وغير مفهومة يمكن أن تظهر بأكثر الطرق غير المتوقعة.
إن كود المكتبات مفتوحة المصدر المعروفة مصححة جيدًا ، واحتمال مواجهة خطأ هناك أقل بكثير مما هو عليه في الكود المشابه الذي كتبته بنفسك. المشكلة هي أنه لا يتم استخدام جميع المكتبات وتصحيح أخطائها على نطاق واسع. وهذا هو المكان الذي تبرز فيه مسألة تقييم جودتها.
لتوضيح الأمر ، دعنا نلقي نظرة على مثال. لنأخذ مكتبة JSONCONS .
JSONCONS هي مكتبة C ++ ، تحتوي على رأس فقط لإنشاء تنسيقات بيانات JSON و JSON مثل CBOR.مكتبة محددة لمهام محددة. قد يعمل بشكل جيد بشكل عام ، ولن ترى أخطاء فيه أبدًا. لكن لا قدر الله تحتاج إلى استخدام هذا المشغل المحمّل << = .
static constexpr uint64_t basic_type_bits = sizeof(uint64_t) * 8;
....
uint64_t* data()
{
return is_dynamic() ? dynamic_stor_.data_ : short_stor_.values_;
}
....
basic_bigint& operator<<=( uint64_t k )
{
size_type q = (size_type)(k / basic_type_bits);
if ( q ) // Increase common_stor_.length_ by q:
{
resize(length() + q);
for (size_type i = length(); i-- > 0; )
data()[i] = ( i < q ? 0 : data()[i - q]);
k %= basic_type_bits;
}
if ( k ) // 0 < k < basic_type_bits:
{
uint64_t k1 = basic_type_bits - k;
uint64_t mask = (1 << k) - 1; // <=
resize( length() + 1 );
for (size_type i = length(); i-- > 0; )
{
data()[i] <<= k;
if ( i > 0 )
data()[i] |= (data()[i-1] >> k1) & mask;
}
}
reduce();
return *this;
}
تحذير محلل PVS-Studio: V629 ضع في اعتبارك فحص التعبير "1 << k". تحويل البت لقيمة 32 بت مع التوسيع اللاحق لنوع 64 بت. bigint.hpp 744
كما أفهمها ، تعمل الوظيفة بأرقام كبيرة مخزنة كمصفوفة من عناصر 64 بت. للعمل مع وحدات بت معينة ، تحتاج إلى تكوين قناع 64 بت:
uint64_t mask = (1 << k) - 1;
لكن هذا القناع لم يتشكل بشكل صحيح. نظرًا لأن الحرف الرقمي 1 من النوع int ، فإن إزاحته بأكثر من 31 بتًا سيؤدي إلى سلوك غير محدد.
من المعيار:يمكن أن يكون القناع المتغير أي شيء تريده. نعم ، أعلم ، نظريًا يمكن أن يحدث أي شيء بسبب UB. لكن في الممارسة العملية ، على الأرجح ، نحن نتحدث عن نتيجة تعبير غير صحيحة.
تعبير التحول << التعبير الإضافي
...
2. قيمة E1 << E2 هي مواضع E2 بت إزاحة يسار E1 ؛ البتات الفارغة مملوءة بصفر. إذا كان E1 يحتوي على نوع غير موقع ، فإن قيمة النتيجة هي E1 * 2 ^ E2 ، وحدة نمطية مخفضة واحدة أكثر من القيمة القصوى التي يمكن تمثيلها في نوع النتيجة. خلاف ذلك ، إذا كان E1 يحتوي على نوع موقّع وقيمة غير سالبة ، وكان E1 * 2 ^ E2 يمكن تمثيله في نوع النتيجة ، فهذه هي القيمة الناتجة ؛ خلاف ذلك ، السلوك غير محدد.
إذن ، لدينا دالة لا يمكن استخدامها. بدلاً من ذلك ، ستعمل فقط مع بعض الحالات الخاصة لقيمة وسيطة الإدخال. هذا فخ محتمل يمكن أن يقع فيه المبرمج. يمكن للبرنامج تشغيل واجتياز العديد من الاختبارات ، ثم رفض المستخدم بشكل غير متوقع في ملفات الإدخال الأخرى.
ويمكن رؤية خطأ آخر في المشغل >> = .
سؤال بلاغي. هل يجب أن تثق بهذه المكتبة؟
ربما يستحق ذلك. بعد كل شيء ، هناك أخطاء في أي مشروع. ومع ذلك ، يجدر التفكير: في حالة وجود هذه الأخطاء ، فهل هناك أخطاء أخرى يمكن أن تؤدي إلى تلف البيانات السيئ؟ ألن يكون من الأفضل إعطاء الأفضلية للمكتبة الأكثر شعبية / المختبرة إذا كان هناك العديد منها؟
مثال غير مقنع؟ حسنًا ، لنحضر واحدة أخرى. لنأخذ مكتبة الرياضيات العالمية . من المتوقع أن توفر المكتبة القدرة على العمل مع النواقل. على سبيل المثال ، قم بضرب متجه وقسمته على قيمة عددية. حسنًا ، دعنا نرى كيف يتم تنفيذ هذه العمليات. عمليه الضرب:
template<typename Scalar>
vector<Scalar> operator*(double scalar, const vector<Scalar>& v) {
vector<Scalar> scaledVector(v);
scaledVector *= scalar;
return v;
}
تحذير محلل PVS-Studio: V1001 تم تعيين متغير "scaledVector" ولكن لا يتم استخدامه في نهاية الوظيفة. vector.hpp 124
نظرًا لوجود خطأ مطبعي ، لم يتم إرجاع الحاوية الجديدة التي تم تحجيمها ، ولكن المتجه الأصلي. نفس الخطأ في عامل القسمة. الوجه.
مرة أخرى ، هذه الأخطاء لا تعني أي شيء بشكل منفصل. على الرغم من الجواب ، فهذه إشارة إلى أن هذه المكتبة غير مستغلة بشكل كافٍ وهناك احتمال كبير لوجود أخطاء خطيرة أخرى غير ملحوظة فيها.
انتاج. إذا كانت العديد من المكتبات توفر نفس الوظيفة ، فمن المفيد إجراء تحليل أولي لجودتها واختيار أكثرها اختبارًا وموثوقية.
كيف تحلل
حسنًا ، نريد أن نفهم جودة كود المكتبات ، لكن كيف نفعل ذلك؟ نعم ، هذا ليس بالأمر السهل. لا يمكنك الذهاب ورؤية الكود. بدلاً من ذلك ، يمكنك أن تنظر إلى شيء ما ، لكنها ستعطيك القليل من المعلومات. علاوة على ذلك ، من غير المرجح أن تساعد مثل هذه المراجعة في تقييم كثافة الأخطاء في المشروع.
دعنا نعود إلى مكتبة الرياضيات العالمية المذكورة سابقًا. حاول العثور على الخطأ في رمز هذه الوظيفة. في الواقع ، عند رؤية التعليق المصاحب ، لا يمكنني تجاوز هذا المكان :).
// subtract module using SUBTRACTOR: CURRENTLY BROKEN FOR UNKNOWN REASON

template<size_t fbits, size_t abits>
void module_subtract_BROKEN(const value<fbits>& lhs, const value<fbits>& rhs,
value<abits + 1>& result) {
if (lhs.isinf() || rhs.isinf()) {
result.setinf();
return;
}
int lhs_scale = lhs.scale(),
rhs_scale = rhs.scale(),
scale_of_result = std::max(lhs_scale, rhs_scale);
// align the fractions
bitblock<abits> r1 = lhs.template nshift<abits>(lhs_scale-scale_of_result+3);
bitblock<abits> r2 = rhs.template nshift<abits>(rhs_scale-scale_of_result+3);
bool r1_sign = lhs.sign(), r2_sign = rhs.sign();
if (r1_sign) r1 = twos_complement(r1);
if (r1_sign) r2 = twos_complement(r2);
if (_trace_value_sub) {
std::cout << (r1_sign ? "sign -1" : "sign 1") << " scale "
<< std::setw(3) << scale_of_result << " r1 " << r1 << std::endl;
std::cout << (r2_sign ? "sign -1" : "sign 1") << " scale "
<< std::setw(3) << scale_of_result << " r2 " << r2 << std::endl;
}
bitblock<abits + 1> difference;
const bool borrow = subtract_unsigned(r1, r2, difference);
if (_trace_value_sub) std::cout << (r1_sign ? "sign -1" : "sign 1")
<< " borrow" << std::setw(3) << (borrow ? 1 : 0) << " diff "
<< difference << std::endl;
long shift = 0;
if (borrow) { // we have a negative value result
difference = twos_complement(difference);
}
// find hidden bit
for (int i = abits - 1; i >= 0 && difference[i]; i--) {
shift++;
}
assert(shift >= -1);
if (shift >= long(abits)) { // we have actual 0
difference.reset();
result.set(false, 0, difference, true, false, false);
return;
}
scale_of_result -= shift;
const int hpos = abits - 1 - shift; // position of the hidden bit
difference <<= abits - hpos + 1;
if (_trace_value_sub) std::cout << (borrow ? "sign -1" : "sign 1")
<< " scale " << std::setw(3) << scale_of_result << " result "
<< difference << std::endl;
result.set(borrow, scale_of_result, difference, false, false, false);
}
أنا متأكد ، على الرغم من حقيقة أنني اقترحت وجود خطأ في هذا الرمز ، إلا أنه ليس من السهل العثور عليه.
إذا لم يتم العثور عليها ، فهذه هي. تحذير PVS-Studio: V581 التعبيرات الشرطية لعبارات "if" الموجودة بجانب بعضها البعض متطابقة. فحص الأسطر: 789 ، 790. value.hpp 790
if (r1_sign) r1 = twos_complement(r1);
if (r1_sign) r2 = twos_complement(r2);
خطأ مطبعي كلاسيكي. في الحالة الثانية ، يجب فحص متغير r2_sign .
بشكل عام ، يمكنك نسيان مراجعة الكود "اليدوية". نعم ، هذا المسار ممكن ، لكنه يستغرق وقتًا طويلاً بشكل غير معقول.
ماذا أقترح؟ بسيط جدا. استخدم تحليل الكود الثابت .
تحقق من المكتبات التي تنوي استخدامها. ابدأ في الاطلاع على التقارير وسيصبح كل شيء واضحًا بسرعة كافية.
لا تحتاج حتى إلى تحليل عميق وشامل ، ولا تحتاج إلى تصفية الإيجابيات الخاطئة. عليك فقط مراجعة التقرير وفحص التحذيرات. يمكن للإيجابيات الكاذبة بسبب نقص الإعدادات ببساطة التحلي بالصبر والتركيز على الأخطاء.
ومع ذلك ، يمكن أيضًا أخذ الإيجابيات الخاطئة في الاعتبار بشكل غير مباشر. كلما زاد عددها ، زادت فوضى الكود. بمعنى آخر ، هناك العديد من الحيل في الشفرة التي تربك المحلل. كما أنها تربك الأشخاص الذين يدعمون المشروع ، ونتيجة لذلك تؤثر سلبًا على جودته.
ملحوظة. لا تنس حجم المشروع. سيحتوي المشروع الكبير دائمًا على المزيد من الأخطاء. لكن عدد الأخطاء لا يتطابق على الإطلاق مع كثافة الخطأ. ضع في اعتبارك هذا عند أخذ مشاريع بأحجام مختلفة وإجراء التعديلات.
ماذا تستخدم
هناك العديد من أدوات تحليل التعليمات البرمجية الثابتة. أقترح بشكل طبيعي استخدام محلل PVS-Studio . إنه أمر رائع لكل من التقييم لمرة واحدة لجودة الشفرة والبحث المنتظم وإصلاح الأخطاء.
يمكنك التحقق من كود المشاريع في C و C ++ و C # و Java. المنتج ملكية. ومع ذلك ، سيكون الترخيص التجريبي المجاني أكثر من كافٍ لتقييم جودة العديد من المكتبات مفتوحة المصدر.
أذكرك أيضًا أن هناك العديد من الخيارات للترخيص المجاني للمحلل من أجل:
- الطلاب .
- مشاريع مفتوحة المصدر .
- المشاريع المغلقة (تحتاج إلى إضافة تعليقات خاصة إلى الكود) ؛
- مايكروسوفت MVP .
خاتمة
لا يزال العديد من المبرمجين يستخف بمنهجية تحليل الشفرة الثابتة بشكل غير مستحق. أحد الأسباب المحتملة لذلك هو تجربة أدوات "لينتر" البسيطة المزعجة ، والتي تقوم بإجراء فحوصات بسيطة للغاية ، ولسوء الحظ ، غالبًا ما تكون غير مفيدة للغاية.
بالنسبة لأولئك الذين يشكون فيما إذا كان الأمر يستحق محاولة تطبيق محلل ثابت في عملية التطوير ، فإن المنشورين التاليين:
- كيفية تنفيذ محلل كود ثابت في مشروع قديم وعدم تثبيط الفريق .
- أسباب إدخال محلل الكود الثابت PVS-Studio في عملية التطوير .
شكرًا لك على اهتمامك ، وأتمنى لك عددًا أقل من الأخطاء في التعليمات البرمجية الخاصة بك وفي كود المكتبات المستخدمة :).

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