Maschinelles Lernen in der statischen Analyse von Programmquellcode

Maschinelles Lernen in der statischen Analyse von Programmquellcode

Maschinelles Lernen hat sich in verschiedenen Bereichen des Menschen fest etabliert, von der Spracherkennung bis zur medizinischen Diagnose. Die Popularität dieses Ansatzes ist so groß, dass die Leute versuchen, ihn zu verwenden, wo immer sie können. Einige Versuche, klassische Ansätze durch neuronale Netze zu ersetzen, schlagen fehl. Dieses Mal werden wir uns mit maschinellem Lernen befassen, um effektive statische Code-Analysatoren zum Auffinden von Fehlern und potenziellen Schwachstellen zu erstellen.

Das PVS-Studio-Team wird häufig gefragt, ob wir mit dem maschinellen Lernen beginnen möchten, um Fehler im Software-Quellcode zu finden. Die kurze Antwort lautet ja, aber in begrenztem Umfang. Wir glauben, dass beim maschinellen Lernen viele Fallstricke bei Code-Analyse-Aufgaben lauern. Im zweiten Teil des Artikels werden wir darüber berichten. Beginnen wir mit einer Überprüfung neuer Lösungen und Ideen.

Neue Ansätze


Heutzutage gibt es viele statische Analysegeräte, die auf maschinellem Lernen basieren oder dieses verwenden, einschließlich Deep Learning und NLP zur Fehlererkennung. Nicht nur Enthusiasten, sondern auch große Unternehmen wie Facebook, Amazon oder Mozilla haben ihr Potenzial für maschinelles Lernen verdoppelt. Einige Projekte sind keine vollwertigen statischen Analysegeräte, da sie nur bestimmte Fehler in Commits finden.

Interessanterweise sind fast alle von ihnen als Game-Changer-Produkte positioniert, die aufgrund künstlicher Intelligenz den Durchbruch im Entwicklungsprozess schaffen werden.



Schauen wir uns einige der bekannten Beispiele an:

  1. Deepcode
  2. Infer, Sapienz, SapFix
  3. Embold
  4. Quelle {d}
  5. Clever-Commit, Commit-Assistent
  6. CodeGuru

Deepcode


Deep Code ist ein Tool zur Suche nach Sicherheitslücken für Java-, JavaScript-, TypeScript- und Python-Software, das maschinelles Lernen als Komponente bietet. Laut Boris Paskalev gibt es bereits mehr als 250.000 Regeln. Dieses Tool lernt aus Änderungen, die von Entwicklern im Quellcode von Open Source-Projekten vorgenommen wurden (eine Million Repositorys). Das Unternehmen selbst sagt, dass ihr Projekt eine Art Grammatik für Entwickler ist.



In der Tat vergleicht dieser Analysator Ihre Lösung mit der Projektbasis und bietet Ihnen die beabsichtigte beste Lösung aus der Erfahrung anderer Entwickler.

Im Mai 2018 teilten die Entwickler mit, dass die Unterstützung von C ++ in Vorbereitung ist, diese Sprache wird jedoch bislang nicht unterstützt. Wie auf der Website angegeben, kann die neue Sprachunterstützung aufgrund der Tatsache, dass die Sprache nur von einer Phase abhängt, die analysiert wird, in wenigen Wochen hinzugefügt werden.





Auf der Website finden Sie auch eine Reihe von Beiträgen zu den grundlegenden Methoden des Analysegeräts.

Infer


Facebook ist ziemlich eifrig bei seinen Versuchen, neue umfassende Ansätze in seine Produkte einzuführen. Auch maschinelles Lernen blieb nicht außen vor. Im Jahr 2013 kauften sie ein Startup, das einen statischen Analysator basierend auf maschinellem Lernen entwickelte. Und im Jahr 2015 wurde der Quellcode des Projekts geöffnet .

Infer ist ein statischer Analysator für Projekte in Java, C, C ++ und Objective-C, der von Facebook entwickelt wurde. Der Website zufolge wird sie auch in Amazon Web Services, Oculus, Uber und anderen beliebten Projekten verwendet.

Derzeit kann Infer Fehler im Zusammenhang mit Nullzeiger-Dereferenzierung und Speicherlecks finden. Infer basiert auf Hoares Logik, Trennungslogik und Biabduktion sowie abstrakter Interpretationstheorie. Durch die Verwendung dieser Ansätze kann der Analysator das Programm in Blöcke aufteilen und diese unabhängig analysieren.

Sie können versuchen, Infer für Ihre Projekte zu verwenden. Entwickler warnen jedoch, dass bei Facebook-Projekten etwa 80% der nützlichen Warnungen generiert werden, bei anderen Projekten jedoch keine geringe Anzahl von Fehlalarmen garantiert ist. Hier sind einige Fehler, die Infer bisher nicht erkennen kann, aber die Entwickler arbeiten daran, diese Warnungen zu implementieren:

  • Array-Index außerhalb der Grenzen;
  • Typ Casting Ausnahmen;
  • nicht verifizierte Datenlecks;
  • Rennbedingung.

Sapfix


SapFix ist ein automatisiertes Bearbeitungswerkzeug. Es erhält Informationen von Sapienz, einem Testautomatisierungs-Tool, und dem statischen Infer-Analysator. Basierend auf den letzten Änderungen und Meldungen wählt Infer eine von mehreren Strategien aus, um Fehler zu beheben.



In einigen Fällen setzt SapFix alle Änderungen oder Teile davon zurück. In anderen Fällen wird versucht, das Problem zu lösen, indem ein Patch aus den Fixierungsmustern generiert wird. Dieser Satz wird aus Mustern von Fixes gebildet, die von Programmierern selbst aus einem Satz bereits vorgenommener Fixes zusammengestellt wurden. Wenn ein solches Muster einen Fehler nicht behebt, versucht SapFix, ihn an die Situation anzupassen, indem kleine Änderungen in einem abstrakten Syntaxbaum vorgenommen werden, bis die potenzielle Lösung gefunden ist.

Eine mögliche Lösung reicht jedoch nicht aus, daher sammelt SapFix mehrere Lösungen auf der Grundlage einiger Punkte: ob Kompilierungsfehler vorliegen, ob es abstürzt, ob es neue Abstürze einführt. Sobald die Bearbeitungen vollständig getestet wurden, werden die Patches von einem Programmierer überprüft, der entscheidet, welche der Bearbeitungen das Problem am besten löst.

Embold


Embold ist eine Startplattform für die statische Analyse von Software-Quellcode, der vor der Umbenennung als Gamma bezeichnet wurde. Der statische Analysator basiert auf der eigenen Diagnose des Tools sowie auf integrierten Analysatoren wie Cppcheck, SpotBugs, SQL Check und anderen.



Neben der eigentlichen Diagnose konzentriert sich die Plattform auf lebendige Infografiken zum Laden der Codebasis und zur bequemen Anzeige gefundener Fehler sowie auf die Suche nach möglichen Umgestaltungen. Außerdem verfügt dieser Analysator über eine Reihe von Anti-Patterns, mit denen Sie Probleme in der Codestruktur auf Klassen- und Methodenebene erkennen und verschiedene Metriken zur Berechnung der Qualität eines Systems verwenden können.



Einer der Hauptvorteile ist das intelligente System des Angebots von Lösungen und Bearbeitungen, das zusätzlich zur herkömmlichen Diagnose Bearbeitungen anhand von Informationen über frühere Änderungen überprüft.



Mit NLP löst Embold den Code auf und sucht nach Zusammenhängen und Abhängigkeiten zwischen Funktionen und Methoden, wodurch Umgestaltungszeit gespart wird.



