Fehlerbehebung bei Software

Trace-Techniken verstehen

27. August 2023, 11:00 Uhr | Von Andre Schmitz
© Maksim Kabakou | stock.adobe.com

Systemabsturz, Rechenfehler oder verpasste Deadline? – Für die Ursachenforschung bedient man sich printf()-Ausgaben, Codeinstrumentierung bis hin zu Hardware-Tracing. Gut zu wissen, was die jeweiligen Vor- und Nachteile sind.

Diesen Artikel anhören

Große Softwareprojekte bestehen aus vielen verschiedenen Komponenten, die von vielen beteiligten Softwareingenieuren entwickelt werden, und die Interaktion und die Abhängigkeiten zwischen diesen Softwarekomponenten sind in der Regel für einen Menschen schwer zu verstehen. Wer als Softwareingenieur an einem solch komplexen Projekt arbeitet, kommt in der Regel irgendwann an einen Punkt, an dem er auf Softwarefehler, Abstürze oder sonstige Mängel stößt, oder manchmal läuft die Software einfach viel langsamer, als erwartet. In solchen Fällen möchte man das System in der Regel debuggen, um herauszufinden, was schiefläuft.

Quellcode-Debugger bieten die Möglichkeit, den aktuellen Zustand der Software zu betrachten, und dies kann bereits die Ursache eines bestimmten Problems aufzeigen, aber es ist oft nicht genug, da ein Debugger nur eine statische Ansicht zeigt, nicht aber, wie sich etwas im Laufe der Zeit verändert. Auch das Timing eines Programms kann ein Debugger nicht sichtbar machen, es sei denn, der verwendete Debugger und Compiler ermöglicht zudem die Erstellung von Leistungsprofilen.
Einige Probleme lassen sich also nur erkennen, wenn Informationen über die Ausführung des Programms im Laufe der Zeit aufgezeichnet werden, was oft als Tracing bezeichnet wird. Dieses Tracing kann durch das Schreiben von Meldungen auf die Konsole oder durch die Ablage im Speicher der Hardware erfolgen. Bei den aufgezeichneten Informationen kann es sich um Ereignisse des Betriebssystems, verarbeitete Datenpakete oder empfangene Signale handeln, die mit Zeitstempeln versehen werden können, sodass eine Zeitanalyse möglich ist.

Interessant ist ein Vergleich verschiedener typischer Ansätze zur Erstellung eines solchen Traces, mit dem Fokus auf die Ressourcennutzung.

passend zum Thema

Für Tracing verwendete Ressourcen

Benötigte Resourcen für die Aufzeichnung von »Trace«
Bild 1. Benötigte Resourcen für die Aufzeichnung von »Trace«.
© Green Hill Software

Für das Sammeln von Trace-Daten auf einem eingebetteten System und das Senden dieser Daten an den PC des Entwicklers sind in der Regel bis zu vier Arten von Ressourcen erforderlich (Bild 1):

➔ CPU-Zeit auf der Hardware, die zur Erzeugung und Übertragung der Trace-Daten benötigt wird; je mehr CPU-Zeit verbraucht wird, desto mehr Nebeneffekte hat das Tracing auf die Ausführung des Programms (siehe Heisenbug [1]). Ideal wäre ein Overhead von Null, aber wenn Null nicht erreichbar ist, sollte er insgesamt nur wenige Prozent betragen.
➔ Speicher (Daten, Code, Buffer) auf der Hardware, der zur Erzeugung oder Speicherung von Trace-Daten benötigt wird. Wenn nur begrenzter RAM-Speicher auf der Hardware zur Verfügung steht, empfiehlt es sich, eine Methode mit geringem Speicher-Overhead zu wählen oder einen Weg zu finden, Trace-Daten sofort zu senden, ohne sie zu speichern.
Bandbreite des Ports, über den die Trace-Daten vom Target zum PC übertragen werden. Verschiedene Ports haben unterschiedliche Bandbreiten, z. B. hat ein serieller Port typischerweise eine Bandbreite im Bereich von 100 kbit/s, während eine serielle Aurora-Trace-Schnittstelle bereits 5 Gbit/s pro Lane hat und mehrere Lanes pro Trace-Schnittstelle kombiniert werden können. Wenn der Anschluss, über den das Senden des Trace erfolgt, mit anderen Kommunikationskanälen geteilt wird, kannibalisiert jeder durch die Trace-Daten hinzugefügte Overhead die Bandbreite der anderen Kanäle.
➔ Speicherplatz auf dem Entwicklungs-PC (RAM und Festplattenplatz) zur Speicherung und Verarbeitung der Trace-Daten. Insbesondere wenn die Trace-Daten zur Laufzeit ausgegeben werden, erfordert das ein ausreichend großes und schnelles Speichermedium.

