التحقق من Clang 11 باستخدام PVS-Studio

PVS-Studio: ما زال يستحق!


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



حان الوقت للتحقق مرة أخرى من كود Clang



لأكون صادقًا ، أخذت مقالة " التحقق من مترجم GCC 10 باستخدام PVS-Studio " كأساس لهذا النص . لذلك إذا بدا لك أنك قرأت بالفعل بعض الفقرات في مكان ما ، فأنت لا تعتقد ذلك :).



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



في الواقع ، لا يمكنك مقارنة أجهزة التحليل الساكنة الكلاسيكية مع المجمعين. لا تعتبر أجهزة التحليل الثابتة مجرد بحث عن أخطاء في الكود ، ولكنها أيضًا بنية تحتية متطورة. على سبيل المثال ، هذا هو التكامل مع أنظمة مثل SonarQube و PlatformIO و Azure DevOps و Travis CI و CircleCI و GitLab CI / CD و Jenkins و Visual Studio. هذه آليات متقدمة للقمع الجماعي للتحذيرات ، والتي تتيح لك البدء بسرعة في استخدام PVS-Studio حتى في مشروع قديم كبير. هذا هو توزيع الإخطار. وهلم جرا وهكذا دواليك. ومع ذلك ، لا يزال السؤال الأول هو: "هل يمكن لبرنامج PVS-Studio أن يجد شيئًا لا يستطيع المترجمون إيجاده؟" هذا يعني أننا سنكتب مرارًا وتكرارًا مقالات حول التحقق من هؤلاء المترجمين أنفسهم.



دعنا نعود إلى موضوع التحقق من مشروع Clang. ليست هناك حاجة إلى الإسهاب في هذا المشروع ومعرفة ماهيته. في الواقع ، لم يتم اختبار رمز Clang 11 نفسه فحسب ، بل تم أيضًا اختبار مكتبة LLVM 11 التي بني عليها. من وجهة نظر هذه المقالة ، لا فرق بين وجود عيب في المترجم أو كود المكتبة.



بدا لي كود Clang / LLVM أكثر وضوحًا من كود دول مجلس التعاون الخليجي. على الأقل كل وحدات الماكرو الرهيبة هذه مفقودة ، ويتم استخدام الميزات الحديثة للغة C ++ بنشاط.



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



مثال للاختبارات:



Spaces.SpacesInParentheses = false;               // <=
Spaces.SpacesInCStyleCastParentheses = true;      // <=
verifyFormat("Type *A = ( Type * )P;", Spaces);
verifyFormat("Type *A = ( vector<Type *, int *> )P;", Spaces);
verifyFormat("x = ( int32 )y;", Spaces);
verifyFormat("int a = ( int )(2.0f);", Spaces);
verifyFormat("#define AA(X) sizeof((( X * )NULL)->a)", Spaces);
verifyFormat("my_int a = ( my_int )sizeof(int);", Spaces);
verifyFormat("#define x (( int )-1)", Spaces);

// Run the first set of tests again with:
Spaces.SpacesInParentheses = false;               // <=
Spaces.SpaceInEmptyParentheses = true;
Spaces.SpacesInCStyleCastParentheses = true;      // <=
verifyFormat("call(x, y, z);", Spaces);
verifyFormat("call( );", Spaces);


يحذر المحلل من أن المتغيرات يتم تخصيص نفس القيم التي تحتوي عليها بالفعل:



  • V1048 تم تخصيص نفس القيمة للمتغير "Spaces.SpacesInParentheses". FormatTest.cpp 11554
  • V1048 تم تخصيص نفس القيمة للمتغير "Spaces.SpacesInCStyleCastParentheses". FormatTest.cpp 11556


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



مثال آخر: يُصدر المحلل عددًا كبيرًا من التحذيرات إلى ملف Options.inc الذي يتم إنشاؤه تلقائيًا. يمكنك رؤية "ورقة التعليمات البرمجية" في الملف:



الرمز


ويرسل PVS-Studio تحذيرات لكل هذا:



  • V501 توجد تعبيرات فرعية متطابقة على يمين ويسار عامل التشغيل '==': nullptr == nullptr Options.inc 26
  • V501 توجد تعبيرات فرعية متطابقة إلى يسار ويمين عامل التشغيل '==': nullptr == nullptr Options.inc 27
  • V501 توجد تعبيرات فرعية متطابقة على يسار ويمين عامل التشغيل '==': nullptr == nullptr Options.inc 28
  • وهكذا لكل سطر ...


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