Auf diese Weise bietet Embold im Grunde genommen eine komfortable Visualisierung Ihrer Quellcode-Analyseergebnisse durch verschiedene Analysegeräte sowie eine eigene Diagnose, von denen einige auf maschinellem Lernen basieren.

Quelle {d}


Source {d} ist im Vergleich zu den von uns getesteten Analysegeräten das offenste Tool in Bezug auf die Art und Weise seiner Implementierung. Es ist auch eine Open-Source-Code-Lösung . Auf ihrer Website erhalten Sie im Austausch für Ihre E-Mail-Adresse eine Produktbroschüre, in der die von ihnen verwendeten Technologien beschrieben werden. Außerdem enthält die Website einen Link zur Datenbank mit Veröffentlichungen zur Verwendung von maschinellem Lernen für die Codeanalyse sowie zum Repository mit Dataset für das codebasierte Lernen. Das Produkt selbst ist eine Plattform zur Analyse des Quellcodes und des Softwareprodukts und richtet sich nicht an Entwickler, sondern an Manager. Zu seinen Fähigkeiten gehören die Berechnung der technischen Schulden, Engpässe im Entwicklungsprozess und andere globale Statistiken zum Projekt.



Ihr Ansatz zur Code-Analyse durch maschinelles Lernen basiert auf der Natural-Hypothese, wie im Artikel " Über die Natürlichkeit von Software " beschrieben.

"Programmiersprachen sind theoretisch komplex, flexibel und leistungsfähig, aber die Programme, die echte Menschen tatsächlich schreiben, sind meistens einfach und eher repetitiv, und daher haben sie brauchbar vorhersagbare statistische Eigenschaften, die in statistischen Sprachmodellen erfasst und für das Software-Engineering genutzt werden können Aufgaben. "

Basierend auf dieser Hypothese sind die statistischen Eigenschaften umso größer und die durch Lernen erreichten Metriken umso genauer, je größer die Codebasis ist.

Zur Analyse des Codes in source {d} wird der Babelfish-Dienst verwendet, der die Codedatei in einer der verfügbaren Sprachen analysieren, einen abstrakten Syntaxbaum abrufen und in einen universellen Syntaxbaum konvertieren kann.



Quelle {d} sucht jedoch nicht nach Fehlern im Code. Basierend auf dem Baum, in dem ML für das gesamte Projekt verwendet wird, erkennt source {d} die Formatierung des Codes, den im Projekt angewendeten Stil und ein Commit. Wenn der neue Code nicht dem Projektcode-Stil entspricht, werden einige Änderungen vorgenommen.





Das Lernen konzentriert sich auf mehrere grundlegende Elemente: Leerzeichen, Tabellierung, Zeilenumbrüche usw.



Lesen Sie mehr dazu in ihrer Publikation: " STYLE-ANALYZER: Beheben von Inkonsistenzen im Code-Stil mit interpretierbaren, nicht überwachten Algorithmen ".

Alles in allem ist source {d} eine breite Plattform für die Erfassung verschiedener Statistiken zum Quellcode und zum Projektentwicklungsprozess: von Effizienzberechnungen der Entwickler bis hin zu Zeitkosten für die Codeüberprüfung.

Clever begehen


Clever-Commit ist ein Analysator, der von Mozilla in Zusammenarbeit mit Ubisoft erstellt wurde. Es basiert auf einer CLEVER-Studie (Combining Levels of Bug Prevention and Resolution Techniques) von Ubisoft und seinem Commit Assistant für untergeordnete Produkte, die verdächtige Commits erkennt, die wahrscheinlich einen Fehler enthalten. Da CLEVER auf einem Codevergleich basiert, kann es sowohl auf gefährlichen Code verweisen als auch Vorschläge für mögliche Änderungen machen. Nach der Beschreibung findet Clever-Commit in 60-70% der Fälle Problemstellen und bietet korrekte Bearbeitungen mit der gleichen Wahrscheinlichkeit an. Im Allgemeinen gibt es nur wenige Informationen zu diesem Projekt und zu den Fehlern, die es finden kann.

CodeGuru


Vor kurzem ist CodeGuru, ein Produkt von Amazon, mit maschinellem Lernen in Einklang mit Analysegeräten gekommen. Es handelt sich um einen maschinellen Lerndienst, mit dem Sie Fehler im Code finden und kostspielige Bereiche darin identifizieren können. Die Analyse ist bisher nur für Java-Code verfügbar, die Autoren versprechen jedoch, in Zukunft andere Sprachen zu unterstützen. Andy Jassy, ​​CEO von AWS (Amazon Web Services), gibt an, dass es bereits seit langer Zeit bei Amazon eingesetzt wird.

Der Website zufolge lernte CodeGuru sowohl auf der Amazon-Codebasis als auch in mehr als 10.000 Open Source-Projekten.

Grundsätzlich besteht der Dienst aus zwei Teilen: CodeGuru Reviewer, der mithilfe der Suche nach assoziativen Regeln und der Suche nach Fehlern im Code unterrichtet wird, und CodeGuru Profiler, der die Leistung von Anwendungen überwacht.



Im Allgemeinen sind nicht viele Informationen zu diesem Projekt verfügbar. Wie auf der Website angegeben, analysiert der Prüfer die Amazon-Codebasen und sucht nach Pull-Anforderungen, die die AWS-API-Aufrufe enthalten, um zu erfahren, wie Abweichungen von "Best Practices" festgestellt werden können. Anschließend werden die vorgenommenen Änderungen betrachtet und mit Daten aus der Dokumentation verglichen, die gleichzeitig analysiert werden. Das Ergebnis ist ein "Best Practices" -Modell.

Es wird auch gesagt, dass sich die Empfehlungen für den Benutzercode tendenziell verbessern, nachdem Feedback zu ihnen eingegangen ist.

Die Liste der Fehler, auf die der Prüfer reagiert, ist ziemlich verschwommen, da keine spezifische Fehlerdokumentation veröffentlicht wurde:

  • Best Practices AWS
  • Parallelität
  • Ressourcenlecks
  • Verlust vertraulicher Informationen
  • Allgemeine "Best Practices" der Codierung

Unsere Skepsis


Betrachten wir nun die Fehlersuche aus der Sicht unseres Teams, das seit vielen Jahren statische Analysegeräte entwickelt. Wir sehen eine Reihe allgemeiner Probleme bei der Anwendung von Lernmethoden, die wir gerne behandeln möchten. Zunächst werden wir alle ML-Ansätze in zwei Typen unterteilen:

  1. Diejenigen, die einem statischen Analysator manuell beibringen, anhand von synthetischen und realen Codebeispielen nach verschiedenen Problemen zu suchen.
  2. Diejenigen, die Algorithmen für eine große Anzahl von Open-Source-Code und Revisionsverlauf (GitHub) lehren, wonach der Analysator beginnt, Fehler zu erkennen und sogar Bearbeitungen anzubieten.

Wir werden über jede Richtung getrennt sprechen, da sie unterschiedliche Nachteile haben. Danach werden die Leser, glaube ich, erfahren, warum wir die Möglichkeiten des maschinellen Lernens nicht leugnen, aber die Begeisterung trotzdem nicht teilen.

Hinweis Wir betrachten die Entwicklung eines universellen statischen Universalanalysators. Wir konzentrieren uns auf die Entwicklung des Analysegeräts, das von jedem Team verwendet werden kann und nicht auf eine bestimmte Codebasis.

Manuelles Einlernen eines statischen Analysators


Angenommen, wir möchten ML verwenden, um nach den folgenden Arten von Fehlern im Code zu suchen:

if (A == A) 

