Trace-Techniken erlauben dem Programmierer einen umfassenden Einblick in die Anwendung. Damit können sie jede einzelne Anweisung nachverfolgen und sehen, ob ihre Anwendung wie vorgesehen läuft oder ob Fehler oder Schwachstellen vorliegen.

Bild 1: Trace eines RISC-V-Prozessors. Die RISC-V-Foundation erarbeitet derzeit die Spezifikationen für Trace und hat für die Processor Trace Task Group gegründet.

Bild 1: Trace eines RISC-V-Prozessors. Die RISC-V-Foundation erarbeitet derzeit die Spezifikationen für Trace und hat für die Processor Trace Task Group gegründet. IAR Systems

Im Gegensatz zum normalen Debugging mit Setzen von Haltepunkten, printf-Ausgaben und dergleichen wird beim Tracing eine Anwendung nur beobachtet, ohne in sie einzugreifen. Im Grunde genommen wird die Funktion einer Anwendung überwacht, ohne sie zu stören. Trace kann einen vollständigen PC-Ablauf umfassen, wobei weder printf noch UART notwendig sind. Sobald Trace-Daten vorliegen, können sie dazu genutzt werden, um in diesen Daten in der Zeit zurückzugehen und Ausnahmen oder schwerwiegende Fehler schnell einzugrenzen.

Dadurch wird das Auffinden von Fehlern, die selten und abhängig von der Reihenfolge der Ausführung sind, viel einfacher. Denn der Entwickler erhält nicht nur einen Informationspfad darüber, was und in welcher Reihenfolge das Programm ausgeführt hat, sondern es lässt sich auf diese Weise auch genau nachvollziehen, wie und warum die Anwendung bei einer bestimmten Codezeile abbricht oder fehlerhaft ausgeführt wird. Schnell lassen sich Ausnahmen oder schwerwiegende Fehler und somit auch Bugs finden, die selten vorkommen oder von der Ausführungsreihenfolge abhängen. Wenn es zu einem Programmabsturz kommt, ist es ohne Trace schwierig, die tatsächliche Ursache dafür zu finden.

Bei Trace geht es jedoch nicht nur darum, Fehler zu finden. Es geht auch um Profiling, Code-Coverage und vieles mehr. Sogar das Verhalten eines Prozessors lässt sich im Livestream beobachten. Performance- und Coverage-Monitoring sind leistungsstarke Funktionen, die bei Trace zur Verfügung stehen. Wenn eine hohe Bandbreite verfügbar ist, lassen sich sogar Trace-Livestreams im Debugger einbinden.

Besonderheiten beim RISC-V-Trace

Einer der hauptsächlichen Arbeitsschwerpunkte der RISC-V Foundation ist aktuell die Erarbeitung einer standardisierten RISC-V Spezifikation. Arbeitsgruppen, denen die Mitgliedsfirmen beitreten können, bemühen sich derzeit um diese Spezifikationen. Ein Beispiel für eine Arbeitsgruppe ist die „Processor Trace Task Group“, die seit Februar 2020 über eine erste ratifizierte Version verfügt, die das Trace-Format beschreibt. Ein weiteres Beispiel ist die Nexus Trace-Arbeitsgruppe, die an Empfehlungen zur Verwendung des Trace-Formats arbeitet, wie es im Nexus-IEEE-ISTO 5001-Standard für RISC-V-Cores definiert ist.

Eck-Daten

Die Visualisierung und Analyse mittels Trace liefert dem Entwickler eine Darstellung seiner laufenden Anwendung und ihres dynamischen Verhaltens. Dies ermöglicht es dem Programmierer, komplexe Fehler im Programmcode zu identifizieren, die anderweitig unentdeckt bleiben. Derartige Analysemethoden sind seit Anfang 2020 nun auch für RISC-V vorgesehen. Integrierte Software-Tools helfen dabei, Trace-Methoden in die tägliche Entwicklungsroutine zu implementieren, was den Software-Entwicklungsprozess beschleunigt und die Code-Qualität erhöht.

Die Arbeit daran wird derzeit fortgesetzt, da alle Aspekte des Trace-Standards zu berücksichtigen sind. Dazu gehören unter anderem auch Trace-Control-Exportformate. Das Minimalziel ist hier, sich an bewährte Architekturen zu halten. Wenn die RISC-V-Trace-Spezifikation ordentlich gemacht wird, lassen sich bestehende Trace-Viewer, Hardware-Trace-Probes und Trace-Analyse-Tools einfach integrieren. Einige wenige Implementierungen sind bereits verfügbar, jedoch benötigt die RISC-V-Architektur eine gute Trace-Spezifikation vom IoT-Device bis hin zu Servern. Selbst ein einfaches Standard-Trace ist besser als überhaupt kein Trace (Bild 1).