»printf« und ähnliche

Mit printf ist die programmatische Erzeugung von Zeichenketten auf der Hardware und das Senden dieser Zeichenketten an die serielle Schnittstelle oder einen ähnlichen Kommunikationskanal wie einen TCP/IP-Socket oder eine CAN-Schnittstelle gemeint. Dies kann nicht nur mit der Funktion printf() geschehen, sondern auch mit Funktionen wie puts(), sprintf(), send(), write() oder ähnlichen. Diese Methode verursacht einen Overhead in Form von CPU-Zeit und Port-Bandbreite. Der benötigte On-Target-Speicher ist durchschnittlich und wird für den zusätzlichen Code und die Strings benötigt, die gespeichert und verarbeitet werden müssen

Optimiertes/gepuffertes »printf«

Sollen trotz Bedenken wegen des CPU-Ressourcen-Overheads von printf keine professionellen Tools verwendet werden, besteht die Möglichkeit, ein ressourcenoptimiertes printf zu verwenden, indem die Daten nicht tatsächlich über die (serielle) Schnittstelle gesendet, sondern in lokalen Puffern auf der Hardware gespeichert werden. Und anstatt die Zeichenkette auf der Hardware zu generieren, sollte man vielleicht nur Nachrichten-IDs speichern, die sich auf dem PC in die eigentlichen Nachrichten decodieren lassen. Dieser Ansatz kann helfen, die CPU-Zeit und den Port-Bandbreiten-Overhead zu reduzieren, erfordert aber zusätzlich Speicher auf der Hardware. Wie groß der Unterschied in der Ressourcennutzung sein kann, wird weiter unten ein Beispiel zeigen.

Zeitstempel abrufen

Wenn es darum geht, Informationen über den Programmablauf zu verfolgen, insbesondere zeitliche Informationen, benötigt man in der Regel eine Art Zeitstempel, der jeder Trace-Nachricht hinzugefügt wird, und der es ermöglicht, zu verstehen, wie lange eine Operation gedauert hat oder wie sich verschiedene Ereignisse zueinander verhalten. Es gibt verschiedene Möglichkeiten, einen solchen Zeitstempel zu erhalten. Der offensichtliche Weg ist die Verwendung einer Funktion des Betriebssystems (OS) oder einer Laufzeitbibliothek, wie gettimeofday() oder time() oder ähnliches. Je nach- dem, wie diese implementiert sind, kann der Aufruf dieser Funktionen auch einen erheblichen Overhead verursachen. So kann es z. B. erforderlich sein, in den Kernel zu wechseln oder zumindest einen Kontextwechsel vorzunehmen, dann einen Treiber zu verwenden, um bestimmte HW-Zähler zu lesen, die HW-Ticks in einen für den Menschen lesbaren Zeitwert umzuwandeln und diesen an den Aufrufer zurückzugeben.

Um dies effizienter zu gestalten, wäre es vielleicht besser, einfach die HW-Ticks auszulesen und zu protokollieren. Auf ARM64-Targets gibt es zum Beispiel zwei Register, auf die vom User-Space aus zugegriffen werden kann und die einen System Counter und dessen Frequenz bereitstellen [2], sodass man im Grunde nur Folgendes tun muss:

MRS X1, CNTPCT_EL0, um den Tick-Wert der System Counters zu erhalten und
MRS X2, CNTFRQ_EL0, um die Frequenz zu ermitteln.

Man kann den Frequenzwert einmal lesen und speichern und danach nur noch den Tick-Wert. Die Umwandlung des Ticks in eine Uhrzeit kann offline erfolgen, was viel CPU-Zeit spart.

Instrumentierung und SW-Tools