Es ist seltsam, eine Variable mit sich selbst zu vergleichen. Wir können viele Beispiele für korrekten und falschen Code schreiben und dem Analysator beibringen, nach solchen Fehlern zu suchen. Außerdem können Sie den Tests echte Beispiele bereits gefundener Fehler hinzufügen. Nun, die Frage ist, wo solche Beispiele zu finden sind. Ok, nehmen wir an, es ist möglich. Zum Beispiel haben wir eine Reihe von Beispielen für solche Fehler: V501 , V3001 , V6001 .

Ist es also möglich, solche Fehler im Code mithilfe der ML-Algorithmen zu identifizieren? Ja, das ist es. Die Sache ist - warum brauchen wir das?

Sehen Sie, um den Analysator zu unterrichten, müssen wir viel Aufwand betreiben, um die Beispiele für den Unterricht vorzubereiten. Eine andere Möglichkeit besteht darin, den Code der realen Anwendungen zu markieren und die Fragmente anzugeben, bei denen der Analysator eine Warnung ausgeben muss. In jedem Fall muss eine Menge Arbeit geleistet werden, da es Tausende von Beispielen für das Lernen geben sollte. Oder Zehntausende.

Schließlich wollen wir nicht nur (A == A) Fälle erkennen, sondern auch:

  • if (X && A == A)
  • if (A + 1 == A + 1)
  • if (A [i] == A [i])
  • if ((A) == (A))
  • und so weiter.


Schauen wir uns die mögliche Implementierung einer so einfachen Diagnose in PVS-Studio an:

 void RulePrototype_V501(VivaWalker &walker, const Ptree *left, const Ptree *right, const Ptree *operation) { if (SafeEq(operation, "==") && SafeEqual(left, right)) { walker.AddError("Oh boy! Holy cow!", left, 501, Level_1, "CWE-571"); } } 

Und das war's auch schon! Sie brauchen keine Beispiele für ML!

Zukünftig muss die Diagnose lernen, eine Reihe von Ausnahmen zu berücksichtigen und Warnungen für (A [0] == A [1-1]) auszugeben. Wie wir wissen, kann es leicht programmiert werden. Im Gegenteil, in diesem Fall wird es mit der Basis von Beispielen schlecht.

Beachten Sie, dass wir in beiden Fällen ein System zum Testen, Dokumentieren usw. benötigen. Was den Arbeitsaufwand bei der Erstellung einer neuen Diagnose anbelangt, übernimmt der klassische Ansatz, bei dem die Regel fest im Code programmiert ist, die Führung.

Ok, es ist Zeit für eine andere Regel. Zum Beispiel die, bei der das Ergebnis einiger Funktionen verwendet werden muss. Es hat keinen Sinn, sie anzurufen und ihr Ergebnis nicht zu verwenden. Hier sind einige solcher Funktionen:

  • Malloc
  • memcmp
  • Zeichenfolge :: leer

Dies ist die Aufgabe der PVS-Studio V530- Diagnose.

Wir möchten also Aufrufe solcher Funktionen erkennen, deren Ergebnis nicht verwendet wird. Dazu können Sie viele Tests generieren. Und wir denken, dass alles gut funktionieren wird. Aber auch hier ist nicht klar, warum es benötigt wird.

Die V530-Diagnoseimplementierung nahm im PVS-Studio-Analysator mit allen Ausnahmen 258 Codezeilen in Anspruch, von denen 64 Kommentare sind. Es gibt auch eine Tabelle mit Funktionsanmerkungen, in der darauf hingewiesen wird, dass deren Ergebnis verwendet werden muss. Es ist viel einfacher, diese Tabelle aufzuladen, als synthetische Beispiele zu erstellen.

Bei Diagnosen mit Datenflussanalyse wird es noch schlimmer. Beispielsweise kann der PVS-Studio-Analysator den Wert von Zeigern verfolgen, wodurch Sie einen solchen Speicherverlust finden können:

 uint32_t* BnNew() { uint32_t* result = new uint32_t[kBigIntSize]; memset(result, 0, kBigIntSize * sizeof(uint32_t)); return result; } std::string AndroidRSAPublicKey(crypto::RSAPrivateKey* key) { .... uint32_t* n = BnNew(); .... RSAPublicKey pkey; pkey.len = kRSANumWords; pkey.exponent = 65537; // Fixed public exponent pkey.n0inv = 0 - ModInverse(n0, 0x100000000LL); if (pkey.n0inv == 0) return kDummyRSAPublicKey; // <= .... } 

Das Beispiel stammt aus dem Artikel " Chrom: Memory Leaks ". Wenn die Bedingung (pkey.n0inv == 0) wahr ist, wird die Funktion beendet, ohne den Puffer freizugeben , auf den der Zeiger in der Variablen n gespeichert ist.

Aus Sicht des PVS-Studios ist hier nichts kompliziert. Der Analysator hat die BnNew- Funktion untersucht und sich daran erinnert, dass er einen Zeiger auf den zugewiesenen Speicherblock zurückgegeben hat. In einer anderen Funktion wurde festgestellt, dass der Puffer möglicherweise nicht freigegeben wird und der Zeiger darauf beim Verlassen der Funktion verloren geht.

Es ist ein üblicher Algorithmus zum Verfolgen von Werten. Es ist egal, wie der Code geschrieben ist. Es spielt keine Rolle, was sich in der Funktion noch befindet, das sich nicht auf die Zeigerarbeit bezieht. Der Algorithmus ist universell und die V773-Diagnose findet viele Fehler in verschiedenen Projekten. Sehen Sie, wie unterschiedlich die Codefragmente mit erkannten Fehlern sind!

Wir sind keine Experten für ML, aber wir haben das Gefühl, dass große Probleme hier gleich um die Ecke liegen. Es gibt unglaublich viele Möglichkeiten, wie Sie Code mit Speicherlecks schreiben können. Selbst wenn die Maschine gut gelernt hätte, wie man Werte von Variablen verfolgt, müsste sie verstehen, dass es auch Aufrufe von Funktionen gibt.

Wir vermuten, dass dafür so viele Beispiele erforderlich sind, dass die Aufgabe nicht mehr greifbar ist. Wir sagen nicht, dass es unrealistisch ist. Wir bezweifeln, dass sich die Kosten für die Erstellung des Analysators auszahlen werden.

Analogie Was mir in den Sinn kommt, ist die Analogie zu einem Taschenrechner, bei dem man anstelle der Diagnose arithmetische Aktionen programmieren muss. Wir sind sicher, dass Sie einem ML-basierten Rechner beibringen können, Zahlen gut zu summieren, indem Sie die Ergebnisse der Operationen 1 + 1 = 2, 1 + 2 = 3, 2 + 1 = 3, 100 + 200 = 300 usw. eingeben . Wie Sie verstehen, ist die Machbarkeit der Entwicklung eines solchen Rechners eine große Frage (es sei denn, es wird ein Zuschuss gewährt :). Mit der einfachen Operation "+" im Code kann ein viel einfacher, schneller, genauer und zuverlässiger Taschenrechner geschrieben werden.

Fazit Nun, dieser Weg wird klappen. Unserer Meinung nach macht es jedoch keinen praktischen Sinn, es zu verwenden. Die Entwicklung wird zeitaufwändiger, aber das Ergebnis ist weniger zuverlässig und genau, insbesondere wenn es um die Implementierung komplexer Diagnosen auf der Grundlage von Datenflussanalysen geht.

Lernen mit einer großen Menge an Open Source Code


Okay, wir haben manuelle Synthesebeispiele zusammengestellt, aber es gibt auch GitHub. Sie können den Commit-Verlauf nachverfolgen und Code-Änderungs- / Korrekturmuster ableiten. Dann können Sie nicht nur auf Fragmente verdächtigen Codes verweisen, sondern auch eine Möglichkeit vorschlagen, den Code zu reparieren.

Wenn Sie auf dieser Detailebene stehen bleiben, sieht alles gut aus. Der Teufel steckt wie immer im Detail. Sprechen wir also richtig über diese Details.

Die erste Nuance. Datenquelle.

GitHub-Bearbeitungen sind ziemlich zufällig und vielfältig. Die Leute sind oft faul, atomare Commits zu machen und gleichzeitig mehrere Änderungen im Code vorzunehmen. Sie wissen, wie es passiert: Sie würden den Fehler beheben und ihn gleichzeitig ein wenig überarbeiten ("Und hier werde ich die Behandlung eines solchen Falls hinzufügen ..."). Sogar eine Person kann dann unverständlich sein, ob diese fest miteinander verwandt sind oder nicht.

Die Herausforderung besteht darin, tatsächliche Fehler vom Hinzufügen neuer Funktionen oder etwas anderem zu unterscheiden. Sie können natürlich 1000 Personen einstellen, die die Commits manuell markieren. Die Leute müssen darauf hinweisen: Hier wurde ein Fehler behoben, hier wird umgestaltet, hier gibt es einige neue Funktionen, hier haben sich die Anforderungen geändert und so weiter.

Ist so ein Aufschlag möglich? Ja! Beachten Sie jedoch, wie schnell das Spoofing erfolgt. Anstatt "der Algorithmus lernt sich auf der Basis von GitHub" diskutieren wir bereits, wie man Hunderte von Menschen für eine lange Zeit rätselt. Der Aufwand und die Kosten für die Erstellung des Tools steigen dramatisch.

Sie können versuchen, automatisch festzustellen, wo die Fehler behoben wurden. Dazu sollten Sie die Kommentare zu den Commits analysieren und auf kleine lokale Änderungen achten, bei denen es sich höchstwahrscheinlich um Fehlerbehebungen handelt. Es ist schwer zu sagen, wie gut Sie automatisch nach Fehlerkorrekturen suchen können. In jedem Fall ist dies eine große Aufgabe, die eine getrennte Recherche und Programmierung erfordert.

Wir müssen also noch nicht einmal lernen, und es gibt bereits Nuancen :).

