CMake ist ein plattformübergreifendes System zur Automatisierung von Projekterstellungen. Dieses System ist viel älter als der statische Code-Analysator PVS-Studio, aber niemand hat versucht, den Analysator auf seinen Code anzuwenden und die Fehler zu überprüfen. Wie sich herausstellte, gibt es viele von ihnen. Das CMake-Publikum ist riesig. Neue Projekte beginnen damit und alte werden portiert. Ich schaudere, wenn ich daran denke, wie viele Entwickler einen bestimmten Fehler hätten haben können.
Einführung
CMake ist ein plattformübergreifendes System zur Automatisierung der Softwareerstellung aus dem Quellcode. CMake ist nicht direkt zum Erstellen gedacht, sondern generiert nur Dateien zur Steuerung eines Builds aus CMakeLists.txt-Dateien. Die erste Veröffentlichung des Programms fand im Jahr 2000 statt. Zum Vergleich: Der PVS-Studio-Analysator erschien erst im Jahr 2008. Zu diesem Zeitpunkt war er auf die Suche nach Fehlern ausgerichtet, die durch die Portierung von 32-Bit-Systemen auf 64-Bit-Systeme entstanden waren. Im Jahr 2010 erschien der erste Satz von Allzweckdiagnosen (V501-
V545 ). Übrigens enthält der CMake-Code einige Warnungen aus diesem ersten Satz.
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 vor nicht allzu langer Zeit implementiert. Zum Zeitpunkt der Veröffentlichung des Artikels wird er höchstwahrscheinlich noch nicht veröffentlicht. Dennoch haben wir mit seiner Hilfe bereits einen coolen Fehler gefunden.
Der Name
__MINGW32_ enthält einen Tippfehler. Am Ende fehlt ein Unterstreichungszeichen. Wenn Sie den Code mit diesem Namen durchsuchen, sehen Sie, dass im Projekt die Version mit zwei Unterstreichungszeichen auf beiden Seiten verwendet wird:
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];
Für ein statisch deklariertes Array berechnet der Operator
sizeof die Größe in Byte unter Berücksichtigung der Anzahl der Elemente und ihrer Größe. Bei der Auswertung des Werts der Variablen
cch_subkeyname hat ein Entwickler dies nicht berücksichtigt und einen viermal höheren Wert als beabsichtigt erhalten. Lassen Sie uns erklären, woher "viermal" kommt.
Das Array und seine falsche Größe werden an die Funktion
RegEnumKeyExW übergeben :
LSTATUS RegEnumKeyExW( HKEY hKey, DWORD dwIndex, LPWSTR lpName,
Der Zeiger
lpcchName muss auf die Variable zeigen, die die Puffergröße in Zeichen enthält: "Ein Zeiger auf eine Variable, die die Größe des durch den Parameter
lpClass angegebenen
Puffers in Zeichen angibt." Die Größe des
Subkeyname- Arrays beträgt 512 Byte und kann 256 Zeichen vom Typ
wchar_t speichern (in Windows beträgt wchar_t 2 Byte). Es ist 256, die an die Funktion übergeben werden soll. Stattdessen wird 512 mit 2 multipliziert und wir erhalten 1024.
Ich denke, es ist jetzt klar, wie dieser Fehler behoben werden kann. Sie müssen Division statt Multiplikation verwenden:
DWORD cch_subkeyname = sizeof(subkeyname) / sizeof(subkeyname[0]);
Übrigens tritt der gleiche Fehler bei der Auswertung des Wertes der Variablen
cch_keyclass auf .
Der beschriebene Fehler kann möglicherweise zu einem Pufferüberlauf führen. Alle diese Fragmente müssen definitiv korrigiert werden:
- 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 Zeiger
this-> BuildFileStream wird unmittelbar vor der Überprüfung auf seine Gültigkeit dereferenziert. Hat das niemandem Probleme bereitet? Unten finden Sie ein weiteres Beispiel für ein solches Snippet. Es ist wie eine Kopie gemacht. Tatsächlich gibt es jedoch viele
V595- Warnungen, von denen die meisten nicht so offensichtlich sind. Aus meiner Erfahrung kann ich sagen, dass das Korrigieren von Warnungen dieser Diagnose am längsten dauert.
- 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 hat die Verwendung des nicht initialisierten
Str- Zeigers festgestellt. Es erschien aufgrund eines gewöhnlichen Tippfehlers. Beim Aufrufen der Funktion
SysAllocStringByteLen sollte der Zeiger
src.str verwendet werden .
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]); } .... }
Dieser Code verbirgt mehrere Probleme gleichzeitig. Beim Zugriff auf
Längenbasis- und
Längenbit- Arrays kann ein Array-Index außerhalb der Grenzen liegen, da Entwickler den Operator '>' anstelle von '> =' oben geschrieben haben. Bei dieser Überprüfung wurde ein inakzeptabler Wert übersehen. Hier haben wir nichts als ein klassisches Fehlermuster namens
Off-by-One-Fehler .
Hier ist die gesamte Liste der Array-Zugriffsvorgänge anhand eines ungültigen Index:
- 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. Der Speicher des
testRun- Zeigers wird nicht freigegeben, wenn die Funktion
testRun-> StartTest true zurückgibt. Wenn Sie einen anderen Code-Zweig ausführen, wird dieser Speicher 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 unter 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++; .... }
* s Zeichenvergleich mit null ist redundant. Der Zustand der
while- Schleife hängt nur davon ab, ob das Zeichen einem Leerzeichen entspricht oder nicht. Dies ist kein Fehler, sondern eine unnötige 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 der Negationsvorgang wahrscheinlich aus den Klammern genommen werden sollte. Es scheint, dass es hier keinen solchen Fehler gibt - nur unnötige doppelte Klammern. Höchstwahrscheinlich liegt jedoch ein logischer Fehler im Code vor.
Der Operator
continue wird nur ausgeführt, wenn die Liste der Tests
this-> TestsToRun nicht leer ist und
cnt nicht vorhanden ist. Es ist davon auszugehen, dass bei leerer Testliste dieselbe Aktion ausgeführt werden muss. Höchstwahrscheinlich sollte der Zustand wie folgt sein:
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; } .... }
Es ist ein ähnliches Beispiel, aber dieses Mal bin ich sicherer, dass ein Fehler auftritt. Die Funktion
IsSet ("CMAKE_WARN_DEPRECATED") prüft, ob der Wert
CMAKE_WARN_DEPRECATED global gesetzt ist, und die Funktion
IsOn ("CMAKE_WARN_DEPRECATED) prüft, ob der Wert in der Projektkonfiguration gesetzt ist. Höchstwahrscheinlich ist der komplementäre Operator redundant, da es in beiden Fällen richtig ist, die gleichen Werte für
Typ und
Ebene 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 "; } .... }
Dieser Code kann einfacher sein. Man kann den bedingten Ausdruck folgendermaßen umschreiben:
} else if (success != this->TestProperties->WillFail) { this->TestResult.Status = cmCTestTestHandler::COMPLETED; outputStream << " Passed "; }
Noch ein paar Orte zur Vereinfachung:
- 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 ähnelt den letzten beiden Zeilen der Funktion. Ein Entwickler kann diesen Code vereinfachen, indem er die Bedingung entfernt, oder es liegt ein Fehler im Code vor, der behoben werden sollte.
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 der äußeren und inneren Schleife verwendet. Gleichzeitig beginnt der Wert des Zählers in der inneren Schleife wieder bei Null. Es ist hier vielleicht 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
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"; } } .... }
Die Variable
tagString wird an allen Stellen mit einem neuen Wert überschrieben. Es ist schwer zu sagen, was das Problem ist oder warum sie es getan haben. Vielleicht waren die Operatoren '=' und '+ =' durcheinander.
Die gesamte 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;
Die erzwungene Einstellung des Werts
AES_SET_UTF8 sieht verdächtig aus. Ich denke, ein solcher Code wird jeden Entwickler verwirren, der dieses Fragment verfeinert.
Dieser Code wurde an einen anderen Ort 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 auf CMake
In diesem Abschnitt werde ich Ihnen kurz erklären, wie Sie CMake-Projekte mit PVS-Studio so einfach wie eins, zwei, drei überprüfen können.
Windows / Visual StudioFür Visual Studio können Sie eine 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 mithilfe des
Plugins für Visual Studio überprüfen.
Linux / MacOSDie Datei compile_commands.json wird für Überprüfungen auf diesen Systemen verwendet. Übrigens kann es in verschiedenen Build-Systemen generiert werden. So machen Sie es in CMake:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On ..
Als letztes müssen Sie den Analysator im Verzeichnis mit der .json-Datei ausführen:
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. Manche 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
Ein großes Publikum von CMake-Benutzern ist großartig, um das Projekt zu testen, aber viele Probleme könnten vor der Veröffentlichung durch die Verwendung statischer Code-Analyse-Tools wie
PVS-Studio verhindert werden .
Wenn Ihnen die Analyseergebnisse 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 .