Jede Anweisung nachverfolgen

Trace-Funktionen, die in Software-Entwicklungswerkzeugen integriert sind, verbessern die alltägliche Code-Entwicklung und das Debugging. Trace sollte ein fester Bestandteil in der Entwicklungsumgebung sein und keine Analyse, die im Nachhinein stattfindet. So ist es möglich, einen Code zu schreiben, ihn zum Laufen zu bringen und nachzuvollziehen, an welchem Punkt der Ausführung die Anwendung steht. Es können Iterationen stattfinden, um sogleich eine gute Codequalität sicherzustellen, indem in der Timeline Ausnahmen und schwerwiegende Fehler schnell eingegrenzt, Bugs, die von der Ausführungsreihenfolge abhängen, gefunden werden und die Performance im Programmablauf geprüft wird. Alle Analysen lassen sich auch in einer Multicore-Umgebung mit ihren anspruchsvollen und komplexen Abhängigkeiten durchführen.

Die Fehlersuche beim Durchlaufen eines Trace gleicht der sprichwörtlichen Suche nach der Nadel im Heuhaufen, da bei der Ausführung eines Programms in nur wenigen Sekunden Hunderte Millionen Anweisungen auszuführen sind. Daher ist es äußerst wichtig, dass die Spezifikation genügend Trigger bereitstellt, um die Erfassung auf die interessanten Bereiche einschränken zu können. Erweiterte Navigations- und Suchfunktionen sind unerlässlich. Falls die vorhandenen Compiler- beziehungsweise Debugger-Tools diese Funktion bieten, sollten die Trace-Trigger Verwendung finden, um die Trace-Daten nur auf das Notwendigste zu beschränken.

Warum Entwickler Trace einsetzen sollten

Mit der Implementierung einer Trace-IP im Prozessor lassen sich dessen Funktionen im Betrieb unverfälscht nachverfolgen, denn schon beim Hinzufügen von Low-Level-Debug-Printouts wird das Timing einer Anwendung und somit deren echtes Verhalten verfälscht.

Es gibt viele Möglichkeiten, Trace-Daten zu erfassen und aus dem Prozessor zu exportieren:

  • Serielle Schnittstellen
    • Ausreichend für PC-Trace-Sampling (und gut für statistisches Code-Profiling)
    • Einfache Anwendung, RTOS-Überwachung, Variablen-Tracing etc.
    • Eine gute Probe ermöglicht Geschwindigkeiten bis zu mehreren MBytes/s
  • Parallele High-Speed-Schnittstellen (4- bis 16-Bit-Dual-Edge)
    • Alles erfassen (mit sehr hoher Taktrate)
    • Trace per „Breadcrumbs“, selbst wenn der Control-Flow abzweigt
    • Jede einzelne ausgeführte Anweisung wird berücksichtigt (gegegenenfalls mit Verzögerung)
    • Trace-„Breadcrumbs“ werden in der Debugger-Probe gespeichert
  • RAM-Puffer
    • Entweder ein spezieller, kleiner RAM-Speicher oder als Teil des Systemspeichers
    • Schon ein Trace-RAM mit 4 KByte kann völlig ausreichend sein
  • Serielle High-Speed-Schnittstellen
    • Geschwindigkeiten von 10 Gbit/s oder höher
    • Vorwiegend für größere, komplexe Systeme
  • Trace über andere Schnittstellen (USB 3.0 verfügt über eine hohe Bandbreite)
    • Anwendungsfälle sind begrenzt; nicht geeignet für kleine IoT-Devices

Debugging von Ausnahmen

Beispiel einer Timeline, die Call-Stack, Interrupts und variable Logs kombiniert.

Beispiel einer Timeline, die Call-Stack, Interrupts und variable Logs kombiniert. IAR Systems

Mit Trace im täglichen Arbeitsablauf lässt sich gut erkennen, wie eine Anwendung läuft. Der Programmablauf ist bis zu einem bestimmten Zustand, zum Beispiel einem Anwendungsabsturz, überprüfbar und anhand der Trace-Daten die Ursache für ein Problem feststellbar. Ausnahmen (Exceptions) und unbereinigte Fehler lassen sich durch Pointer-Probleme, unzulässige Anweisungen oder Datenabbrüche verursachen.