Die zweite Nuance. Eine Verzögerung in der Entwicklung.

Analysatoren, die auf solchen Plattformen wie GitHub lernen, werden immer einem solchen Syndrom wie "mental retardation delay" ausgesetzt sein. Dies liegt daran, dass sich die Programmiersprachen im Laufe der Zeit ändern.

Seit C # 8.0 gibt es nullbare Referenztypen, die dazu beitragen, NRE (Null Reference Exceptions) zu bekämpfen. In JDK 12 erschien ein neuer Vermittlungsoperator ( JEP 325 ). In C ++ 17 gibt es die Möglichkeit, bedingte Konstrukte zur Kompilierungszeit auszuführen ( constexpr if ). Und so weiter.

Programmiersprachen entwickeln sich weiter. Darüber hinaus entwickeln sich diejenigen wie C ++ sehr schnell. Neue Konstruktionen erscheinen, neue Standardfunktionen werden hinzugefügt und so weiter. Neben den neuen Funktionen gibt es neue Fehlermuster, die wir auch mit der statischen Code-Analyse identifizieren möchten.

An dieser Stelle steht die ML-Methode vor einem Problem: Das Fehlermuster ist bereits klar, wir möchten es erkennen, aber es gibt keine Codebasis zum Lernen.

Schauen wir uns dieses Problem an einem bestimmten Beispiel an. Die bereichsbasierte for-Schleife wurde in C ++ 11 angezeigt. Sie können den folgenden Code schreiben, der alle Elemente im Container durchläuft:

 std::vector<int> numbers; .... for (int num : numbers) foo(num); 

Die neue Schleife hat das neue Fehlermuster mitgebracht. Wenn wir den Container innerhalb der Schleife ändern, führt dies dazu, dass die "Schatten" -Iteratoren ungültig werden.

Schauen wir uns den folgenden falschen Code an:

 for (int num : numbers) { numbers.push_back(num * 2); } 

Der Compiler wird es in so etwas verwandeln:

 for (auto __begin = begin(numbers), __end = end(numbers); __begin != __end; ++__begin) { int num = *__begin; numbers.push_back(num * 2); } 

Während des Push- Backs können die Iteratoren __begin und __end ungültig gemacht werden, wenn der Speicher innerhalb des Vektors verschoben wird. Das Ergebnis ist das undefinierte Verhalten des Programms.

Daher ist das Fehlermuster seit langem bekannt und in der Literatur beschrieben. Der PVS-Studio-Analysator diagnostiziert es mit der V789- Diagnose und hat in Open-Source-Projekten bereits echte Fehler gefunden.

Wie schnell wird GitHub genug neuen Code bekommen, um dieses Muster zu bemerken? Gute Frage ... Es ist wichtig zu bedenken, dass wenn es eine bereichsbasierte for-Schleife gibt, dies nicht bedeutet, dass alle Programmierer sofort damit beginnen, sie sofort zu verwenden. Es kann Jahre dauern, bis die neue Schleife viel Code enthält. Außerdem müssen viele Fehler gemacht und dann behoben werden, damit der Algorithmus das Muster in den Bearbeitungen erkennen kann.

Wie viele Jahre wird es dauern? Fünf? Zehn?

Zehn ist zu viel, oder ist es eine pessimistische Vorhersage? Weit davon entfernt. Zu dem Zeitpunkt, als der Artikel verfasst wurde, waren bereits acht Jahre vergangen, seit eine bereichsbasierte for-Schleife in C ++ 11 erschienen war. Bisher gibt es in unserer Datenbank jedoch nur drei Fälle eines solchen Fehlers. Drei Fehler sind nicht viel und nicht wenig. Aus dieser Zahl sollte man keine Schlussfolgerung ziehen. Die Hauptsache ist, zu bestätigen, dass ein solches Fehlermuster real ist und es sinnvoll ist, es zu erkennen.

Vergleichen Sie nun beispielsweise diese Zahl mit diesem Fehlermuster: Der Zeiger wird vor der Prüfung dereferenziert . Insgesamt haben wir bei der Prüfung von Open-Source-Projekten bereits 1.716 solcher Fälle identifiziert.

Vielleicht sollten wir überhaupt nicht nach Fehlern in bereichsbasierten for-Schleifen suchen? Nr Es ist nur so, dass Programmierer träge sind und dieser Operator sehr langsam populär wird. Allmählich wird es sowohl mehr Code als auch Fehler geben.

Dies wird wahrscheinlich erst 10-15 Jahre nach Erscheinen von C ++ 11 geschehen. Dies führt zu einer philosophischen Frage. Nehmen wir an, wir kennen das Fehlermuster bereits und warten viele Jahre, bis wir viele Fehler in Open Source-Projekten haben. Wird es so sein

Wenn "Ja", ist es sicher, "geistige Entwicklungsverzögerung" für alle ML-basierten Analysegeräte zu diagnostizieren.

Wenn "nein", was sollen wir tun? Es gibt keine Beispiele. Manuell schreiben? Auf diese Weise kehren wir zum vorherigen Kapitel zurück, in dem wir eine detaillierte Beschreibung der Option gegeben haben, in der die Leute eine ganze Reihe von Beispielen für das Lernen schreiben würden.

Dies kann getan werden, aber die Frage der Zweckmäßigkeit stellt sich erneut. Die Implementierung der V789-Diagnose mit allen Ausnahmen im PVS-Studio-Analysator erfordert nur 118 Codezeilen, von denen 13 Kommentarzeilen sind. Das heißt, es ist eine sehr einfache Diagnose, die auf klassische Weise leicht programmiert werden kann.