11 مقتطفات شفرة مشبوهة



جزء N1 ، تقسيم modulo بواحد



٪ 1 - تقسيم النموذج على 1


خطأ رائع! انا احب هذا!



void Act() override {
  ....
  // If the value type is a vector, and we allow vector select, then in 50%
  // of the cases generate a vector select.
  if (isa<FixedVectorType>(Val0->getType()) && (getRandom() % 1)) {
    unsigned NumElem =
        cast<FixedVectorType>(Val0->getType())->getNumElements();
    CondTy = FixedVectorType::get(CondTy, NumElem);
  }
  ....
}


تحذير PVS-Studio: V1063 لا معنى للعملية modulo by 1. ستكون النتيجة دائمًا صفرًا. llvm-Stress.cpp 631



تقسيم Modulo يستخدم للحصول على قيمة عشوائية من 0 أو 1. لكن من الواضح أن قيمة 1 هذه مربكة ، ويرتكب الناس النمط الكلاسيكي للخطأ ، الذي يقسم على واحد ، على الرغم من أنه من الضروري القسمة على اثنين. لا معنى للعملية X٪ 1 ، لأن النتيجة دائمًا هي 0 . النسخة الصحيحة من الكود:



if (isa<FixedVectorType>(Val0->getType()) && (getRandom() % 2)) {


تعتبر أداة التشخيص V1063 ، التي ظهرت مؤخرًا في برنامج PVS-Studio ، بسيطة للغاية ، ولكنها تعمل كما ترى.



كما نعلم ، ينظر مطورو المترجمين إلى ما نقوم به ويستعيرون أفضل ممارساتنا. لا يوجد خطأ في هذا. يسعدنا أن PVS-Studio هو محرك التقدم . قمنا بتعيين الوقت بعد ظهور نفس التشخيصات في Clang و GCC :).



جزء N2 ، خطأ مطبعي في الشرط



class ReturnValueSlot {
  ....
  bool isNull() const { return !Addr.isValid(); }
  ....
};

static bool haveSameParameterTypes(ASTContext &Context, const FunctionDecl *F1,
                                   const FunctionDecl *F2, unsigned NumParams) {
  ....
  unsigned I1 = 0, I2 = 0;
  for (unsigned I = 0; I != NumParams; ++I) {
    QualType T1 = NextParam(F1, I1, I == 0);
    QualType T2 = NextParam(F2, I2, I == 0);
    if (!T1.isNull() && !T1.isNull() && !Context.hasSameUnqualifiedType(T1, T2))
      return false;
  }
  return true;
}


تحذير PVS-Studio: V501 توجد تعبيرات فرعية متطابقة إلى يسار ويمين عامل التشغيل "&&" :! T1.isNull () &&! T1.isNull () SemaOverload.cpp 9493 فحص



مرتين ! T1.isNull () . هذا خطأ مطبعي واضح ، ويجب أن يتحقق الجزء الثاني من الشرط من متغير T2 .



جزء N3 ، احتمال خارج حدود المصفوفة



std::vector<Decl *> DeclsLoaded;

SourceLocation ASTReader::getSourceLocationForDeclID(GlobalDeclID ID) {
  ....
  unsigned Index = ID - NUM_PREDEF_DECL_IDS;

  if (Index > DeclsLoaded.size()) {
    Error("declaration ID out-of-range for AST file");
    return SourceLocation();
  }

  if (Decl *D = DeclsLoaded[Index])
    return D->getLocation();
  ....
}


تحذير PVS-Studio: يمكن تجاوز صفيف V557. يشير فهرس "الفهرس" إلى ما وراء حدود الصفيف. ASTReader.cpp 7318



افترض أن الصفيف يحتوي على عنصر واحد ، ومتغير الفهرس واحد أيضًا. الشرط (1> 1) خاطئ ، ونتيجة لذلك سيتم تجاوز المصفوفة. الاختيار الصحيح:



if (Index >= DeclsLoaded.size()) {


جزء N4 ، ترتيب تقييم الحجج



void IHexELFBuilder::addDataSections() {
  ....
  uint32_t SecNo = 1;
  ....
  Section = &Obj->addSection<OwnedDataSection>(
      ".sec" + std::to_string(SecNo++), RecAddr,
      ELF::SHF_ALLOC | ELF::SHF_WRITE, SecNo);
  ....
}


تحذير PVS-Studio: سلوك غير محدد V567. لم يتم تحديد ترتيب تقييم الوسيطة لوظيفة "addSection". ضع في اعتبارك فحص متغير "SecNo". Object.cpp 1223



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



دعني أوضح هذا بمثال اصطناعي:



#include <cstdio>
int main()
{
  int i = 1;
  printf("%d, %d\n", i, i++);
  return 0;
}


اعتمادًا على المترجم ، يمكن طباعة "1 ، 2" و "2 ، 1". باستخدام مستكشف المترجم ، أحصل على النتائج التالية:



  • ينتج برنامج تم تجميعه باستخدام Clang 11.0.0 : 1 ، 1.
  • ينتج برنامج تم تجميعه مع GCC 10.2 : 2 ، 1.


من المثير للاهتمام ، في هذه الحالة البسيطة ، أن مترجم Clang يصدر تحذيرًا:



<source>:6:26: warning:
unsequenced modification and access to 'i' [-Wunsequenced]
printf("%d, %d\n", i, i++);


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



جزء N5 ، إعادة اختبار غريبة



template <class ELFT>
void GNUStyle<ELFT>::printVersionSymbolSection(const ELFFile<ELFT> *Obj,
                                               const Elf_Shdr *Sec) {

  ....
  Expected<StringRef> NameOrErr =
      this->dumper()->getSymbolVersionByIndex(Ndx, IsDefault);
  if (!NameOrErr) {
    if (!NameOrErr) {
      unsigned SecNdx = Sec - &cantFail(Obj->sections()).front();
      this->reportUniqueWarning(createError(
          "unable to get a version for entry " + Twine(I) +
          " of SHT_GNU_versym section with index " + Twine(SecNdx) + ": " +
          toString(NameOrErr.takeError())));
    }
    Versions.emplace_back("<corrupt>");
    continue;
  }
  ....
}


تحذير PVS-Studio: فحص متكرر V571. تم التحقق بالفعل من شرط 'if (! NameOrErr)' في السطر 4666. ELFDumper.cpp 4667



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



Snippet N6 ، الذي يشير إلى مؤشر فارغ محتمل



void RewriteObjCFragileABI::RewriteObjCClassMetaData(
  ObjCImplementationDecl *IDecl, std::string &Result)
{
  ObjCInterfaceDecl *CDecl = IDecl->getClassInterface();

  if (CDecl->isImplicitInterfaceDecl()) {
    RewriteObjCInternalStruct(CDecl, Result);
  }

  unsigned NumIvars = !IDecl->ivar_empty()
  ? IDecl->ivar_size()
  : (CDecl ? CDecl->ivar_size() : 0);
  ....
}


تحذير PVS-Studio: V595 تم استخدام المؤشر "CDecl" قبل أن يتم التحقق منه مقابل nullptr. تحقق من الأسطر: 5275 ، 5284. RewriteObjC.cpp 5275



أثناء الفحص الأول ، يتم دائمًا إلغاء الإشارة إلى مؤشر CDecl بجرأة:



if (CDecl->isImplicitInterfaceDecl())


وفقط من الكود المكتوب أدناه يتضح أن هذا المؤشر يمكن أن يكون فارغًا:



(CDecl ? CDecl->ivar_size() : 0)


على الأرجح ، يجب أن يبدو الفحص الأول كما يلي:



if (CDecl && CDecl->isImplicitInterfaceDecl())


Snippet N7 ، الذي يشير إلى مؤشر فارغ محتمل



bool
Sema::InstantiateClass(....)
{
  ....
  NamedDecl *ND = dyn_cast<NamedDecl>(I->NewDecl);
  CXXRecordDecl *ThisContext =
      dyn_cast_or_null<CXXRecordDecl>(ND->getDeclContext());
  CXXThisScopeRAII ThisScope(*this, ThisContext, Qualifiers(),
                              ND && ND->isCXXInstanceMember());
  ....
}


تحذير PVS-Studio: تم استخدام مؤشر "ND" V595 قبل أن يتم التحقق منه مقابل nullptr. فحص السطور: 2803 ، 2805. SemaTemplateInstantiate.cpp 2803



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



الجزء N8 ، تستمر الوظيفة في التنفيذ على الرغم من حالة الخطأ



bool VerifyObject(llvm::yaml::Node &N,
                  std::map<std::string, std::string> Expected) {
  ....
  auto *V = llvm::dyn_cast_or_null<llvm::yaml::ScalarNode>(Prop.getValue());
  if (!V) {
    ADD_FAILURE() << KS << " is not a string";
    Match = false;
  }
  std::string VS = V->getValue(Tmp).str();
  ....
}


تحذير PVS-Studio: V1004 تم استخدام المؤشر "V" بشكل غير آمن بعد أن تم التحقق منه مقابل nullptr. فحص الأسطر: 61 ، 65. قد يكون TraceTests.cpp 65



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



auto *V = llvm::dyn_cast_or_null<llvm::yaml::ScalarNode>(Prop.getValue());
if (!V) {
  ADD_FAILURE() << KS << " is not a string";
  Match = false;
  return false;
}
std::string VS = V->getValue(Tmp).str();


Snippet N9 ، خطأ مطبعي



const char *tools::SplitDebugName(const ArgList &Args, const InputInfo &Input,
                                  const InputInfo &Output) {
  if (Arg *A = Args.getLastArg(options::OPT_gsplit_dwarf_EQ))
    if (StringRef(A->getValue()) == "single")
      return Args.MakeArgString(Output.getFilename());

  Arg *FinalOutput = Args.getLastArg(options::OPT_o);
  if (FinalOutput && Args.hasArg(options::OPT_c)) {
    SmallString<128> T(FinalOutput->getValue());
    llvm::sys::path::replace_extension(T, "dwo");
    return Args.MakeArgString(T);
  } else {
    // Use the compilation dir.
    SmallString<128> T(
        Args.getLastArgValue(options::OPT_fdebug_compilation_dir));
    SmallString<128> F(llvm::sys::path::stem(Input.getBaseInput()));
    llvm::sys::path::replace_extension(F, "dwo");
    T += F;
    return Args.MakeArgString(F);       // <=
  }
}


تحذير PVS-Studio: V1001 تم تعيين المتغير "T" ولكن لا يتم استخدامه في نهاية الوظيفة. CommonArgs.cpp 873



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



T += F;
return Args.MakeArgString(T);


الجزء N10 ، المقسوم عليه صفر



typedef int32_t si_int;
typedef uint32_t su_int;

typedef union {
  du_int all;
  struct {
#if _YUGA_LITTLE_ENDIAN
    su_int low;
    su_int high;
#else
    su_int high;
    su_int low;
#endif // _YUGA_LITTLE_ENDIAN
  } s;
} udwords;

COMPILER_RT_ABI du_int __udivmoddi4(du_int a, du_int b, du_int *rem) {
  ....
  if (d.s.low == 0) {
    if (d.s.high == 0) {
      // K X
      // ---
      // 0 0
      if (rem)
        *rem = n.s.high % d.s.low;
      return n.s.high / d.s.low;
    }
  ....
}


تحذيرات PVS-Studio:



  • V609 Mod بصفر. المقام 'dslow' == 0.udivmoddi4.c 61
  • V609 قسّم على صفر. المقام 'dslow' == 0.udivmoddi4.c 62


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



جزء N11 ، نسخ ولصق



bool MallocChecker::mayFreeAnyEscapedMemoryOrIsModeledExplicitly(....)
{
  ....
  StringRef FName = II->getName();
  ....
  if (FName == "postEvent" &&
      FD->getQualifiedNameAsString() == "QCoreApplication::postEvent") {
    return true;
  }

  if (FName == "postEvent" &&
      FD->getQualifiedNameAsString() == "QCoreApplication::postEvent") {
    return true;
  }
  ....
}


تحذير PVS-Studio: V581 التعبيرات الشرطية لعبارات "if" الموجودة بجانب بعضها البعض متطابقة. فحص الأسطر: 3108 ، 3113. MallocChecker.cpp 3113



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



خاتمة





اسمح لي أن أذكرك أنه يمكنك استخدام خيار الترخيص المجاني هذا للتحقق من المشاريع مفتوحة المصدر . بالمناسبة ، هناك خيارات أخرى للترخيص المجاني لـ PVS-Studio ، بما في ذلك حتى للمشاريع المغلقة. تم سردها هنا: " خيارات ترخيص PVS-Studio المجانية ". شكرآ لك على أهتمامك.



مقالاتنا الأخرى حول فحص المترجم









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



All Articles