Anstatt programmatische Mechanismen zu verwenden, wie oben beschrieben, ist es effizienter, werkzeugbasierte Protokollierung und Trace zu verwenden. In diesem Fall läuft ein Tool über den Code oder das Binärprogramm und erzeugt zusätzliche Instruktionen, die Zeitstempel und Protokollinformationen erzeugen. Diese Informationen werden dann entweder in einem Puffer gespeichert oder über einen Port an den PC des Entwicklers gesendet. Bei den zu protokollierenden Daten kann es sich beispielsweise um benutzerdefinierte Zeichenketten, Variablenwerte, ausgeführte Funktionen oder Task-Ausführungszustände mit Kontextwechseln handeln.

Manchmal wird die Instrumentierung bereits vom Compiler selbst vorgenommen, d. h. während der regulären Übersetzung des Quellcodes fügt der Compiler eine minimale Anzahl zusätzlicher Instruktionen hinzu, die Zeitstempel und Protokolldaten in einen lokalen Puffer übertragen. Der dadurch entstehende Overhead ist sehr gering, sowohl in Bezug auf die CPU-Zeit als auch auf die Codegröße, und wenn die Daten auf der Hardware gespeichert sind, wird auch keine Port-Bandbreite benötigt. Aber natürlich erfordert dies einen Puffer auf der Hardware, sodass diese Option nicht auf Systemen mit begrenztem Speicherplatz funktioniert. In diesen Fällen empfiehlt sich eine Lösung, die die Daten über einen Port ausgibt, und auch hier müssen die Kommunikationsmechanismen hochgradig optimiert sein.

Hardware-Trace

Die effizienteste Art, die Programmausführung zu verfolgen, ist der Hardware-Trace. Dieser erlaubt es, die komplette Ausführung der CPU über ein bestimmtes Zeitfenster zu verfolgen, also grundsätzlich alle Programm-Counter(PC)-Werte und manchmal auch alle Datenbuszugriffe, d. h. die Adresse und die jeweils geschriebenen oder gelesenen Datenwerte. Diese Daten werden direkt in der CPU erzeugt, z. B. durch den Einsatz von Hardwaremodulen wie ARM ETM, PTM oder ähnlichem. Die erzeugten Trace-Daten können dann entweder in einem kleinen dedizierten Puffer auf dem Chip (z. B. ARM ETB oder ETF) oder im dafür reservierten RAM (z. B. ARM ETR) gespeichert werden, oder sie werden über eine Trace-Schnittstelle (z. B. ARM TPIU) [3] nach außen gesendet. Wenn Sie also den Embedded-Prozessor für Ihr Projekt auswählen und Hardware-Trace verwenden wollen, stellen Sie sicher, dass die Hardware dies unterstützt und Sie die entsprechenden Trace-Schnittstellen auch auf Ihrer Leiterplatte vorgesehen haben.

Im Allgemeinen gibt es zwei Arten von Trace-Schnittstellen, die parallele Trace-Schnittstelle und die serielle Hochgeschwindigkeits-Trace-Schnittstelle. Beide Arten haben unterschiedliche Eigenschaften in Bezug auf die Bandbreite, und es ist zu prüfen, ob die angebotene Bandbreite der erwarteten Trace-Datenlast entspricht. Dies ist wichtig, da auch der Hardware-Trace einige Einschränkungen aufweist. Angenommen, es liegt eine serielle Trace-Schnittstelle mit einer Aurora-Lane mit einer Nettobandbreite von ~4 Gbit/s vor, aber die Hardware hat zwei Cortex-A57-Kerne, von denen jeder mit 2 GHz läuft. Selbst bei minimalen Trace-Daten (nur PC), die von der Hardware erzeugt werden, kann es passieren, dass Trace-Daten aufgrund von internem FiFo-Überlauf verloren gehen.


  1. Trace-Techniken verstehen
  2. Experiment zum Vergleich

Lesen Sie mehr zum Thema


Jetzt kostenfreie Newsletter bestellen!

Weitere Artikel zu INFINEON Technologies AG Neubiberg

Weitere Artikel zu Green Hills Software GmbH

Weitere Artikel zu Entwicklungswerkzeuge

Weitere Artikel zu Softwareentwicklung