Die Situation wird ähnlich sein wie bei anderen Innovationen, die in einer anderen Sprache erscheinen. Wie sie sagen, gibt es etwas zu überlegen.

Die dritte Nuance. Dokumentation

Ein wichtiger Bestandteil jedes statischen Analysators ist die Dokumentation, in der die einzelnen Diagnosen beschrieben werden. Ohne diesen kann der Analysator nur sehr schwer oder gar nicht verwendet werden. In der Dokumentation zu PVS-Studio finden Sie eine Beschreibung der einzelnen Diagnosen, die ein Beispiel für einen fehlerhaften Code und dessen Behebung enthält. Wir geben auch den Link zu CWE , wo man eine alternative Problembeschreibung lesen kann. Und dennoch verstehen Benutzer manchmal etwas nicht und stellen uns klärende Fragen.

Bei ML-basierten statischen Analysatoren ist das Dokumentationsproblem irgendwie vertuscht. Es wird davon ausgegangen, dass der Analysator lediglich auf einen Ort zeigt, der ihm verdächtig erscheint, und möglicherweise sogar vorschlägt, wie er behoben werden kann. Die Entscheidung, eine Änderung vorzunehmen oder nicht, liegt bei der Person. Hier beginnt der Ärger ... Es ist nicht einfach, eine Entscheidung zu treffen, ohne lesen zu können, was den Analyzer an einer bestimmten Stelle im Code misstrauisch erscheinen lässt.

Natürlich wird in einigen Fällen alles offensichtlich sein. Angenommen, der Analysator verweist auf diesen Code:

 char *p = (char *)malloc(strlen(src + 1)); strcpy(p, src); 

Und schlagen vor, dass wir es ersetzen durch:

 char *p = (char *)malloc(strlen(src) + 1); strcpy(p, src); 

Es ist sofort klar, dass der Programmierer einen Tippfehler gemacht und 1 an der falschen Stelle hinzugefügt hat. Infolgedessen wird weniger Speicher als erforderlich zugewiesen.

Hier ist auch ohne Dokumentation alles klar. Dies wird jedoch nicht immer der Fall sein.

Stellen Sie sich vor, der Analysator zeigt "still" auf diesen Code:

 char check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); } 

Und schlägt vor, dass wir den char-Typ des Rückgabewerts für int ändern:

 int check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); } 

Es gibt keine Dokumentation für die Warnung. Anscheinend enthält die Warnmeldung auch keinen Text, wenn es sich um einen völlig unabhängigen Analysator handelt.

Was sollen wir tun Was ist der unterschied Lohnt sich ein solcher Ersatz?

Eigentlich könnte ich das Risiko eingehen und zustimmen, den Code zu reparieren. Obwohl es eine heikle Übung ist, Fixes zu akzeptieren, ohne sie zu verstehen ... :) Sie können in die Beschreibung der memcmp- Funktion schauen und feststellen, dass die Funktion wirklich Werte wie int : 0, mehr als null und weniger als null zurückgibt. Es ist jedoch möglicherweise immer noch unklar, warum Änderungen vorgenommen werden, wenn der Code bereits ordnungsgemäß funktioniert.

Wenn Sie jetzt nicht wissen, was die Bearbeitung ist, lesen Sie die Beschreibung der V642- Diagnose. Es wird sofort klar, dass dies ein echter Bug ist. Darüber hinaus kann es zu einer Sicherheitsanfälligkeit kommen.

Vielleicht schien das Beispiel nicht zu überzeugen. Immerhin schlug der Analysator einen Code vor, der wahrscheinlich besser ist. Ok Schauen wir uns zur Abwechslung ein weiteres Beispiel für Pseudocode in Java an.

 ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj.state = 200; out.writeObject(obj); out.close(); 

Da ist ein Gegenstand. Es wird serialisiert. Dann ändert sich der Status des Objekts und es wird erneut serialisiert. Es sieht gut aus. Stellen Sie sich nun vor, der Analysator mag den Code plötzlich nicht mehr und möchte ihn durch den folgenden ersetzen:

 ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj = new SerializedObject(); // The line is added obj.state = 200; out.writeObject(obj); out.close(); 

Anstatt das Objekt zu ändern und neu zu schreiben, wird ein neues Objekt erstellt und es wird serialisiert.

Es gibt keine Beschreibung des Problems. Keine Dokumentation. Der Code ist länger geworden. Aus irgendeinem Grund wird ein neues Objekt erstellt. Sind Sie bereit, eine solche Änderung in Ihrem Code vorzunehmen?

Du wirst sagen, es ist nicht klar. In der Tat ist es unverständlich. Und es wird die ganze Zeit so sein. Die Arbeit mit einem solchen "stillen" Analysegerät wird eine endlose Studie sein, um zu verstehen, warum das Analysegerät nichts mag.

Wenn es Dokumentation gibt, wird alles transparent. Die für die Serialisierung verwendete Klasse java.io.ObjectOuputStream speichert die geschriebenen Objekte im Cache. Dies bedeutet, dass dasselbe Objekt nicht zweimal serialisiert wird. Die Klasse serialisiert das Objekt einmal und schreibt beim zweiten Mal nur einen Verweis auf dasselbe erste Objekt in den Stream. Weiterlesen : V6076 - Bei der wiederkehrenden Serialisierung wird der Status des zwischengespeicherten Objekts aus der ersten Serialisierung verwendet.

Wir hoffen, wir haben es geschafft, die Wichtigkeit der Dokumentation zu erklären. Hier kommt die Frage. Wie erscheint die Dokumentation für den ML-basierten Analysator?

Wenn ein klassischer Code-Analysator entwickelt wird, ist alles einfach und klar. Es gibt ein Fehlermuster. Wir beschreiben es in der Dokumentation und implementieren die Diagnose.

Im Fall von ML ist der Prozess umgekehrt. Ja, der Analysator kann eine Abweichung im Code feststellen und darauf hinweisen. Aber es weiß nichts über das Wesen des Mangels. Es versteht nicht und sagt Ihnen auch nicht, warum Sie so keinen Code schreiben können. Dies sind zu hochrangige Abstraktionen. Auf diese Weise sollte der Analysator auch lernen, die Dokumentation für Funktionen zu lesen und zu verstehen .

Wie gesagt, da das Dokumentationsproblem in Artikeln zum maschinellen Lernen vermieden wird, sind wir nicht bereit, weiter darauf einzugehen. Nur eine weitere große Nuance, die wir ausgesprochen haben.

Hinweis Sie könnten argumentieren, dass die Dokumentation optional ist. Der Analysator kann auf viele Beispiele für Korrekturen in GitHub verweisen, und die Person, die sich die Commits und Kommentare ansieht, wird verstehen, was was ist. Ja es ist so Aber die Idee sieht nicht attraktiv aus. Hier ist der Analysator der böse Kerl, der einen Programmierer eher verwirren wird, als ihm zu helfen.

Vierte Nuance. Hochspezialisierte Sprachen.

Der beschriebene Ansatz ist nicht auf hochspezialisierte Sprachen anwendbar, für die die statische Analyse ebenfalls äußerst nützlich sein kann. Der Grund dafür ist, dass GitHub und andere Quellen einfach keine ausreichende Quellcodebasis haben, um effektives Lernen zu ermöglichen.

Schauen wir uns das an einem konkreten Beispiel an. Lassen Sie uns zunächst zu GitHub gehen und nach Repositorys für die beliebte Java-Sprache suchen.

Ergebnis: Sprache: "Java": 3.128.884 verfügbare Repository-Ergebnisse

Nehmen wir nun die Fachsprache „1C Enterprise“, die in Buchhaltungsanwendungen der russischen Firma 1C verwendet wird .

Ergebnis: Sprache: "1C Enterprise": 551 verfügbare Repository-Ergebnisse

