CMake ist ein plattformübergreifendes Automatisierungssystem für Bauprojekte. Dieses System ist viel älter als der statische Code-Analysator PVS-Studio, während noch niemand versucht hat, es auf den Code anzuwenden und Fehler zu überprüfen. Es stellt sich heraus, dass es viele Fehler gibt. Das Publikum von CMake ist riesig. Darauf beginnen neue Projekte und alte werden übertragen. Es ist beängstigend, sich vorzustellen, wie viele Programmierer diesen oder jenen Fehler haben könnten.
Einführung
CMake (vom englischen plattformübergreifenden Hersteller) ist ein plattformübergreifendes Automatisierungssystem zum Erstellen von Software aus dem Quellcode. CMake erstellt nicht direkt, sondern generiert nur Build-Steuerdateien aus CMakeLists.txt-Dateien. Die erste Veröffentlichung des Programms erfolgte im Jahr 2000. Zum Vergleich erschien der statische Analysator
PVS-Studio erst 2008. Dann konzentrierte es sich darauf, Fehler beim Portieren von Programmen von 32-Bit-Systemen auf 64-Bit-Systeme zu finden, und 2010 erschien der erste Satz allgemeiner Diagnosen (
V501 -
V545 ). Übrigens gibt es einige Warnungen von diesem ersten Satz im CMake-Code.
Unverzeihliche Fehler
V1040 Möglicher Tippfehler bei der Schreibweise eines vordefinierten
Makronamens . Das Makro '__MINGW32_' ähnelt '__MINGW32__'. winapi.h 4112
#if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32_) #define __UNICODE_STRING_DEFINED #endif
Die V1040- Diagnose wurde erst kürzlich implementiert. Zum Zeitpunkt der Veröffentlichung des Artikels wird es höchstwahrscheinlich keine Veröffentlichung geben, aber mit Hilfe dieser Diagnose ist es uns bereits gelungen, einen schweren Fehler zu finden.
Hier machten sie einen Tippfehler im Namen
__MINGW32_ . Am Ende fehlt ein Unterstrich. Wenn Sie nach Code mit diesem Namen suchen, können Sie sicherstellen, dass das Projekt tatsächlich die Version mit zwei Unterstrichen auf beiden Seiten verwendet:
V531 Es ist seltsam, dass ein Operator
sizeof () mit
sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 558
bool IsVisualStudioMacrosFileRegistered(const std::string& macrosFile, const std::string& regKeyBase, std::string& nextAvailableSubKeyName) { .... if (ERROR_SUCCESS == result) { wchar_t subkeyname[256];
Wenn das Array statisch deklariert ist, berechnet der Operator
sizeof seine Größe in Bytes, wobei sowohl die Anzahl der Elemente als auch die Größe der Elemente berücksichtigt werden. Bei der Berechnung des Werts der Variablen
cch_subkeyname hat der Programmierer dies nicht berücksichtigt und einen viermal größeren Wert als geplant erhalten. Lassen Sie uns erklären, wo es "4 mal" ist.
Das Array und seine falsche Größe werden an die
RegEnumKeyExW- Funktion übergeben:
LSTATUS RegEnumKeyExW( HKEY hKey, DWORD dwIndex, LPWSTR lpName,
Der Zeiger
lpcchName muss auf eine Variable zeigen, die die Größe des in Zeichen angegebenen Puffers enthält: "Ein Zeiger auf eine Variable, die die Größe des durch den Parameter
lpClass angegebenen
Puffers in Zeichen angibt." Das
Subkeyname- Array hat eine Größe von 512 Byte und kann 256 Zeichen vom Typ
wchar_t speichern (in Windows wchar_t sind es 2 Bytes). Dieser Wert ist 256 und sollte an die Funktion übergeben werden. Stattdessen wird 512 erneut mit 2 multipliziert, um 1024 zu erhalten.
Ich denke, wie man den Fehler behebt, ist jetzt klar. Verwenden Sie anstelle der Multiplikation die Division:
DWORD cch_subkeyname = sizeof(subkeyname) / sizeof(subkeyname[0]);
Übrigens tritt bei der Berechnung des Wertes der Variablen
cch_keyclass genau der gleiche Fehler auf.
Der beschriebene Fehler kann möglicherweise zu einem Pufferüberlauf führen. Es ist notwendig, alle diese Stellen zu reparieren:
- V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 556
- V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 572
- V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 621
- V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 622
- V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 649
V595 Der Zeiger 'this-> BuildFileStream' wurde verwendet, bevor er gegen nullptr verifiziert wurde. Überprüfen Sie die Zeilen: 133, 134. cmMakefileTargetGenerator.cxx 133
void cmMakefileTargetGenerator::CreateRuleFile() { .... this->BuildFileStream->SetCopyIfDifferent(true); if (!this->BuildFileStream) { return; } .... }
Der
this-> BuildFileStream-Zeiger wird unmittelbar vor der Validierungsprüfung dereferenziert. Hat das wirklich Probleme verursacht? Unten ist ein weiteres Beispiel für einen solchen Ort. Es wird direkt unter dem Kohlepapier hergestellt. Tatsächlich
gibt es jedoch viele
V595- Warnungen, von denen die meisten nicht so offensichtlich sind. Aus Erfahrung kann ich sagen, dass die Korrektur von Warnungen vor dieser Diagnose am längsten ist.
- V595 Der Zeiger 'this-> FlagFileStream' wurde verwendet, bevor er gegen nullptr verifiziert wurde. Überprüfen Sie die Zeilen: 303, 304. cmMakefileTargetGenerator.cxx 303
V614 Nicht initialisierter Zeiger 'str' verwendet. 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; };
Der Analysator erkannte die Verwendung des nicht initialisierten Zeigers
str . Und das entstand wegen des üblichen Tippfehlers. Beim Aufrufen der Funktion
SysAllocStringByteLen mussten Sie den Zeiger
src.str verwenden .
V557 Array-Überlauf ist möglich. Der Wert des 'Lensymbol'-Index könnte 28 erreichen. 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]); } .... }
In diesem Code wurden mehrere Probleme gefunden. Beim Zugriff auf die Arrays von
Längenbasen und
Längenbits kann die Grenze des Arrays überschritten werden, da die Entwickler über dem Code den Operator '>' anstelle von '> =' geschrieben haben. Bei einer solchen Überprüfung wurde ein ungültiger Wert übersprungen. Wir sind mit einem klassischen Fehlermuster konfrontiert, das als
Off-by-One-Fehler bezeichnet wird .
Die gesamte Liste der Orte, an denen über einen ungültigen Index auf Arrays zugegriffen werden kann:
- V557 Array-Überlauf ist möglich. Der Wert des 'Lensymbol'-Index könnte 28 erreichen. Archive_read_support_format_rar.c 2750
- V557 Array-Überlauf ist möglich. Der Wert des 'Lensymbol'-Index könnte 28 erreichen. Archive_read_support_format_rar.c 2751
- V557 Array-Überlauf ist möglich. Der Wert des 'Lensymbol'-Index könnte 28 erreichen. Archive_read_support_format_rar.c 2753
- V557 Array-Überlauf ist möglich. Der Wert des 'Lensymbol'-Index könnte 28 erreichen. Archive_read_support_format_rar.c 2754
- V557 Array-Überlauf ist möglich. Der Wert des 'offssymbol'-Index könnte 60 erreichen. Archive_read_support_format_rar.c 2797
Speicherverlust
V773 Die Funktion wurde beendet, ohne den Zeiger 'testRun' loszulassen. Ein Speicherverlust ist möglich. 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);
Der Analysator hat einen Speicherverlust festgestellt. Speicher durch Zeiger
testRun wird nicht freigegeben, wenn die Funktion
testRun-> StartTest true zurückgibt. Wenn ein anderer
Codezweig ausgeführt wird, wird der Speicher, der den
testRun- Zeiger verwendet, in der
Funktion this-> FinishTestProcess freigegeben .
Ressourcenleck
V773 Die Funktion wurde beendet, ohne die Datei zu schließen, auf die das Handle 'fd' verweist. Ein Ressourcenleck ist möglich. 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;
Seltsame Logik in Bedingungen
V590 Überprüfen Sie den
Ausdruck '* s! =' \ 0 '&& * s ==' ''. Der Ausdruck ist übertrieben oder enthält einen Druckfehler. 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++; .... }
Der Vergleich des Zeichens
* s mit einer Endnull ist überflüssig. Der Zustand der
while-Schleife hängt nur davon ab, ob das Zeichen einem Leerzeichen entspricht oder nicht. Dies ist kein Fehler, sondern eine zusätzliche Komplikation des Codes.
V592 Der Ausdruck wurde zweimal in Klammern gesetzt: ((Ausdruck)). Ein Klammerpaar ist nicht erforderlich oder es liegt ein Druckfehler vor. cmCTestTestHandler.cxx 899
void cmCTestTestHandler::ComputeTestListForRerunFailed() { this->ExpandTestsToRunInformationForRerunFailed(); ListOfTests finalList; int cnt = 0; for (cmCTestTestProperties& tp : this->TestList) { cnt++;
Der Analysator warnt davor, dass die Negation möglicherweise in Klammern gesetzt werden sollte. Es scheint, dass es hier keinen solchen Fehler gibt - nur zusätzliche doppelte Klammern. Höchstwahrscheinlich liegt jedoch ein logischer Fehler in diesem Zustand vor.
Die
continue- Anweisung wird ausgeführt, wenn die Liste der Tests
this-> TestsToRun nicht leer ist und
cnt nicht vorhanden ist. Es ist logisch anzunehmen, dass dieselbe Aktion ausgeführt werden muss, wenn die Testliste leer ist. Höchstwahrscheinlich sollte der Zustand folgendermaßen aussehen:
if (this->TestsToRun.empty() || std::find(this->TestsToRun.begin(), this->TestsToRun.end(), cnt) == this->TestsToRun.end()) { continue; }
V592 Der Ausdruck wurde zweimal in Klammern gesetzt: ((Ausdruck)). Ein Klammerpaar ist nicht erforderlich oder es liegt ein Druckfehler vor. 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; } .... }
Ein ähnliches Beispiel, aber hier bin ich sicherer, wenn ein Fehler vorliegt. Die
IsSet- Funktion
("CMAKE_WARN_DEPRECATED") überprüft,
ob der Wert
CMAKE_WARN_DEPRECATED global festgelegt ist, und die
IsOn- Funktion
("CMAKE_WARN_DEPRECATED") überprüft, ob der Wert in der Projektkonfiguration angegeben ist. Höchstwahrscheinlich ist der Negationsoperator überflüssig, weil In beiden Fällen ist es richtig, die gleichen
Typ- und
Pegelwerte festzulegen.
V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' 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 "; } .... }
Ein solcher Code kann erheblich vereinfacht werden, indem der bedingte Ausdruck folgendermaßen umgeschrieben wird:
} else if (success != this->TestProperties->WillFail) { this->TestResult.Status = cmCTestTestHandler::COMPLETED; outputStream << " Passed "; }
Einige weitere Orte, die Sie vereinfachen können:
- V728 Eine übermäßige Überprüfung kann vereinfacht werden. Die '(A && B) || (! A &&! B) 'Ausdruck entspricht dem Ausdruck' bool (A) == bool (B) '. cmCTestTestHandler.cxx 702
- V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. Digest_sspi.c 443
- V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. tcp.c 1295
- V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. testDynamicLoader.cxx 58
- V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. testDynamicLoader.cxx 65
- V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. testDynamicLoader.cxx 72
Verschiedene Warnungen
V523 Die Anweisung 'then' entspricht dem nachfolgenden Codefragment. 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)); }
Der Ausdruck in der letzten Bedingung ist identisch mit den letzten beiden Zeilen der Funktion. Dieser Code kann durch Entfernen der Bedingung vereinfacht werden, oder es liegt ein Fehler im Code vor und sollte behoben werden.
V535 Die Variable 'i' wird für diese Schleife und für die äußere Schleife verwendet. Überprüfen Sie die Zeilen: 2220, 2241. multi.c 2241
static CURLMcode singlesocket(struct Curl_multi *multi, struct Curl_easy *data) { .... for(i = 0; (i< MAX_SOCKSPEREASYHANDLE) &&
Die Variable
i wird als Schleifenzähler in den äußeren und verschachtelten Schleifen verwendet. In diesem Fall beginnt der Zählerwert im eingeschlossenen Eins erneut von Null an zu zählen. Dies ist hier möglicherweise kein Fehler, aber der Code ist verdächtig.
V519 Der Variablen 'tagString' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 84, 86. cmCPackLog.cxx 86
oid 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"; } } .... }
Die Variable
tagString wird an allen Stellen
durch den neuen Wert
ausgefranst . Es ist schwer zu sagen, was der Fehler war oder warum sie es getan haben. Vielleicht waren die Operatoren '=' und '+ =' verwirrt.
Die ganze Liste solcher Orte:
- V519 Der Variablen 'tagString' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 94, 96. cmCPackLog.cxx 96
- V519 Der Variablen 'tagString' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 104, 106. cmCPackLog.cxx 106
- V519 Der Variablen 'tagString' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 114, 116. cmCPackLog.cxx 116
- V519 Der Variablen 'tagString' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 125, 127. cmCPackLog.cxx 127
V519 Der Variablen 'aes-> aes_set' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 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;
Das Erzwingen von AES_SET_UTF8 sieht verdächtig aus. Ich denke, ein solcher Code wird jeden Entwickler irreführen, der mit der Verfeinerung dieses Ortes konfrontiert ist.
Dieser Code wurde an eine weitere Stelle kopiert:
- V519 Der Variablen 'aes-> aes_set' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 4066, 4068. archive_string.c 4068
So finden Sie Fehler in einem Projekt in CMake
In diesem Abschnitt werde ich Ihnen ein wenig erklären, wie Sie Projekte auf CMake mit PVS-Studio einfach und problemlos überprüfen können.
Windows / Visual StudioFür Visual Studio können Sie die Projektdatei mithilfe der CMake-GUI oder des folgenden Befehls generieren:
cmake -G "Visual Studio 15 2017 Win64" ..
Als Nächstes können Sie die SLN-Datei öffnen und das Projekt mit dem
Plug-In für Visual Studio testen.
Linux / MacOSAuf diesen Systemen wird die Datei compile_commands.json verwendet, um das Projekt zu überprüfen. Es kann übrigens in verschiedenen Montagesystemen erzeugt werden. In CMake geschieht dies folgendermaßen:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On ..
Es bleibt, den Analysator im Verzeichnis mit der .json-Datei zu starten:
pvs-studio-analyzer analyze -l /path/to/PVS-Studio.lic -o /path/to/project.log -e /path/to/exclude-path -j<N>
Wir haben auch ein Modul für CMake-Projekte entwickelt. Einige Leute benutzen es gerne. Das CMake-Modul und Beispiele für seine Verwendung finden Sie in unserem Repository auf GitHub:
pvs-studio-cmake-examples .
Fazit
Das große Publikum von CMake-Benutzern ist ein guter Tester des Projekts, aber viele Probleme konnten vor der Veröffentlichung mit Tools zur statischen Code-Analyse wie
PVS-Studio nicht verhindert werden .
Wenn Ihnen die Ergebnisse des Analysators gefallen haben, Ihr Projekt jedoch nicht in C und C ++ geschrieben ist, möchte ich Sie daran erinnern, dass der Analysator auch die Analyse von Projekten in C # und Java unterstützt. Sie können den Analysator in Ihrem Projekt testen, indem Sie auf
diese Seite gehen.

Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: Svyatoslav Razmyslov.
CMake: Der Fall, in dem die Qualität des Projekts unverzeihlich ist .