Finden Sie toten Code in einem C-Programm durch Laufzeitanalyse

Ich habe eine große Anwendung, die in C + POSIX geschrieben ist. Es gibt viele Funktionen, die darin nie aufgerufen werden. Aufgrund der Größe des Codes ist es jedoch schwierig, sie manuell zu verfolgen.
Einige Leute haben vorgeschlagen, gcc mit -Wunusedund lto zu verwenden, aber es wurden keine verwendeten Funktionen zurückgegeben, während ich weiterhin einige manuell finde und entferne.

Ich denke also, ich brauche ein Code-Coverage-Tool, um das Programm zur Laufzeit zu analysieren. Peoples schlug mir gcov oder valgrind vor, aber ich konnte nicht finden, wie man damit eine Liste toter Funktionen ausdruckt. gcov allein ergab, dass nur 68 % der kompilierten Funktionen verwendet werden, aber ich habe keine Möglichkeit, sie aufzulisten.

Kennt jemand ein gutes Tool, und wenn ja, sagen Sie mir, wie genau ich es für diesen Zweck verwenden kann (ein Befehlszeilenbeispiel wäre willkommen) ?

Ich habe alle Funktionen entfernt, die im Quellcode nicht verwendet werden. Nur Funktionen wie diese verbleiben im Quellcode:

if(conditional statement) {
    some stuff;
    dead_function();
    some_stuff;
}

Where conditional statementist zur Laufzeit nie wahr, und das Entfernen von dead_function()würde zum Entfernen der Anweisung führen, um undefinierte Fehler zu vermeiden.

Was Sie also suchen, sind eigentlich nicht ungenutzte Funktionen ( dead_function wird verwendet), sondern toter Code. Das erfordert ganz andere Techniken! Beachten Sie, dass die Laufzeitanalyse nur Code findet, der zufällig nicht in einer bestimmten Ausführung des Programms ausgeführt wurde – dieser Code kann unter anderen Umständen live sein.
@Gilles: Das ist irgendwie das Problem von gcov ... Wie kombiniert man das Ergebnis verschiedener Starts? Ich habe für den Parameter. Das heißt, das Programm macht nur eines: sehr große Primzahlen finden, so dass es einfach ist, alle Fälle zu testen. Aus diesem Grund versuche ich auch, toten Code zu entfernen (leistungskritisch) .
Anscheinend möchten Sie keine tote Funktion entfernen, sondern tote bedingte Codes entfernen. Habe ich das Recht?
@IraBaxter: Nein, weil ich mich derzeit nicht wirklich um unbenutzte Anweisungen innerhalb von Funktionen kümmere. Ich möchte nur die Funktionen entfernen, die völlig nutzlos sind (wie die im Umgang mit Floats) .
Denken Sie daran, dass Code, der nie aufgerufen wird, außer der Ladezeit und dem Speicherbedarf nur sehr geringe Auswirkungen auf die Leistung hat. Um beides zu unterstützen, verschieben Sie so viel Code wie möglich in viele kleine DLLs oder gemeinsam genutzte .so-Bibliotheken - diejenigen, die nie wirklich verwendet werden, werden nie geladen. Wenn Sie schwerwiegende Leistungsprobleme haben, haben Sie wahrscheinlich Code, der aufgerufen wird, aber die Ergebnisse werden nie verwendet - lint kann helfen, diese zu finden.
Obwohl ich zustimme, dass es unansehnlich ist, unbenutzten Code herumliegen zu haben (einschließlich Code, der auskommentiert ist), können Sie sich mit dem Wissen trösten, dass der Linker in der Lage sein sollte, sich darum zu kümmern. Wenn Sie ein Tool finden, vergewissern Sie sich, dass es Funktionszeiger korrekt behandelt. In meiner Arbeit verlassen wir uns beispielsweise auf en.wikipedia.org/wiki/Finite-state_machine , die von en.wikipedia.org/wiki/Branch_table gesteuert werden , sodass unsere wichtigsten Funktionen niemals direkt, sondern nur indirekt aufgerufen werden. Dasselbe kann bei Rückrufen auftreten.
@Mawg: Der Linker kann dies nur tun, wenn kein Aufruf von anderen Funktionen erfolgt. Aber in meinem Fall existieren die Aufrufe, aber sie werden nie verwendet, wenn das Programm ausgeführt wird. Ich erkenne, dass dies mit der Entfernung von totem bedingtem Code zusammenhängt.
@SteveBarnes: Code, der nie für ein kleines Programm aufgerufen wird, wirkt sich aufgrund von CPU-Caches aus. Die Verwendung gemeinsam genutzter Bibliotheken würde nur für Funktionen funktionieren, die keine verknüpften Aufrufe innerhalb des Programms haben, nicht für solche, die zur Laufzeit nie aufgerufen werden. Dasselbe gilt für die Linker-Lösung.
Die einzige "kostenlose" oder nahezu kostenlose Lösung, die ich für den Anwendungsfall kenne, bei dem die Funktion vom Code auf höherer Ebene "verwendet", aber in Wirklichkeit nie aufgerufen wird, besteht darin, a) einige Tests zu schreiben, von denen Sie sicher sind, dass sie alle Anwendungsfälle abdecken b) Fügen Sie in jeder Funktion printf(__func__);als erste Zeile hinzu, in gcc wird der Name der Funktion bei jedem Aufruf gedruckt - führen Sie Ihre Testerfassungsausgabe aus, dann wird jeder Name, der nicht in der Ausgabe enthalten ist, nicht aufgerufen.
@SteveBarnes: Es gab viele Funktionen, von denen ich wünschte, ich könnte es automatisch tun, wie gcov es bereits tut. Vielleicht habe ich nur eine Option vergessen, um sie aufzulisten, aber ich kann sie nicht finden.
Verwenden Sie Ihre Map-Datei, um eine Liste aller Funktionen zu erhalten, die im Code enthalten sind, und gcov, um alle aufgerufenen Funktionen aufzulisten. Subtrahieren Sie diese von der ersten Liste, und Sie haben eine Liste der Funktionen, die nicht aufgerufen wurden. Ein kurzes Python-Skript oder etwas Awk-Magie sollte ausreichen.
Wenn Sie gcovr, gcovr.com/guide.html verwenden , sollten Sie natürlich in der Lage sein, Codezeilen schnell zu identifizieren, die nie ausgeführt werden.
@SteveBarnes: Ich habe die Anleitungen falsch befolgt und kann die entsprechende Option nicht finden, mit der die Funktionen aufgelistet werden können, die nicht Teil der 68 % sind, die in der gcov-Ausgabe verwendet werden.
@ user2284570 - siehe meine neue Antwort.
Egal welche Lösung, es gibt Probleme wenn Sprungtabellen verwendet werden (Funktionszeiger)
@Mawg Ich verwende Sprungtabellen nicht direkt. Vielleicht kann der Compiler einige automatisch generieren, aber ich verwende sie nicht.
Das tun nicht viele. Wir neigen dazu, sie stark eingebettet zu verwenden, für die Zustands-/Geraden-Kopplung. Hüten Sie sich auch vor Callback-Funktionen.