Vielleicht werden für diese Sprache keine Analysatoren benötigt? Nein, das sind sie. Es besteht ein praktischer Bedarf, solche Programme zu analysieren, und es gibt bereits geeignete Analysegeräte. Zum Beispiel gibt es das SonarQube 1C (BSL) Plugin der Firma " Silver Bullet ".

Ich denke, es sind keine spezifischen Erklärungen erforderlich, warum der ML-Ansatz für Fachsprachen schwierig sein wird.

Die fünfte Nuance. C, C ++, #include .

In Artikeln zur ML-basierten statischen Code-Analyse geht es hauptsächlich um Sprachen wie Java, JavaScript und Python. Dies erklärt sich durch ihre extreme Beliebtheit. C und C ++ werden ignoriert, obwohl man sie nicht als unbeliebt bezeichnen kann.

Wir schlagen vor, dass es nicht um ihre Popularität / vielversprechende Aussichten geht, sondern um die Probleme mit C- und C ++ - Sprachen. Und jetzt werden wir ein unangenehmes Problem ans Licht bringen.

Eine abstrakte c / cpp-Datei kann sehr schwierig zu kompilieren sein. Zumindest können Sie kein Projekt von GitHub laden, eine zufällige cpp-Datei auswählen und sie einfach kompilieren. Nun werden wir erklären, was dies alles mit ML zu tun hat.

Also wollen wir den Analysator unterrichten. Wir haben ein Projekt von GitHub heruntergeladen. Wir kennen den Patch und gehen davon aus, dass er den Fehler behebt. Wir möchten, dass diese Bearbeitung ein Beispiel für das Lernen ist. Mit anderen Worten, wir haben eine .cpp-Datei vor und nach der Bearbeitung.

Hier beginnt das Problem. Es reicht nicht aus, nur die Korrekturen zu studieren. Der vollständige Kontext ist ebenfalls erforderlich. Sie müssen die Deklaration der verwendeten Klassen kennen, Sie müssen die Prototypen der verwendeten Funktionen kennen, Sie müssen wissen, wie Makros erweitert werden und so weiter. Dazu müssen Sie eine vollständige Dateivorbearbeitung durchführen.

Schauen wir uns das Beispiel an. Der Code sah zunächst so aus:

 bool Class::IsMagicWord() { return m_name == "ML"; } 

Es wurde auf diese Weise behoben:

 bool Class::IsMagicWord() { return strcmp(m_name, "ML") == 0; } 

Sollte der Analysator anfangen zu lernen, um (x == "y") Ersatz für strcmp (x, "y") vorzuschlagen?

Sie können diese Frage nicht beantworten, ohne zu wissen, wie das Member m_name in der Klasse deklariert ist. Es könnte zum Beispiel solche Optionen geben:

 class Class { .... char *m_name; }; class Class { .... std::string m_name; }; 

Änderungen werden vorgenommen, falls es sich um einen normalen Zeiger handelt. Wenn wir den Variablentyp nicht berücksichtigen, lernt der Analysator möglicherweise, sowohl gute als auch schlechte Warnungen auszugeben (für den Fall mit std :: string ).

Klassendeklarationen befinden sich normalerweise in Header-Dateien. Hier bestand die Notwendigkeit, eine Vorverarbeitung durchzuführen, um alle notwendigen Informationen zu erhalten. Es ist extrem wichtig für C und C ++.

Wenn jemand sagt, dass es möglich ist, auf eine Vorverarbeitung zu verzichten, ist er entweder ein Betrüger oder nur mit C- oder C ++ -Sprachen nicht vertraut.

Um alle notwendigen Informationen zu sammeln, benötigen Sie eine korrekte Vorverarbeitung. Dazu müssen Sie wissen, wo und welche Header-Dateien sich befinden und welche Makros während des Erstellungsprozesses festgelegt werden. Sie müssen auch wissen, wie eine bestimmte cpp-Datei kompiliert wird.

Das ist das Problem. Man kompiliert die Datei nicht einfach (oder spezifiziert vielmehr den Schlüssel für den Compiler, damit er eine Vorverarbeitungsdatei erzeugt). Wir müssen herausfinden, wie diese Datei kompiliert wird. Diese Informationen sind in den Build-Skripten enthalten, die Frage ist jedoch, wie Sie sie von dort abrufen können. Im Allgemeinen ist die Aufgabe kompliziert.



Darüber hinaus sind viele Projekte auf GitHub ein Chaos. Wenn Sie ein abstraktes Projekt von dort nehmen, müssen Sie oft basteln, um es zu kompilieren. Eines Tages fehlt Ihnen eine Bibliothek und Sie müssen sie manuell suchen und herunterladen. An einem anderen Tag wird eine Art selbst geschriebenes Build-System verwendet, das behandelt werden muss. Es könnte alles sein. Manchmal lässt sich das heruntergeladene Projekt einfach nicht erstellen und muss irgendwie angepasst werden. Sie können nicht einfach eine vorverarbeitete (.i) Darstellung für .cpp-Dateien erstellen und automatisch abrufen. Es kann schwierig sein, auch wenn Sie es manuell tun.

Wir können sagen, dass das Problem mit Nichtbauprojekten verständlich, aber nicht entscheidend ist. Arbeiten wir nur mit Projekten, die erstellt werden können. Es bleibt noch die Aufgabe, eine bestimmte Datei vorzuverarbeiten. Ganz zu schweigen von den Fällen, in denen wir uns mit einigen spezialisierten Compilern befassen, zum Beispiel für eingebettete Systeme.

Immerhin ist das beschriebene Problem nicht unüberwindbar. All dies ist jedoch sehr schwierig und arbeitsintensiv. Im Falle von C und C ++ hat der auf GitHub befindliche Quellcode keine Wirkung. Es ist viel zu tun, um zu lernen, wie Compiler automatisch ausgeführt werden.

Hinweis Wenn der Leser die Tiefe des Problems immer noch nicht kennt, laden wir Sie ein, an dem folgenden Experiment teilzunehmen. Nehmen Sie zehn mittelgroße Zufallsprojekte aus GitHub und versuchen Sie, sie zu kompilieren und erhalten Sie dann ihre vorverarbeitete Version für CPP-Dateien. Danach verschwindet die Frage nach der Mühsamkeit dieser Aufgabe :).

Es kann ähnliche Probleme mit anderen Sprachen geben, die jedoch in C und C ++ besonders offensichtlich sind.

Sechste Nuance. Der Preis für die Beseitigung von Fehlalarmen.

Statische Analysegeräte neigen dazu, Fehlalarme zu generieren, und wir müssen die Diagnose ständig verfeinern, um die Anzahl der Fehlalarme zu verringern.