Normalerweise wird in solchen Fällen ein Stack (und die Call-Frame-Information) unbrauchbar, doch mit Trace steht die vollständige Anwendungshistorie zur Verfügung. Trace-Daten können zudem auch nützlich sein, um Programmierfehler, die zu Unregelmäßigkeiten führen und nur sporadisch auftreten, zu lokalisieren. Auf diese Weise lassen sich Fehler aufspüren, deren Behebung sonst zu immensem Zusatzaufwand geführt hätte.

Integration von Hard- und Software-Tools

Die beste Möglichkeit, um einen möglichst hochwertigen Code zu erhalten, besteht darin, die Trace-Analyse bereits in die alltägliche Entwicklung zu integrieren. Wenn das Timing und die Richtigkeit bei jeder vorgenommenen Änderung verifiziert wird, wird die Wahrscheinlichkeit, dass komplexe Fehler bei einer späteren Systemverifikation oder sogar beim Kunden auftreten, stark verringert. Die beste Lösung sind Hardware-Tools, beispielsweise eine Trace-Debug-Probe, die in einer integrierten Entwicklungsumgebung nahtlos mit den Software-Tools zusammenarbeitet. IAR Systems bietet hierfür beispielsweise die Trace-Probe I-jet Trace an, die vollständig in der kompletten C/C++-Compiler- und Debugger-Toolchain IAR Embedded Workbench integriert ist.

I-jet Trace schaltet mehrere erweiterte Funktionen in der integrierten Entwicklungsumgebung der IAR Embedded Workbench frei. Bild 2 zeigt beispielhaft eine erweiterte Ansicht mit einer Timeline (oben) und einem Code-Stack (unten). Erkennbar ist das laufende Programm, bei dem ein Call jeweils einen anderen aufruft, wobei die Unterbrechungen (Interrupts) sichtbar sind. Der blaue Teil zeigt das Daten-Tracing sowie das RTOS-Switching etc. Das detaillierte Diagramm (unten) zeigt den Code beim Start – mit komplexen Calls, einigen längeren und kürzeren Funktionen und sogar einem Tool-Tipp zur Optimierung eines Call-Codes.

Code-Qualität

Bild 3: Mit der Trace-Funktion lässst sich auch Code-Coverage überprüfen.

Bild 3: Mit der Trace-Funktion lässst sich auch Code-Coverage überprüfen. IAR Systems

Bei Trace geht es aber nicht nur darum, Fehler zu finden. Es dient dem Entwickler auch zur Überwachung, ob eine Anwendung wie vorgesehen läuft. Die Integration einer Performance-Überwachung ist einer der Trace-Hauptvorteile und trägt zum Verständnis bei, wo und wieviel Zeit eine Anwendung zur Ausführung benötigt, ob viele Interrupts auftreten, ob sie manchmal nicht schnell genug reagiert etc.

Code-Coverage ist eine Funktion, die beim Trace ebenfalls implementierbar ist, Bild 3 zeigt, wie dies in der IAR Embedded Workbench aussieht. Sie ist zum Nachweis einsetzbar, dass ein Code mindestens einmal ausgeführt wurde, oder um toten Code zu isolieren beziehungsweise Testmängel aufzuzeigen. In den Richtlinien zur funktionalen Sicherheit wird die Code-Coverage-Analyse eindringlich als eine Möglichkeit der Qualitätssteigerung empfohlen. Ein statisches Code-Analyse-Tool ist deshalb eine gute Ergänzung zum Trace-Profiler. Es gewährleistet die Konformität des Codes mit branchenspezifischen Standards und aktuellsten Programmiertechniken. Neben der Abdeckung kann das Tool auch erfassen, wie oft jede einzelne Anweisung ausgeführt wurde. Einige Instruktionsblöcke wurden viermal und einige 12-mal ausgeführt. Offensichtlich nicht abgedeckter Code wurde 0-mal ausgeführt.

Schlussfolgerung

Bei Trace können mehrere Datenquellen Verwendung finden, um Entwickler neben dem Sammeln von Daten auch beim Filtern der Informationen zu unterstützen, damit sie ein besseres Verständnis und mehr Erkenntnisse erhalten. Die Visualisierung und Analyse mit verschiedenen Trace-Viewern liefern eine zutreffende Darstellung einer laufenden Anwendung und ihres dynamischen Verhaltens. Das hilft Entwicklern dabei, komplexe Fehler aufzuspüren, die ansonsten nur schwer oder überhaupt nicht zu finden wären. Durch die Trace-Integration in die tägliche Entwicklungsroutine – was sich durch den Einsatz integrierter Software-Tools wie der IAR Embedded Workbench und einer Debug-Probe wie I-jet Trace realisieren lässt – können Entwickler den Software-Entwicklungsprozess beschleunigen und die Software-Qualität erhöhen.