CMake est un système multiplateforme pour automatiser les builds de projet. Ce système est beaucoup plus ancien que l'analyseur de code statique PVS-Studio, mais personne n'a essayé d'appliquer l'analyseur sur son code et de revoir les erreurs. En fait, il y en a beaucoup. Le public de CMake est énorme. De nouveaux projets y démarrent et les anciens sont portés. Je frémis en pensant au nombre de développeurs qui auraient pu avoir une erreur donnée.
Présentation
CMake est un système multiplateforme pour automatiser la création de logiciels à partir du code source. CMake n'est pas destiné directement à la construction, il génère uniquement des fichiers pour contrôler une construction à partir des fichiers CMakeLists.txt. La première version du programme a eu lieu en 2000. À titre de comparaison, l'analyseur PVS-Studio n'est apparu qu'en 2008. À cette époque, il visait à rechercher les bogues résultant du portage de systèmes 32 bits vers des systèmes 64 bits. En 2010, le premier ensemble de diagnostics à usage général est apparu (V501-
V545 ). Par ailleurs, le code CMake a quelques avertissements de ce premier ensemble.
Erreurs impardonnables
V1040 Typo possible dans l'orthographe d'un nom de macro prédéfini. La macro '__MINGW32_' est similaire à '__MINGW32__'. winapi.h 4112
#if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32_) #define __UNICODE_STRING_DEFINED #endif
Le diagnostic
V1040 a été implanté il n'y a pas si longtemps. Très probablement, au moment de publier l'article, il ne sera pas encore publié, néanmoins, nous avons déjà trouvé une erreur sympa avec son aide.
Il y a une faute de frappe faite dans le nom
__MINGW32_ . À la fin, un caractère de soulignement est manquant. Si vous recherchez le code avec ce nom, vous pouvez voir que la version avec deux caractères de soulignement des deux côtés est utilisée dans le projet:
V531 Il est étrange qu'un opérateur sizeof () soit multiplié par sizeof (). cmGlobalVisualStudioGenerator.cxx 558
bool IsVisualStudioMacrosFileRegistered(const std::string& macrosFile, const std::string& regKeyBase, std::string& nextAvailableSubKeyName) { .... if (ERROR_SUCCESS == result) { wchar_t subkeyname[256];
Pour un tableau statiquement déclaré, l'opérateur
sizeof calculera la taille en octets, en tenant compte du nombre d'éléments et de leur taille. Lors de l'évaluation de la valeur de la variable
cch_subkeyname , un développeur ne l'a pas prise en compte et a obtenu une valeur 4 fois supérieure à celle prévue. Expliquons d'où viennent "quatre fois".
Le tableau et sa taille incorrecte sont passés à la fonction
RegEnumKeyExW :
LSTATUS RegEnumKeyExW( HKEY hKey, DWORD dwIndex, LPWSTR lpName,
Le pointeur
lpcchName doit pointer vers la variable, contenant la taille du tampon en caractères: "Un pointeur vers une variable qui spécifie la taille du tampon spécifié par le paramètre
lpClass , en caractères". La taille du tableau de sous-
clés est de 512 octets et peut stocker 256 caractères du type
wchar_t (sous Windows, wchar_t est de 2 octets). C'est 256 qui doit être passé à la fonction. Au lieu de cela, 512 est multiplié par 2 et nous obtenons 1024.
Je pense, il est clair maintenant comment corriger cette erreur. Vous devez utiliser la division au lieu de la multiplication:
DWORD cch_subkeyname = sizeof(subkeyname) / sizeof(subkeyname[0]);
Par ailleurs, la même erreur se produit lors de l'évaluation de la valeur de la variable
cch_keyclass .
L'erreur décrite peut potentiellement entraîner un débordement de tampon. Tous ces fragments doivent définitivement être corrigés:
- V531 Il est étrange qu'un opérateur sizeof () soit multiplié par sizeof (). cmGlobalVisualStudioGenerator.cxx 556
- V531 Il est étrange qu'un opérateur sizeof () soit multiplié par sizeof (). cmGlobalVisualStudioGenerator.cxx 572
- V531 Il est étrange qu'un opérateur sizeof () soit multiplié par sizeof (). cmGlobalVisualStudioGenerator.cxx 621
- V531 Il est étrange qu'un opérateur sizeof () soit multiplié par sizeof (). cmGlobalVisualStudioGenerator.cxx 622
- V531 Il est étrange qu'un opérateur sizeof () soit multiplié par sizeof (). cmGlobalVisualStudioGenerator.cxx 649
V595 Le
pointeur 'this-> BuildFileStream' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 133, 134. cmMakefileTargetGenerator.cxx 133
void cmMakefileTargetGenerator::CreateRuleFile() { .... this->BuildFileStream->SetCopyIfDifferent(true); if (!this->BuildFileStream) { return; } .... }
Le pointeur
this-> BuildFileStream est déréférencé juste avant la vérification de sa validité. Cela n'a-t-il pas posé de problème à personne? Ci-dessous, il y a un autre exemple d'un tel extrait. Il est fait comme une copie carbone. Mais en fait, il existe de
nombreux avertissements
V595 et la plupart ne sont pas si évidents. D'après mon expérience, je peux dire que la correction des avertissements de ce diagnostic prend le plus de temps.
- V595 Le pointeur 'this-> FlagFileStream' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 303, 304. cmMakefileTargetGenerator.cxx 303
V614 Pointeur
non initialisé 'str' utilisé. cmVSSetupHelper.h 80
class SmartBSTR { public: SmartBSTR() { str = NULL; } SmartBSTR(const SmartBSTR& src) { if (src.str != NULL) { str = ::SysAllocStringByteLen((char*)str, ::SysStringByteLen(str)); } else { str = ::SysAllocStringByteLen(NULL, 0); } } .... private: BSTR str; };
L'analyseur a détecté l'utilisation du pointeur
str non initialisé. Il est apparu en raison d'une faute de frappe ordinaire. Lors de l'appel de la fonction
SysAllocStringByteLen , on aurait dû utiliser le pointeur
src.str .
V557 Le dépassement de matrice est possible. La valeur de l'indice 'lensymbol' pourrait atteindre 28. archive_read_support_format_rar.c 2749
static int64_t expand(struct archive_read *a, int64_t end) { .... if ((lensymbol = read_next_symbol(a, &rar->lengthcode)) < 0) goto bad_data; if (lensymbol > (int)(sizeof(lengthbases)/sizeof(lengthbases[0]))) goto bad_data; if (lensymbol > (int)(sizeof(lengthbits)/sizeof(lengthbits[0]))) goto bad_data; len = lengthbases[lensymbol] + 2; if (lengthbits[lensymbol] > 0) { if (!rar_br_read_ahead(a, br, lengthbits[lensymbol])) goto truncated_data; len += rar_br_bits(br, lengthbits[lensymbol]); rar_br_consume(br, lengthbits[lensymbol]); } .... }
Ce morceau de code cache plusieurs problèmes à la fois. Lors de l'accès aux
bases de données et
aux tableaux de
longueurs , un index de tableau peut sortir des limites, car les développeurs ont écrit l'opérateur «>» au lieu de «> =» ci-dessus. Cette vérification a commencé à manquer une valeur inacceptable. Ici, nous n'avons rien d'autre qu'un modèle d'erreur classique appelé Erreur
Off-by-one .
Voici la liste complète des opérations d'accès aux tableaux par un index non valide:
- V557 Le dépassement de matrice est possible. La valeur de l'indice 'lensymbol' pourrait atteindre 28. archive_read_support_format_rar.c 2750
- V557 Le dépassement de matrice est possible. La valeur de l'indice 'lensymbol' pourrait atteindre 28. archive_read_support_format_rar.c 2751
- V557 Le dépassement de matrice est possible. La valeur de l'index 'lensymbol' pourrait atteindre 28. archive_read_support_format_rar.c 2753
- V557 Le dépassement de matrice est possible. La valeur de l'indice 'lensymbol' pourrait atteindre 28. archive_read_support_format_rar.c 2754
- V557 Le dépassement de matrice est possible. La valeur de l'index 'offssymbol' pourrait atteindre 60. archive_read_support_format_rar.c 2797
Fuite de mémoire
V773 La fonction a été
fermée sans libérer le pointeur 'testRun'. Une fuite de mémoire est possible. cmCTestMultiProcessHandler.cxx 193
void cmCTestMultiProcessHandler::FinishTestProcess(cmCTestRunTest* runner, bool started) { .... delete runner; if (started) { this->StartNextTests(); } } bool cmCTestMultiProcessHandler::StartTestProcess(int test) { .... cmCTestRunTest* testRun = new cmCTestRunTest(*this);
L'analyseur a détecté une fuite de mémoire. La mémoire du pointeur
testRun n'est pas libérée si la fonction
testRun-> StartTest renvoie
true . Lors de l'exécution d'une autre branche de code, cette mémoire est libérée dans la fonction
this-> FinishTestProcess.Fuite de ressources
V773 La fonction a été fermée sans fermer le fichier référencé par le descripteur 'fd'. Une fuite de ressource est possible. rhash.c 450
RHASH_API int rhash_file(....) { FILE* fd; rhash ctx; int res; hash_id &= RHASH_ALL_HASHES; if (hash_id == 0) { errno = EINVAL; return -1; } if ((fd = fopen(filepath, "rb")) == NULL) return -1; if ((ctx = rhash_init(hash_id)) == NULL) return -1;
Logique étrange dans des conditions
V590 Envisagez d'inspecter l'
expression '* s! =' \ 0 '&& * s ==' ''. L'expression est excessive ou contient une erreur d'impression. archive_cmdline.c 76
static ssize_t get_argument(struct archive_string *as, const char *p) { const char *s = p; archive_string_empty(as); while (*s != '\0' && *s == ' ') s++; .... }
La comparaison des caractères de
* avec null est redondante. La condition de la boucle while dépend uniquement du fait que le caractère soit égal ou non à un espace. Ce n'est pas une erreur, mais une complication inutile du code.
V592 L'expression a été placée entre parenthèses deux fois: ((expression)). Une paire de parenthèses n'est pas nécessaire ou une erreur d'impression est présente. cmCTestTestHandler.cxx 899
void cmCTestTestHandler::ComputeTestListForRerunFailed() { this->ExpandTestsToRunInformationForRerunFailed(); ListOfTests finalList; int cnt = 0; for (cmCTestTestProperties& tp : this->TestList) { cnt++;
L'analyseur avertit que l'opération de négation doit probablement être retirée des crochets. Il semble qu'il n'y ait pas un tel bug ici - juste des doubles crochets inutiles. Mais très probablement, il y a une erreur logique dans le code.
L'opérateur
continue n'est exécuté que dans le cas où la liste des tests
this-> TestsToRun n'est pas vide et
cnt est absent. Il est raisonnable de supposer que si la liste de tests est vide, la même action doit avoir lieu. Très probablement, la condition devrait être la suivante:
if (this->TestsToRun.empty() || std::find(this->TestsToRun.begin(), this->TestsToRun.end(), cnt) == this->TestsToRun.end()) { continue; }
V592 L'expression a été placée entre parenthèses deux fois: ((expression)). Une paire de parenthèses n'est pas nécessaire ou une erreur d'impression est présente. cmMessageCommand.cxx 73
bool cmMessageCommand::InitialPass(std::vector<std::string> const& args, cmExecutionStatus&) { .... } else if (*i == "DEPRECATION") { if (this->Makefile->IsOn("CMAKE_ERROR_DEPRECATED")) { fatal = true; type = MessageType::DEPRECATION_ERROR; level = cmake::LogLevel::LOG_ERROR; } else if ((!this->Makefile->IsSet("CMAKE_WARN_DEPRECATED") || this->Makefile->IsOn("CMAKE_WARN_DEPRECATED"))) { type = MessageType::DEPRECATION_WARNING; level = cmake::LogLevel::LOG_WARNING; } else { return true; } ++i; } .... }
C'est un exemple similaire, mais cette fois, je suis plus confiant qu'une erreur se produit. La fonction
IsSet ("CMAKE_WARN_DEPRECATED") vérifie que la valeur
CMAKE_WARN_DEPRECATED est définie globalement et la fonction
IsOn ("CMAKE_WARN_DEPRECATED) vérifie que la valeur est définie dans la configuration du projet. Très probablement, l'opérateur complémentaire est redondant, car dans les deux cas, il est correct de définir les mêmes valeurs de
type et de
niveau.V728 Un contrôle excessif peut être simplifié. Le '(A &&! B) || (! A && B) 'est équivalente à l'expression' bool (A)! = Bool (B) '. cmCTestRunTest.cxx 151
bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started) { .... } else if ((success && !this->TestProperties->WillFail) || (!success && this->TestProperties->WillFail)) { this->TestResult.Status = cmCTestTestHandler::COMPLETED; outputStream << " Passed "; } .... }
Ce code peut être plus simple. On peut réécrire l'expression conditionnelle de la manière suivante:
} else if (success != this->TestProperties->WillFail) { this->TestResult.Status = cmCTestTestHandler::COMPLETED; outputStream << " Passed "; }
Quelques autres endroits pour simplifier:
- V728 Un contrôle excessif peut être simplifié. Le '(A && B) || (! A &&! B) 'est équivalente à l'expression' bool (A) == bool (B) '. cmCTestTestHandler.cxx 702
- V728 Un contrôle excessif peut être simplifié. Le '(A &&! B) || (! A && B) 'est équivalente à l'expression' bool (A)! = Bool (B) '. digest_sspi.c 443
- V728 Un contrôle excessif peut être simplifié. Le '(A &&! B) || (! A && B) 'est équivalente à l'expression' bool (A)! = Bool (B) '. tcp.c 1295
- V728 Un contrôle excessif peut être simplifié. Le '(A &&! B) || (! A && B) 'est équivalente à l'expression' bool (A)! = Bool (B) '. testDynamicLoader.cxx 58
- V728 Un contrôle excessif peut être simplifié. Le '(A &&! B) || (! A && B) 'est équivalente à l'expression' bool (A)! = Bool (B) '. testDynamicLoader.cxx 65
- V728 Un contrôle excessif peut être simplifié. Le '(A &&! B) || (! A && B) 'est équivalente à l'expression' bool (A)! = Bool (B) '. testDynamicLoader.cxx 72
Avertissements divers
V523 L'
instruction 'then' est équivalente au fragment de code suivant. archive_read_support_format_ar.c 415
static int _ar_read_header(struct archive_read *a, struct archive_entry *entry, struct ar *ar, const char *h, size_t *unconsumed) { .... if (strcmp(filename, "__.SYMDEF") == 0) { archive_entry_copy_pathname(entry, filename); return (ar_parse_common_header(ar, entry, h)); } archive_entry_copy_pathname(entry, filename); return (ar_parse_common_header(ar, entry, h)); }
L'expression dans la dernière condition est similaire aux deux dernières lignes de la fonction. Un développeur peut simplifier ce code en supprimant la condition, ou il y a une erreur dans le code et elle doit être corrigée.
V535 La variable 'i' est utilisée pour cette boucle et pour la boucle externe. Vérifiez les lignes: 2220, 2241. multi.c 2241
static CURLMcode singlesocket(struct Curl_multi *multi, struct Curl_easy *data) { .... for(i = 0; (i< MAX_SOCKSPEREASYHANDLE) &&
La variable
i est utilisée comme compteur de boucles dans les boucles externes et internes. Dans le même temps, la valeur du compteur recommence à zéro dans la boucle intérieure. Ce n'est peut-être pas un bug ici, mais le code est suspect.
V519 La variable 'tagString' reçoit des valeurs successives deux fois. C'est peut-être une erreur. Vérifiez les lignes: 84, 86. cmCPackLog.cxx 86
void cmCPackLog::Log(int tag, const char* file, int line, const char* msg, size_t length) { .... if (tag & LOG_OUTPUT) { output = true; display = true; if (needTagString) { if (!tagString.empty()) { tagString += ","; } tagString = "VERBOSE"; } } if (tag & LOG_WARNING) { warning = true; display = true; if (needTagString) { if (!tagString.empty()) { tagString += ","; } tagString = "WARNING"; } } .... }
La variable
tagString est remplacée par une nouvelle valeur à tous les endroits. Il est difficile de dire quel est le problème ou pourquoi ils l'ont fait. Peut-être que les opérateurs «=» et «+ =» étaient confus.
La liste complète de ces lieux:
- V519 La variable 'tagString' reçoit des valeurs successives deux fois. C'est peut-être une erreur. Vérifiez les lignes: 94, 96. cmCPackLog.cxx 96
- V519 La variable 'tagString' reçoit des valeurs successives deux fois. C'est peut-être une erreur. Vérifiez les lignes: 104, 106. cmCPackLog.cxx 106
- V519 La variable 'tagString' reçoit des valeurs successives deux fois. C'est peut-être une erreur. Vérifiez les lignes: 114, 116. cmCPackLog.cxx 116
- V519 La variable 'tagString' reçoit des valeurs successives deux fois. C'est peut-être une erreur. Vérifiez les lignes: 125, 127. cmCPackLog.cxx 127
V519 La
variable 'aes-> aes_set' se voit attribuer des valeurs deux fois de suite. C'est peut-être une erreur. Vérifier les lignes: 4052, 4054. archive_string.c 4054
int archive_mstring_copy_utf8(struct archive_mstring *aes, const char *utf8) { if (utf8 == NULL) { aes->aes_set = 0;
Le réglage forcé de la valeur
AES_SET_UTF8 semble suspect. Je pense qu'un tel code va dérouter tout développeur, qui vient d'affiner ce fragment.
Ce code a été copié dans un autre endroit:
- V519 La variable 'aes-> aes_set' se voit attribuer des valeurs deux fois de suite. C'est peut-être une erreur. Vérifiez les lignes: 4066, 4068. archive_string.c 4068
Comment trouver des bogues dans un projet sur CMake
Dans cette section, je vais vous expliquer brièvement comment vérifier des projets CMake avec PVS-Studio aussi facilement qu'un-deux-trois.
Windows / Visual StudioPour Visual Studio, vous pouvez générer un fichier de projet à l'aide de l'interface graphique CMake ou la commande suivante:
cmake -G "Visual Studio 15 2017 Win64" ..
Ensuite, vous pouvez ouvrir le fichier .sln et vérifier le projet à l'aide du
plug -
in pour Visual Studio.
Linux / macOSLe fichier compile_commands.json est utilisé pour les vérifications sur ces systèmes. Soit dit en passant, il peut être généré dans différents systèmes de génération. Voici comment vous le faites dans CMake:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On ..
La dernière chose à faire est d'exécuter l'analyseur dans le répertoire avec le fichier .json:
pvs-studio-analyzer analyze -l /path/to/PVS-Studio.lic -o /path/to/project.log -e /path/to/exclude-path -j<N>
Nous avons également développé un module pour les projets CMake. Certaines personnes aiment l'utiliser. Le module CMake et des exemples de son utilisation peuvent être trouvés dans notre référentiel sur GitHub:
pvs-studio-cmake-examples .
Conclusion
Un large public d'utilisateurs de CMake est idéal pour tester le projet, mais de nombreux problèmes pourraient être évités avant la sortie en utilisant des outils d'analyse de code statique, tels que
PVS-Studio .
Si vous avez aimé les résultats de l'analyseur, mais que votre projet n'est pas écrit en C et C ++, je voudrais rappeler que l'analyseur prend également en charge l'analyse des projets en C # et Java. Vous pouvez tester l'analyseur sur votre projet en allant sur cette
page .