Jetzt kehren wir zur zuvor betrachteten V789- Diagnose zurück und erkennen Containeränderungen innerhalb der bereichsbasierten for-Schleife. Nehmen wir an, wir waren beim Schreiben nicht vorsichtig genug und der Kunde meldet ein falsches Positiv. Er schreibt, dass der Analysator das Szenario nicht berücksichtigt, wenn die Schleife endet, nachdem der Container gewechselt wurde, und daher kein Problem besteht. Dann gibt er das folgende Codebeispiel an, in dem der Analysator ein falsches Positiv ausgibt:

 std::vector<int> numbers; .... for (int num : numbers) { if (num < 5) { numbers.push_back(0); break; // or, for example, return } } 

Ja, das ist ein Fehler. In einem klassischen Analysegerät ist die Eliminierung extrem schnell und kostengünstig. In PVS-Studio besteht die Implementierung dieser Ausnahme aus 26 Codezeilen.

Dieser Fehler kann auch behoben werden, wenn der Analysator auf Lernalgorithmen basiert. Es kann mit Sicherheit gelehrt werden, indem Dutzende oder Hunderte von Codebeispielen gesammelt werden, die als korrekt angesehen werden sollten.

Auch hier geht es nicht um Machbarkeit, sondern um praktischen Ansatz. Wir vermuten, dass der Kampf gegen bestimmte Fehlalarme, die die Kunden stören, bei ML weitaus kostspieliger ist. Das heißt, die Kundenunterstützung bei der Beseitigung von Fehlalarmen kostet mehr Geld.

Siebte Nuance. Selten genutzte Features und langer Schwanz.

Bisher haben wir uns mit dem Problem der hochspezialisierten Sprachen auseinandergesetzt, für die möglicherweise nicht genug Quellcode zum Lernen vorhanden ist. Ein ähnliches Problem tritt bei selten verwendeten Funktionen auf (Systemfunktionen, WinAPI, aus gängigen Bibliotheken usw.).

Wenn wir über solche Funktionen aus der C-Sprache sprechen, wie strcmp , dann gibt es tatsächlich eine Lernbasis . GitHub, verfügbare Code-Ergebnisse:

  • strcmp - 40,462,158
  • stricmp - 1,256,053

Ja, es gibt viele Anwendungsbeispiele. Vielleicht lernt der Analysator, zum Beispiel die folgenden Muster zu bemerken:

  • Es ist seltsam, wenn die Zeichenfolge mit sich selbst verglichen wird. Es wird behoben.
  • Es ist seltsam, wenn einer der Zeiger NULL ist. Es wird behoben.
  • Es ist seltsam, dass das Ergebnis dieser Funktion nicht verwendet wird. Es wird behoben.
  • Und so weiter.

Ist es nicht cool? Nr Hier stehen wir vor dem Problem des "langen Schwanzes". Ganz kurz die Spitze des "langen Schwanzes" im Folgenden. Es ist unpraktisch, nur die Top50 der beliebtesten und derzeit gelesenen Bücher in einem Buchladen zu verkaufen. Ja, jedes dieser Bücher wird beispielsweise 100-mal häufiger gekauft als Bücher, die nicht von dieser Liste stammen. Der Großteil des Erlöses wird jedoch aus anderen Büchern bestehen, die, wie sie sagen, ihren Leser finden. Zum Beispiel erhält ein Online-Shop Amazon.com mehr als die Hälfte der Gewinne aus nicht mehr als 130.000 "beliebtesten Artikeln".

Es gibt beliebte Funktionen, von denen es nur wenige gibt. Es gibt unpopuläre, aber es gibt viele von ihnen. Beispielsweise gibt es die folgenden Variationen der Zeichenfolgenvergleichsfunktion:

  • g_ascii_strncasecmp - 35.695
  • lstrcmpiA - 27,512
  • _wcsicmp_l - 5.737
  • _strnicmp_l - 5.848
  • _mbscmp_l - 2.458
  • und andere.

Wie Sie sehen, werden sie viel seltener verwendet, aber wenn Sie sie verwenden, können Sie dieselben Fehler machen. Es gibt zu wenige Beispiele, um Muster zu identifizieren. Diese Funktionen können jedoch nicht ignoriert werden. Einzeln werden sie selten verwendet, aber mit ihrer Verwendung wird viel Code geschrieben, der besser überprüft werden sollte. Hier zeigt sich der "lange Schwanz".

Bei PVS-Studio werden Funktionen manuell mit Anmerkungen versehen. Zum Beispiel wurden bis jetzt ungefähr 7.200 Funktionen für C und C ++ kommentiert. Dies ist, was wir markieren:

  • Winapi
  • Standard C-Bibliothek,
  • Standard Template Library (STL),
  • glibc (GNU C-Bibliothek)
  • Qt
  • Mfc
  • zlib
  • libpng
  • Öffnetsl
  • und andere.

Einerseits scheint es eine Sackgasse zu sein. Sie können nicht alles mit Anmerkungen versehen. Auf der anderen Seite funktioniert es.

Nun ist hier die Frage. Welche Vorteile kann ML haben? Wesentliche Vorteile sind nicht so offensichtlich, aber Sie können die Komplexität sehen.

Man könnte argumentieren, dass Algorithmen, die auf ML selbst aufbauen, Muster mit häufig verwendeten Funktionen finden und nicht kommentiert werden müssen. Ja, das stimmt. Es ist jedoch kein Problem, beliebte Funktionen wie strcmp oder malloc unabhängig voneinander zu kommentieren.

Trotzdem bereitet der lange Schwanz Probleme. Sie können unterrichten, indem Sie synthetische Beispiele erstellen. Hier kehren wir jedoch zum Artikelteil zurück, in dem wir sagten, es sei einfacher und schneller, klassische Diagnosen zu schreiben, als viele Beispiele zu generieren.

Nehmen Sie zum Beispiel eine Funktion wie _fread_nolock . Natürlich wird es seltener als Fread verwendet . Aber wenn Sie es verwenden, können Sie die gleichen Fehler machen. Beispielsweise sollte der Puffer groß genug sein. Diese Größe sollte nicht kleiner sein als das Ergebnis der Multiplikation des zweiten und dritten Arguments. Das heißt, Sie möchten einen solchen falschen Code finden:

 int buffer[10]; size_t n = _fread_nolock(buffer, size_of(int), 100, stream); 

So sieht die Annotation dieser Funktion in PVS-Studio aus:

 C_"size_t _fread_nolock" "(void * _DstBuf, size_t _ElementSize, size_t _Count, FILE * _File);" ADD(HAVE_STATE | RET_SKIP | F_MODIFY_PTR_1, nullptr, nullptr, "_fread_nolock", POINTER_1, BYTE_COUNT, COUNT, POINTER_2). Add_Read(from_2_3, to_return, buf_1). Add_DataSafetyStatusRelations(0, 3); 

Auf den ersten Blick mag eine solche Annotation schwierig aussehen, aber tatsächlich wird es einfach, wenn Sie anfangen, sie zu schreiben. Plus, es ist schreibgeschützter Code. Schrieb und vergaß. Anmerkungen ändern sich selten.

Lassen Sie uns nun über diese Funktion aus der Sicht von ML sprechen. GitHub wird uns nicht helfen. Es gibt ungefähr 15.000 Erwähnungen dieser Funktion. Es gibt noch weniger guten Code. Ein wesentlicher Teil der Suchergebnisse nimmt Folgendes ein:

 #define fread_unlocked _fread_nolock 

Welche Möglichkeiten gibt es?
  1. Tu nichts. Es ist ein Weg ins Nirgendwo.
  2. Stellen Sie sich vor, Sie lehren den Analysator, indem Sie Hunderte von Beispielen nur für eine Funktion schreiben, damit der Analysator die Verbindung zwischen dem Puffer und anderen Argumenten versteht. Ja, das können Sie, aber es ist wirtschaftlich irrational. Es ist eine Sackgasse.
  3. Sie können einen ähnlichen Weg wie den unseren finden, wenn die Anmerkungen zu Funktionen manuell festgelegt werden. Es ist ein guter, vernünftiger Weg. Das ist nur ML, was nichts damit zu tun hat :). Dies ist ein Rückfall auf die klassische Art, statische Analysatoren zu schreiben.

Wie Sie sehen, passen ML und der lange Schwanz der selten verwendeten Features nicht zusammen.

Zu diesem Zeitpunkt gab es mit ML verwandte Personen, die Einwände erhoben und sagten, dass wir nicht in Betracht gezogen hätten, wann der Analysator alle Funktionen lernen und Schlussfolgerungen daraus ziehen würde, was er tut. Hier verstehen wir anscheinend entweder die Experten nicht oder sie verstehen unseren Standpunkt nicht.

Körper von Funktionen können unbekannt sein. Beispielsweise könnte es sich um eine WinAPI-bezogene Funktion handeln. Wenn dies eine selten verwendete Funktion ist, wie wird der Analysator verstehen, was er tut? Wir können uns vorstellen, dass der Analysator Google selbst verwendet, eine Beschreibung der Funktion findet, sie liest und versteht . Darüber hinaus müsste es aus der Dokumentation Schlussfolgerungen auf hoher Ebene ziehen. Die Beschreibung von _fread_nolock sagt nichts über die Verbindung zwischen dem Puffer, dem zweiten und dem dritten Argument aus. Dieser Vergleich sollte durch künstliche Intelligenz allein abgeleitet werden, basierend auf einem Verständnis der allgemeinen Prinzipien der Programmierung und der Funktionsweise der C ++ - Sprache. Ich denke, wir sollten in 20 Jahren ernsthaft darüber nachdenken.

Möglicherweise sind Funktionskörper verfügbar, die jedoch möglicherweise nicht verwendet werden können. Sehen wir uns eine Funktion wie memmove an . Es wird oft so implementiert:

 void *memmove (void *dest, const void *src, size_t len) { return __builtin___memmove_chk(dest, src, len, __builtin_object_size(dest, 0)); } 

Was ist __builtin___memmove_chk ? Dies ist eine intrinsische Funktion, die der Compiler selbst bereits implementiert. Diese Funktion verfügt nicht über den Quellcode.

Oder memmove sieht ungefähr so aus: Die erste Assembly-Version . Sie können dem Analysator beibringen, verschiedene Montageoptionen zu verstehen, aber ein solcher Ansatz scheint falsch zu sein.

Ok, manchmal sind Körper von Funktionen wirklich bekannt. Darüber hinaus kennen wir auch Körper von Funktionen im Code des Benutzers. Es scheint, dass ML in diesem Fall enorme Vorteile hat, wenn man liest und versteht, was all diese Funktionen bewirken.

Aber auch in diesem Fall sind wir voller Pessimismus. Diese Aufgabe ist zu komplex. Es ist sogar für einen Menschen kompliziert. Überlegen Sie, wie schwierig es für Sie ist, den Code zu verstehen, den Sie nicht geschrieben haben. Wenn es für eine Person schwierig ist, warum sollte diese Aufgabe für eine KI einfach sein? Tatsächlich hat AI ein großes Problem beim Verständnis von Konzepten auf hoher Ebene.Wenn wir über das Verstehen des Codes sprechen, können wir nicht auf die Fähigkeit verzichten, die Details der Implementierung zu abstrahieren und den Algorithmus auf einer hohen Ebene zu betrachten. Es scheint, dass diese Diskussion auch um 20 Jahre verschoben werden kann.

Andere Nuancen

Es gibt andere Punkte, die ebenfalls berücksichtigt werden sollten, auf die wir jedoch nicht näher eingegangen sind. Der Artikel fällt übrigens recht lang aus. Deshalb werden wir einige andere Nuancen kurz auflisten und sie dem Leser zum Nachdenken überlassen.

  • Outdated recommendations. As mentioned, languages change, and recommendations for their use change, respectively. If the analyzer learns on old source code, it might start issuing outdated recommendations at some point. Example. Formerly, C++ programmers have been recommended using auto_ptr instead of half-done pointers. This smart pointer is now considered obsolete and it is recommended that you use unique_ptr .
  • Data models. At the very least, C and C++ languages have such a thing as a data model . This means that data types have different number of bits across platforms. If you don't take this into account, you can incorrectly teach the analyzer. For example, in Windows 32/64 the long type always has 32 bits. But in Linux, its size will vary and take 32/64 bits depending on the platform's number of bits. Without taking all this into account, the analyzer can learn to miscalculate the size of the types and structures it forms. But the types also align in different ways. All this, of course, can be taken into account. You can teach the analyzer to know about the size of the types, their alignment and mark the projects (indicate how they are building). However, all this is an additional complexity, which is not mentioned in the research articles.
  • Behavioral unambiguousness. Since we're talking about ML, the analysis result is more likely to have probabilistic nature. That is, sometimes the erroneous pattern will be recognized, and sometimes not, depending on how the code is written. From our experience, we know that the user is extremely irritated by the ambiguity of the analyzer's behavior. He wants to know exactly which pattern will be considered erroneous and which will not, and why. In the case of the classical analyzer developing approach, this problem is poorly expressed. Only sometimes we need to explain our clients why there is a/there is no analyzer warning and how the algorithm works, what exceptions are handled in it. Algorithms are clear and everything can always be easily explained. An example of this kind of communication: " False Positives in PVS-Studio: How Deep the Rabbit Hole Goes ". It's not clear how the described problem will be solved in the analyzers built on ML.

Conclusions


Wir bestreiten nicht die Aussichten der ML-Richtung, einschließlich ihrer Anwendung in Bezug auf die statische Code-Analyse. ML kann potenziell bei der Suche nach Tippfehlern, beim Filtern von False Positives, bei der Suche nach neuen (noch nicht beschriebenen) Fehlermustern usw. verwendet werden. Wir teilen jedoch nicht den Optimismus, der die Artikel über ML in Bezug auf die Code-Analyse durchdringt.

In diesem Artikel haben wir einige Probleme umrissen, an denen man arbeiten muss, wenn er ML verwenden will. Die beschriebenen Nuancen machen die Vorteile des neuen Ansatzes weitgehend zunichte. Darüber hinaus sind die alten klassischen Ansätze der Implementierung von Analysatoren rentabler und wirtschaftlicher.

Interessanterweise erwähnen die Artikel der Anhänger der ML-Methodik diese Fallstricke nicht. Naja, nichts neues. ML ist ein gewisser Hype, und wir sollten wahrscheinlich keine ausgewogene Einschätzung der ML-Anwendbarkeit bei statischen Code-Analyse-Aufgaben von seinen Entschuldigern erwarten.

Aus unserer Sicht wird maschinelles Lernen eine Nische in Technologien füllen, die in statischen Analysegeräten zusammen mit Kontrollflussanalysen, symbolischen Ausführungen und anderen verwendet werden.

Die Methode der statischen Analyse kann von der Einführung von ML profitieren, aber übertreiben Sie nicht die Möglichkeiten dieser Technologie.

PS


Da der Artikel im Allgemeinen kritisch ist, könnten einige denken, wir fürchten das Neue und als Luddites sich gegen ML wandte, weil sie befürchten, den Markt für statische Analysewerkzeuge zu verlieren.

Luddites


Nein, wir haben keine Angst. Wir sehen keinen Sinn darin, Geld für ineffiziente Ansätze in der Entwicklung des PVS-Studio-Code-Analysators auszugeben. In der einen oder anderen Form werden wir ML übernehmen. Darüber hinaus enthalten einige Diagnosen bereits Elemente von selbstlernenden Algorithmen. Wir werden jedoch auf jeden Fall sehr konservativ sein und nur das nehmen, was eindeutig eine größere Wirkung hat als die klassischen Ansätze, die auf Schleifen und Ifs basieren :). Schließlich müssen wir ein effektives Tool erstellen, nicht einen Zuschuss abarbeiten :).

Der Artikel wurde aus dem Grund geschrieben, dass immer mehr Fragen zum Thema gestellt werden und wir wollten einen Expository-Artikel, der alles an seine Stelle stellt.

Vielen Dank für Ihre Aufmerksamkeit. Wir laden Sie ein, den Artikel zu lesen "Warum Sie sich für den statischen Analysator von PVS-Studio entscheiden sollten, um ihn in Ihren Entwicklungsprozess zu integrieren . "

Source: https://habr.com/ru/post/de484202/


All Articles