Antworten (3)

Da sich @user2284570 in der komfortablen Situation befindet, 100 % der Anwendungsfälle durch Tests abzudecken, wird die dynamische Codeanalyse die Antwort liefern. In anderen Fällen würde das Entfernen von Funktionen, deren Aufrufen und Bedingungen eine gründliche Überprüfung erfordern.

Jedes Codeabdeckungstool meldet die Funktionsabdeckung in der einen oder anderen Form. Der Hauptkritikpunkt scheint die Meldung der genauen Position ungenutzter (hier: toter) Funktionen zu sein. Ich kann nicht für andere Tools sprechen, aber das unseres Unternehmens verfügt über eine Berichtsoption im Textformat, die Platzhalter für Quelldateinamen und Zeilendaten enthält. Da nach einem konkreten Kommandozeilenbeispiel gefragt wurde hier eines:

$ csgcc -o myapp mycode.c
$ ./myapp --run-all-tests
$ cmcsexeimport -m myapp.csmes -e myapp.csexe --title=mytests
$ cmreport --function-coverage -m myapp.csmes --format-unexecuted='%f:%l'

Dadurch werden die toten Funktionsorte wie folgt gedruckt:

mycode.c:101
mycode.c:213
mycode.c:1032

Ein ortsansässiger Universitätsstudent verfasste detailliertere Anweisungen für diesen Ansatz.

Nachdem Sie ungenutzte Funktionen eliminiert haben, sollten Sie auch die Zweigabdeckung analysieren und überflüssige if()-Anweisungen und andere entfernen. Hüten Sie sich nur vor Nebeneffekten ausgewerteter Ausdrücke. Aber zum Glück erkennt Ihre perfekte Codeabdeckung Regressionen.

Ich muss sagen, dass ich seit dieser Zeit das schrittweise manuelle Debuggen verwendet habe, um toten Code zu entfernen. Also werde ich dein Tool nicht ausprobieren.

Sie können splintmit dem alluseFlag nach nicht verwendeten Funktionen suchen, aber ich persönlich würde verwenden doxygen, um eine Aufrufzuordnung zu erstellen - alle Funktionen, die keine Eltern haben, werden wahrscheinlich nicht verwendet - suchen Sie einfach nach Funktionen, die sich in Tabellen von Funktionen befinden, die möglicherweise nicht direkt aufgerufen werden aber Dinge wie Zustandsmaschinen könnten aus dem Tabellenindex aufgerufen werden.

Doxygen ist ein unschätzbares Werkzeug für den Umgang mit großen Codebasen und es lohnt sich auf jeden Fall, seine Verwendung zu lernen, es ist kostenlos und für mehrere Plattformen verfügbar, es neigt auch dazu, die Dokumentation Ihres Codes zu fördern, während Sie fortfahren.

