De temps en temps, nous devons écrire des articles sur la vérification de la prochaine version d'un compilateur. Ce n'est pas intéressant. Cependant, comme le montre la pratique, si cela n'est pas fait pendant longtemps, les gens commencent à douter que l'analyseur PVS-Studio mérite le titre d'un bon capteur de bogues et de vulnérabilités potentielles. Peut-être que le nouveau compilateur sait déjà comment faire cela? Oui, les compilateurs ne restent pas immobiles. Cependant, PVS-Studio développe également, démontrant encore et encore la capacité de trouver des erreurs même dans le code de projets de haute qualité tels que les compilateurs.
Il est temps de vérifier le code Clang
Pour être honnête, j'ai pris l'article " Vérification du compilateur GCC 10 à l'aide de PVS-Studio " comme base de ce texte . Donc, s'il vous semble que vous avez déjà lu quelques paragraphes quelque part, alors vous ne pensez pas :).
Ce n'est un secret pour personne que les compilateurs ont leurs propres analyseurs de code statique intégrés et qu'ils sont également en cours de développement. Par conséquent, de temps en temps, nous écrivons des articles sur la façon dont l'analyseur statique PVS-Studio peut trouver des erreurs même à l'intérieur des compilateurs et que nous mangeons du pain pour une raison :).
En fait, vous ne pouvez pas comparer des analyseurs statiques classiques avec des compilateurs. Les analyseurs statiques ne consistent pas seulement à trouver des erreurs dans le code, mais également à développer une infrastructure. Par exemple, il s'agit d'une intégration avec des systèmes tels que SonarQube, PlatformIO, Azure DevOps, Travis CI, CircleCI, GitLab CI / CD, Jenkins, Visual Studio. Ce sont des mécanismes avancés pour la suppression massive des avertissements, qui vous permettent de commencer rapidement à utiliser PVS-Studio même dans un grand projet ancien. Ceci est une distribution de notification. Et ainsi de suite. Cependant, c'est toujours la première question qui est posée: "PVS-Studio peut-il trouver quelque chose que les compilateurs ne peuvent pas trouver?" Cela signifie que nous écrirons encore et encore des articles sur la vérification de ces compilateurs eux-mêmes.
Revenons au sujet de la vérification du projet Clang. Il n'est pas nécessaire de s'attarder sur ce projet et de dire de quoi il s'agit. En fait, non seulement le code de Clang 11 lui-même a été testé, mais aussi la bibliothèque LLVM 11 sur laquelle il est construit. Du point de vue de cet article, cela ne fait aucune différence si un défaut est détecté dans le code du compilateur ou de la bibliothèque.
Le code Clang / LLVM m'a semblé beaucoup plus clair que le code GCC. Au moins, toutes ces macros terribles manquent et les fonctionnalités modernes du langage C ++ sont activement utilisées.
Malgré cela, le projet est volumineux, et sans réglages préliminaires de l'analyseur, il est encore très fastidieux de visualiser le rapport. Fondamentalement, les demi-faux positifs interfèrent. Par semi-faux positifs, j'entends des situations où l'analyseur a formellement raison, mais il n'y a aucun sens à avertir. Par exemple, un grand nombre de ces positifs sont émis pour les tests unitaires et le code généré.
Exemple de tests:
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);
L'analyseur avertit que les variables reçoivent les mêmes valeurs qu'elles contiennent déjà:
- V1048 La variable «Spaces.SpacesInParentheses» a reçu la même valeur. FormatTest.cpp 11554
- V1048 La variable «Spaces.SpacesInCStyleCastParentheses» a reçu la même valeur. FormatTest.cpp 11556
Formellement, l'analyseur a renvoyé la réponse correcte, et c'est le fragment de code qui doit être simplifié ou corrigé. En même temps, il est clair qu'en fait tout va bien et qu'il ne sert à rien d'éditer quelque chose non plus.
Autre exemple: l'analyseur émet un grand nombre d'avertissements dans le fichier Options.inc généré automatiquement. Vous pouvez voir la "feuille de code" dans le fichier:
Et PVS-Studio envoie des avertissements à tout cela:
- V501 Il existe des sous-expressions identiques à gauche et à droite de l'opérateur '==': nullptr == nullptr Options.inc 26
- V501 Il existe des sous-expressions identiques à gauche et à droite de l'opérateur '==': nullptr == nullptr Options.inc 27
- V501 Il existe des sous-expressions identiques à gauche et à droite de l'opérateur '==': nullptr == nullptr Options.inc 28
- et ainsi de suite pour chaque ligne ...
Tout cela n'est pas effrayant. Tout cela peut être vaincu: désactivez la vérification des fichiers inutiles, annotez certaines macros et fonctions, supprimez certains types d'alarmes, etc. C'est possible, mais faire cela dans le cadre de la rédaction d'un article n'est pas intéressant. Par conséquent, j'ai fait exactement la même chose que dans l'article sur le compilateur GCC. J'ai étudié le rapport jusqu'à ce que j'aie 11 exemples de code intéressants pour écrire un article. Pourquoi 11? Je pensais que puisque la version Clang est 11, alors laissez les fragments être 11 :).
11 extraits de code suspects
Fragment N1, division modulo par un
Cool erreur! Je les adore!
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);
}
....
}
Avertissement PVS-Studio: V1063 L'opération modulo par 1 n'a pas de sens. Le résultat sera toujours nul. llvm-stress.cpp 631 La
division Modulo est utilisée pour obtenir une valeur aléatoire de 0 ou 1. Mais, apparemment, cette valeur de 1 est déroutante et les gens font le modèle d'erreur classique, en divisant par un, bien qu'il soit nécessaire de diviser par deux. L'opération X% 1 n'a pas de sens, car le résultat est toujours 0 . Version correcte du code:
if (isa<FixedVectorType>(Val0->getType()) && (getRandom() % 2)) {
Diagnostics V1063, qui est récemment apparu dans PVS-Studio, est scandaleusement simple, mais, comme vous pouvez le voir, cela fonctionne.
Comme nous le savons, les développeurs de compilateurs examinent ce que nous faisons et empruntent nos meilleures pratiques. Il n'y a rien de mal à cela. Nous sommes heureux que PVS-Studio soit le moteur du progrès . Nous définissons l'heure après laquelle les mêmes diagnostics apparaîtront dans Clang et GCC :).
Fragment N2, faute de frappe dans l'état
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;
}
Avertissement PVS-Studio: V501 Il existe des sous-expressions identiques à gauche et à droite de l'opérateur '&&':! T1.isNull () &&! T1.isNull () SemaOverload.cpp 9493 Vérification
deux fois ! T1.isNull () . Il s'agit d'une faute de frappe évidente, et la deuxième partie de la condition doit vérifier la variable T2 .
Fragment N3, potentiel hors limites du tableau
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();
....
}
Avertissement PVS-Studio: Un dépassement de la baie V557 est possible. L'index «Index» pointe au-delà de la limite du tableau. ASTReader.cpp 7318
Supposons que le tableau contienne un élément et que la variable Index en soit également un. La condition (1> 1) est fausse, et par conséquent le tableau sera dépassé. Vérification correcte:
if (Index >= DeclsLoaded.size()) {
Fragment N4, l'ordre d'évaluation des arguments
void IHexELFBuilder::addDataSections() {
....
uint32_t SecNo = 1;
....
Section = &Obj->addSection<OwnedDataSection>(
".sec" + std::to_string(SecNo++), RecAddr,
ELF::SHF_ALLOC | ELF::SHF_WRITE, SecNo);
....
}
Avertissement PVS-Studio: V567 Comportement non spécifié. L'ordre d'évaluation des arguments n'est pas défini pour la fonction 'addSection'. Pensez à inspecter la variable «SecNo». Object.cpp 1223
Notez que l'argument SecNo est utilisé deux fois et incrémenté en cours de route. Il est tout simplement impossible de dire dans quel ordre les arguments seront évalués. Par conséquent, le résultat varie en fonction de la version du compilateur ou des paramètres de compilation.
Laissez-moi vous expliquer cela avec un exemple synthétique:
#include <cstdio>
int main()
{
int i = 1;
printf("%d, %d\n", i, i++);
return 0;
}
Selon le compilateur, «1, 2» et «2, 1» peuvent être imprimés. En utilisant l'explorateur du compilateur, j'obtiens les résultats suivants:
- un programme compilé avec Clang 11.0.0 produit : 1, 1.
- un programme compilé avec GCC 10.2 produit : 2, 1.
Fait intéressant, pour ce cas simple, le compilateur Clang émet un avertissement:
<source>:6:26: warning:
unsequenced modification and access to 'i' [-Wunsequenced]
printf("%d, %d\n", i, i++);
Apparemment, dans des conditions réelles, cet avertissement n'a pas aidé. Les diagnostics sont soit désactivés, car cela n'est pas pratique pour une utilisation pratique, soit le compilateur n'a pas pu avertir d'un cas plus complexe.
Fragment N5, étrange retest
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;
}
....
}
Avertissement PVS-Studio: V571 Contrôle récurrent. La condition 'if (! NameOrErr)' a déjà été vérifiée à la ligne 4666. ELFDumper.cpp 4667
Le deuxième contrôle duplique le premier et est redondant. Peut-être que la deuxième vérification peut simplement être supprimée. Mais il s'agit très probablement d'une faute de frappe, et une variable différente doit être utilisée dans la deuxième condition.
Extrait N6, déréférencement d'un pointeur potentiellement nul
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);
....
}
Avertissement PVS-Studio: V595 Le pointeur 'CDecl' a été utilisé avant d'être vérifié par rapport à nullptr. Lignes de contrôle: 5275, 5284. RewriteObjC.cpp 5275
Lors de la première vérification, le pointeur CDecl est toujours déréférencé en gras :
if (CDecl->isImplicitInterfaceDecl())
Et seulement à partir du code écrit ci-dessous, il devient clair que ce pointeur peut être nul:
(CDecl ? CDecl->ivar_size() : 0)
Très probablement, la première vérification devrait ressembler à ceci:
if (CDecl && CDecl->isImplicitInterfaceDecl())
Extrait N7, déréférencement d'un pointeur potentiellement nul
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());
....
}
Avertissement PVS-Studio: V595 Le pointeur 'ND' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 2803, 2805. SemaTemplateInstantiate.cpp 2803
Une variation de l'erreur précédente. Il est dangereux de déréférencer un pointeur sans le vérifier au préalable si sa valeur est obtenue à l'aide d'une conversion dynamique. De plus, à partir du code ci-dessous, vous pouvez voir qu'une telle vérification est nécessaire.
Fragment N8, la fonction continue l'exécution malgré un état d'erreur
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();
....
}
Avertissement PVS-Studio: V1004 Le pointeur 'V' a été utilisé de manière non sécurisée après avoir été vérifié par rapport à nullptr. Vérifiez les lignes: 61, 65. TraceTests.cpp 65 Le
pointeur V peut être nul. Il s'agit clairement d'une condition erronée, qui est même signalée. Cependant, après cela, la fonction continue son exécution comme si de rien n'était, ce qui entraînera un déréférencement de ce pointeur très nul. Peut-être ont-ils oublié d'interrompre la fonction, et l'option correcte devrait ressembler à ceci:
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();
Extrait N9, faute de frappe
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); // <=
}
}
Avertissement PVS-Studio: V1001 La variable 'T' est affectée mais n'est pas utilisée à la fin de la fonction. CommonArgs.cpp 873
Notez la fin de la fonction. La variable locale T est modifiée mais n'est en aucun cas utilisée. Très probablement, il s'agit d'une faute de frappe et la fonction doit se terminer par les lignes de code suivantes:
T += F;
return Args.MakeArgString(T);
Fragment N10, le diviseur est nul
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;
}
....
}
Avertissements PVS-Studio:
- Mod V609 par zéro. Dénominateur 'dslow' == 0.udivmoddi4.c 61
- V609 Diviser par zéro. Dénominateur 'dslow' == 0.udivmoddi4.c 62
Je ne sais pas si c'est une erreur ou une idée intelligente, mais le code est très étrange. Il existe deux variables entières ordinaires, et l'une est divisible par l'autre. Fait intéressant, cela ne se produit que si les deux variables sont nulles. Qu'est-ce que tout cela signifie?
Fragment N11, copier-coller
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;
}
....
}
Avertissement PVS-Studio: V581 Les expressions conditionnelles des instructions 'if' situées côte à côte sont identiques. Vérifiez les lignes: 3108, 3113. MallocChecker.cpp 3113
L'extrait de code a été copié, mais n'a été modifié d'aucune façon. Le deuxième extrait de code doit être supprimé ou modifié pour commencer à effectuer une vérification utile.
Conclusion
Laissez-moi vous rappeler que vous pouvez utiliser cette option de licence gratuite pour vérifier les projets open source . À propos, il existe d'autres options pour la licence gratuite de PVS-Studio, y compris même pour les projets fermés. Ils sont listés ici: " Options de licence PVS-Studio gratuites ". Merci de votre attention.
Nos autres articles sur la vérification du compilateur
- Analyse LLVM (Clang) (août 2011), deuxième analyse (août 2012), troisième analyse (octobre 2016), quatrième analyse (avril 2019)
- Analyse GCC (août 2016), deuxième analyse (avril 2020)
- Analyse du compilateur Huawei Ark (décembre 2019)
- Analyse .NET Compiler Platform («Roslyn») (décembre 2015), deuxième analyse (avril 2019)
- Analyse des analyseurs Roslyn (août 2019)
- Analyse PascalABC.NET (mars 2017)
Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien de traduction: Andrey Karpov. Vérification de Clang 11 avec PVS-Studio .