Im Fall von Code, der aufgerufen wird, aber nur von unerreichbarem Code, müssen Sie ein vollständiges statisches Analysetool wie LDRA (kostspielig) verwenden, das Sie auf unerreichbaren Code hinweist. In diesem Fall ist es besser, zuerst den gesamten nicht erreichbaren Code zu entfernen und dann nach nicht aufgerufenen Funktionen zu suchen. Alternativ benötigen Sie eine Testsuite, von der Sie sicher sind, dass sie 100 % der Funktionalität ausführt. Dann können Sie einen Profiler oder ein Abdeckungstool wie gcov in Ihrem Programm verwenden, während Sie Ihre Testsuite ausführen. Wenn Ihr Test Ihre gesamte Funktionalität ausgeführt hat und Sie Teile mit einer Abdeckung von 0 % haben, werden sie nicht aufgerufen , aber Sie müssen dann die Aufrufe finden, die nicht erreichbar sind, und diesen Code entfernen, damit sich der Linker trotzdem nicht beschwert.

" Sie können Splint mit dem Alluse-Flag verwenden " Ich verstehe immer noch nicht. Können Sie ein Beispiel für die Befehlszeile angeben? Für Doxygen lautet die Antwort Nein! Ich habe gerade alle Funktionen entfernt, die nicht im Code verwendet wurden. Es bleiben nur diejenigen übrig, die im Code verwendet werden, aber einige bedingte Anweisungen dazu führen, dass sie zur Laufzeit nie aufgerufen werden. und eine Anrufkarte kann bei Hunderten von Funktionen nicht helfen. Ich brauche unbedingt eine lesbare Ausgabe mit aussortierten nicht aufgerufenen Funktionen und mit den Dateinamen und Zeilennummern des Bereichs, in dem sie deklariert sind.
Stahl kein detailliertes Beispiel ... Also verstehe ich nicht, was ich tun kann ...
@ user2284570 Diese Antwort ist richtig für die Frage, wie Sie sie ursprünglich gepostet haben. Sie sollten Fragen nicht so ändern, dass die Antworten ungültig werden! Es wäre besser, wenn Sie Ihre „Update“-Bearbeitung auf die Frage zurücksetzen und stattdessen eine neue Frage stellen würden.
Hüten Sie sich natürlich vor Funktionen, die per Funktionszeiger aufgerufen werden, wie Callbacks und Zustands-/Ereignis-Sprungtabellen.
@Mawg solche Funktionen gab es nicht.

Um Ihre überarbeitete Frage zu beantworten: Wenn Sie Ihren gesamten Code mit gcc -fprofile-arcs -ftest-coveragefestgelegten Optionen kompilieren und dann eine Reihe von Tests ausführen, von denen Sie sicher sind, dass sie alle Funktionen und alle Möglichkeiten abdecken (möglicherweise über mehrere Durchläufe hinweg).

Das gcovDienstprogramm erwartet, dass Sie einen Teil der Arbeit erledigen - es hat nicht nur eine Option für "Sagen Sie mir, was nicht aufgerufen wurde", sodass Sie die Funktionen finden müssen, die nicht aufgerufen wurden.

Sie können mit gcovder --function-summariesOption für jede Quelldatei eine Reihe von Ausgabedateien generieren, die Funktionszusammenfassungen enthalten. Suchen Sie nach Dateien , die entweder neveroder enthalten 0%, um die nicht ausgeführten Funktionen zu finden.

Ich würde vorschlagen, entweder eine Funktion hinzuzufügen, von der Sie wissen, dass sie niemals aufgerufen wird, oder eine zu kennen, die Sie noch nicht entfernt haben - so können Sie sehen, wie die Ausgabe aussieht - Sie können sie dann verwenden grep, um sie alle zu finden.

Ihr nächster Schritt besteht darin, grepdie gcovAusgabe für alle Stellen zu verwenden, an denen diese Funktionen in Ihrem Code vorhanden sind. Sie sollten Ausführungszähler von 0 für den gesamten Zweig sehen, der den Aufruf enthält . Dies gibt Ihnen einen guten Ausgangspunkt um entweder Ihre Tests zu erweitern - für Anwendungsfälle, die Sie verpasst haben - oder um Code zu entfernen.

Dann kann es genau das nicht tun, was ich brauche: Ungenutzte Funktionen aussortieren und die Zeilen entlang der Dateipfade angeben. Es ist dasselbe, was ich mit kcachegrind gemacht habe, bevor ich diese Frage gestellt habe. Das manuelle Aussortieren der Funktion ist in Ordnung und das Suchen nach ihnen ist in Ordnung, aber nicht, wenn Sie viele haben.
Ich schätze, Sie müssen jemanden einstellen, der codieren kann , um es dann zu tun! Tools werden Ihnen nicht viel abnehmen, sonst wären Sie überflüssig.
Ich brauche nichts für die automatische Entfernung. Ich brauche etwas, das ungenutzte Funktionen zur Laufzeit ohne verwendete Funktion in dieser Liste auflisten kann. Ich brauche auch ihren Deklarationspfad. Auch ich mache das für mich. Erwarten Sie also, dass ich einige Jahre lang keine eigene Antwort schreiben werde.
Steel kann ungenutzte Funktionen nicht automatisch aus aufgerufenen aussortieren… In der Zwischenzeit habe ich die Rechenanforderungen für die Generierung sicherer zufälliger Primzahlen erkannt, die auf 65Ko passen.