Aus Linux-Magazin 09/2013

Fehlersuche auf Embedded-Systemen

© Barbara Reddoch, 123RF.com

Auf der Pirsch nach Fehlern verlangen eingebettete Systeme dem Entwickler etwas mehr Einsatz ab als gewöhnliche PCs. Gut also, wenn der Bug-Jäger die verschiedenen Techniken kennt.

Smartphones, Waschmaschinen, LCD-TVs, Router, Industriesteuerungen, Digitaluhren und Raspberry Pi haben gemeinsam, dass sie statt eines x86-Prozessors ein System-on-Chip mit einem oder mehreren ARM-Kernen in sich tragen. Ihre Bedien- und Kommunikations-Schnittstellen sind deutlich eingeschränkt, was sich nicht nur auf die Handhabung auswirkt, sondern auch auf die Software-Entwicklung und die damit einhergehende Fehlersuche, die dieser Artikel besonders beleuchtet. Zwar stehen ARM-Systeme im Fokus, doch ist das Vorgehen prozessorunabhängig und gilt für die meisten eingebetteten Geräte.

Generell fallen Software und Hardware, die der Embedded-Entwickler üblicherweise ins Visier nimmt, höchst unterschiedlich aus: Während Tablets in ihrer Ausstattung herkömmlichen PCs ähneln, besitzt die Steuerungshardware einer Waschmaschine weder die für das gewohnte Debugging notwendigen Schnittstellen noch eine ausreichende Rechenleistung oder Software-Unterstützung. Kein Wunder also, dass sich die Ansätze beim Debuggen unterscheiden und von der Beschaffenheit der eingebetteten Geräte abhängen.

Debug im Dreierpack

Grundsätzlich fallen die Debugging-Ansätze im eingebetteten Bereich unter drei Kategorien, die alle ihre Vor- und Nachteile besitzen. In die erste Kategorie gehören jene Methoden, die bestehende Debugging-Techniken einfach auf die eingebetteten Systeme ausdehnen. Diese können auch Entwickler mit wenig Erfahrung im eingebetteten Bereich nutzen. Der Nachteil: Sie sind sehr restriktiv hinsichtlich der Software auf dem Zielsystem.

Die zweite Kategorie von Techniken setzt zusätzliche Hardwareschnittstellen voraus, die vornehmlich dem Debugging dienen. Sie besitzen den Vorteil, die größtmögliche Kontrolle bei der Programmausführung zu gewährleisten, erhöhen allerdings aufgrund eines erweiterten Hardware-Aufbaus den Aufwand.

Eine dritte Variante von Debugging-Ansätzen basiert auf Emulation. Diese bietet einem Entwickler zwar große Flexibilität bei der Arbeit, jedoch zu dem Preis, dass die Emulation häufig nicht zu 100 Prozent mit der real existierenden Hardware übereinstimmt. Potenziell kann es daher zu nicht vorhersehbaren Fehlern kommen.

Native Entwicklung

Die Fehlersuche lehnt sich in der Regel eng an die eigentliche Entwicklung von Software an. Diese unterscheidet sich bei eingebetteten Systemen erheblich von dem, was bei Desktop- oder Serversystemen sonst üblich ist. Wer Software für solche klassischen Systeme entwickelt, greift heute in den meisten Fällen auf einen Entwicklungsrechner zurück, der häufig besser ausgestattet ist als die Systeme, auf denen die Software später laufen soll.

Theoretisch ließe sich die Entwicklung auch auf dem Produktionssystem selbst vornehmen, weil die Hardware in der Lage ist, die Entwicklungsumgebung mitsamt Benutzeroberfläche auszuführen. In beiden Fällen ist das Testen der Software sehr einfach, weil der Entwickler die Fehlersuche auf demselben System erledigt.

Cross-Entwicklung

Bei eingebetteten Systemen funktioniert dieser Ansatz nur selten. Zum einen verfügen die Zielsysteme oft über andere Prozessoren, zum Beispiel ARM, während als Entwicklungsmaschinen üblicherweise PCs mit x86-Prozessor dienen, die den Zielcode nativ nicht ausführen können. Zum anderen sind die zur Ausführung der Entwicklungsumgebung notwendigen Ressourcen nur unzureichend vorhanden oder fehlen ganz.

Ein anderes Hemmnis: Ein modernes Tablet könnte eine komplexe Entwicklungsumgebung zwar technisch problemlos ausführen, doch die Bedienoberfläche würde das Entwickeln mit den üblichen Werkzeugen sehr erschweren. Bei tiefer eingebetteten Systemen wie der Steuerung einer Waschmaschine oder eines Fahrzeugsystems ist das Problem noch deutlicher ausgeprägt: Hier halten Speicher und CPU nicht mit, ganz zu schweigen von der unzureichenden Bedienschnittstelle.

Diese Faktoren machen eine sehr deutliche Trennung von Entwicklungs- und Zielsystem notwendig. Die Programmierung erfolgt auf einem leistungsfähigen Entwicklungssystem (meist einem PC), die dabei erzeugte Software wird zur Ausführung auf das Zielsystem oder ein dazu kompatibles Entwicklungsboard übertragen. Letzteres ist in der Regel etwas leistungsfähiger als das offizielle Zielsystem und verfügt über zusätzliche Schnittstellen.

Integrationsfragen

Unterschiedliche Prozessorarchitekturen (etwa x86 und ARM) erfordern es, statt des systemeigenen Compilers und Linkers eine Cross-Toolchain (siehe Kasten “Cross-Toolchain”) zu verwenden. Sie erzeugt ausführbare Programme für die Zielplattform, die der Entwickler auf das Embedded-Gerät kopiert. Komplexere Mobilsysteme wie Tablets oder Smartphones bringen zudem Mechanismen mit, um Software in das System zu integrieren, etwa in Form von Apps.

Cross-Toolchain

Bei der Entwicklung für eingebettete Systeme, die zum Beispiel ARM-Prozessoren verwenden, kann der Entwickler die systemeigene Compiler-Toolchain (Assembler, Compiler, Linker und Debugger) des Entwicklungssystems nicht verwenden, um Quelltext in Binärcode für das Zielsystem zu übersetzen.

Hier kommt eine Cross-Toolchain zum Einsatz, die auf dem Entwicklungssystem läuft, während der von ihr erzeugte Binärcode nur auf dem Zielsystem funktioniert. Gleiches gilt für den Debugger in der Cross-Toolchain, der nur den Binärcode des Zielsystems versteht.

Cross-Toolchains auf GCC-Basis lassen sich bei den meisten Linux-Distributionen für verschiedene Zielarchitekturen aus der Paketverwaltung heraus installieren oder durch entsprechende Skripte einfach selbst übersetzen.

Schlichtere Systeme, zum Beispiel die Steuergeräte eines Fahrstuhls, laden keine Software nach. Hier muss der Entwickler ein wenig mehr Aufwand betreiben, indem er ein kompiliertes Abbild des Betriebssystems, das auf dem eingebetteten System läuft, mit der neu entwickelten Software zu einem bootfähigen Image kombiniert. Dieses Image wird dann wahlweise über ein bootfähiges Medium, über das Schreiben in den Flashspeicher oder über das Netzwerk auf dem Zielsystem ausgeführt.

Im Vergleich zu herkömmlichen Systemen muss der Debugger hier nicht nur aufwändigere Entwicklungsmethoden anwenden, sondern auch mit der Trennung von Entwicklungs- und Zielsystem sowie der eingeschränkten Leistungsfähigkeit des Geräts leben.

Debugging via Printf

Bei der einfachsten Form der Fehlersuche baut der Programmierer Textausgaben in das Programm ein, die ihn über den aktuellen Zustand während der Ausführung informieren. Die Technik heißt, in Anlehnung an den Funktionsaufruf in C, meist einfach nur Printf. Sie hat den Nachteil, dass der Entwickler das Programm anpassen und die zu beobachtenden Teile vor dem Übersetzen kennen muss. Dafür steht der schlichte Ansatz fast immer zur Verfügung.

Die Ausgabe muss nicht über einen Bildschirm laufen, für den die Zielsysteme oft keine Schnittstelle mitbringen, sondern kann über einen Ethernet-Port oder eine serielle RS232-Verbindung erfolgen. Fehlt auch diese, etwa bei sehr einfachen Systemen, lässt sich eine LED einsetzen, die ein Muster blinkt, das den Programmfortschritt anzeigt.

Gemeinsam ist diesen Ansätzen, dass sie keine explizite Kopplung des Zielsystems mit dem Entwicklungssystem voraussetzen, sondern dass der Entwickler seine Schlüsse aus den Ausgaben des Zielsystems zieht, die er dann auf dem Entwicklungssystem umsetzt.

Remote Debugging

Eine komfortablere Fehlersuche schließt einen Debugger wie GDB [1] ein. Mit dessen Hilfe unterbricht der Entwickler die Ausführung seines Programms an bestimmten Stellen, verfolgt sie Quelltextzeile für Quelltextzeile weiter, kontrolliert und manipuliert den Inhalt von Registern und Variablen.

In der klassischen Software-Entwicklung finden sowohl die Analyse als auch die Ausführung des Programms auf dem Entwicklungssystem statt, das dem Zielsystem sehr stark ähnelt. Eine Entwicklungsumgebung (Abbildung 1) unterstützt den Programmierer und erleichtert die Analyse. Dies setzt voraus, dass die untersuchte Software auf demselben Rechner läuft wie der Debugger beziehungsweise die Entwicklungsumgebung.

Abbildung 1: Remote Debugging lässt sich über die Kommandozeile oder ein IDE wie Eclipse bewerkstelligen.

Abbildung 1: Remote Debugging lässt sich über die Kommandozeile oder ein IDE wie Eclipse bewerkstelligen.

Weil dies bei eingebetteten Systemen oft nicht zutrifft, kommt Remote Debugging zum Einsatz. Dabei startet der Entwickler auf dem Zielsystem eine Helfer-Applikation, mit der sich der Debugger des Entwicklungssystems verbindet, zum Beispiel über Ethernet oder die serielle RS232-Schnittstelle.

Im Falle von GDB führt der Zielrechner einen GDB-Server aus, bei dem sich der GDB-Client auf dem Entwicklungssystem anmeldet. Der Client leitet nun Anweisungen an die Serverinstanz auf dem Zielsystem weiter, um zum Beispiel das untersuchte Programm anzuhalten oder die Registerinhalte auszulesen. Zugleich steuert der Programmierer die Anwendung auf diesem Weg.

Der Server setzt die Anweisungen um und schickt die Resultate an das Entwicklungssystem zurück. Dessen Debugger übernimmt dann die rechenintensiven Aufgaben. Er gleicht Assembler-Instruktionen mit bestimmten Quelltextzeilen ab und ordnet Variablen richtig zu.

Debugging über Kreuz

Unterscheiden sich die Architekturen von Entwicklungs- und Zielsystem, versteht der Debugger des Entwicklungssystems weder die Assembler-Instruktionen des Gegenübers, noch kennt er dessen Register. Das verhindert ein direktes Auswerten von Variablen und Zuordnen von Instruktionen zum Quelltext.

Dieses Problem löst ein so genannter Cross-Debugger, der Bestandteil der Cross-Toolchain ist. Er lässt sich ebenfalls auf dem Entwicklungssystem ausführen, versteht jedoch die Assembler-Instruktionen des jeweiligen Zielsystems. Das macht es unter anderem möglich, mit Hilfe von Remote Debugging ein ARM-Programm auf einem x86-System zu untersuchen (Abbildung 2).

Abbildung 2: Um ARM-Programme auf einem herkömmlichen x86-System zu debuggen, hilft der Einsatz eines Cross-Debuggers.

Abbildung 2: Um ARM-Programme auf einem herkömmlichen x86-System zu debuggen, hilft der Einsatz eines Cross-Debuggers.

Im Falle von Android stellt Google beispielsweise Entwicklungsumgebungen für Smartphone- und Tablet-Entwickler bereit. Letztere können beim Remote Debugging kaum noch unterscheiden, ob die Anwendung auf dem Endgerät oder dem Rechner des Entwicklers läuft. Auf dem Entwicklungssystem arbeitet dabei ein Emulator.

DIY: Remote Debugging

Die eben vorgestellte Form des Remote Debugging setzt voraus, dass das Zielsystem mehrere Programme zugleich ausführen kann, also mindestens das zu untersuchende Programm und ein Hilfsprogramm wie den GDB-Server. Das funktioniert zum Beispiel problemlos, wenn auf dem Zielsystem ein Betriebssystem wie etwa Linux läuft.

Manchmal untersucht der Entwickler jedoch Programme, die ohne Betriebssystem funktionieren, oder er will Fehler im Betriebssystem selbst aufspüren. Die Programme im ersten Fall heißen Bare-Metal-Applikationen, da sie ohne Zwischenschicht direkt auf der Hardware laufen. Auch sie lassen sich mit einem gewissen Mehraufwand über Remote-Debugging-Techniken erforschen.

Eine Variante besteht darin, spezielle Debug-Routinen in das zu untersuchende Programm einzuschleusen. So kann dieses selbst als Debug-Server arbeiten, mit dem sich der Debugger des Entwicklungssystems dann verbindet. Was GDB betrifft, stehen für einige Architekturen (mit Ausnahme von beispielsweise ARM) fertige Bibliotheken bereit, die nur ein geringfügiges Eingreifen des Programmierers erfordern. Sobald die Routinen in dem zu untersuchenden Programm stecken, kann sich der Debugger auf der Entwicklungsmaschine über eine serielle RS232-Verbindung mit dem Zielsystem verbinden.

Diese Variante des Remote Debugging lässt sich auch für den Linux-Kernel einsetzen, der unter der Bezeichnung KGDB bereits Routinen mitbringt, die ihn in einen GDB-Server verwandeln. Aktiviert man die passenden KGDB-Optionen (zum Beispiel »CONFIG_KGDB=y« und »CONFIG_KGDB_SERIAL_CONSOLE=y« ), lässt er sich über eine serielle Verbindung wie jedes andere Bare-Metal-Programm untersuchen.

Debugging via JTAG

JTAG

JTAG steht als Kürzel für Joint Test Action Group und ist die geläufige Bezeichnung für den IEEE-Standard 1149.1. Dieser implementierte ursprünglich lediglich einen Boundary-Scan-Test, mit dem es möglich ist, Zustände von Flipflops in einer kompletten Schaltung auszulesen. Obwohl der Standard in der Zwischenzeit wesentlich erweitert wurde, bleibt der Boundary-Scan-Test der wichtigste Bestandteil und der Begriff wird von manchen Entwicklern als Synonym für JTAG verwendet.

Neben dem Auslesen von Flipflops lassen sich mit JTAG inzwischen auch eingebettete Schaltungen wie Microcontroller, System-on-Chips und Field Programmable Gate Arrays (FPGA) steuern und überwachen. Er basiert auf einem seriellen Protokoll, welches das entsprechende Gerät über einen Zustandsautomaten steuert.

Sollte auch die Erweiterung des zu untersuchenden Programms nicht möglich oder zu aufwändig sein, lässt sich die Software im Embedded-Bereich auch mit Hilfe spezieller Debugging-Schnittstellen der Hardware genauer untersuchen. Als Standard hierfür hat sich inzwischen die JTAG-Schnittstelle etabliert (siehe Kasten “JTAG”).

Diese ermöglicht es, den Prozessor des Zielsystems an den zur Laufzeit genutzten Steuerungsmöglichkeiten vorbei zu dirigieren. Der Entwickler kann ihn anhalten, die instruktionsweise Ausführung eines Programms erzwingen, aber auch die Register- und Speicherinhalte auslesen und manipulieren. Die JTAG-Kette akzeptiert sogar mehrere Geräte, wodurch sich auch Koprozessoren, zum Beispiel digitale Signalprozessoren, ansteuern lassen.

Wer auf die JTAG-Schnittstelle zugreifen möchte, benötigt einen Adapter, den es für die ARM-Plattformen bereits ab zirka 25 Euro gibt und den man per USB- oder RS232-Schnittstelle mit dem Entwicklungsrechner verbindet. Die Nutzer der Schnittstelle gehen dann nach dem Schema des Remote-Debugging (Abbildung 3) vor. Dabei stellt der JTAG-Debug-Server eine Brücke zwischen JTAG-Schnittstelle und Debugger auf dem Entwicklungssystem her.

Abbildung 3: Der JTAG-Debug-Server baut eine Brücke zwischen der JTAG-Schnittstelle und dem Debugger auf dem Entwicklungssystem. Anschließend folgt der Entwickler dem bekannten Remote-Debugging-Schema.

Abbildung 3: Der JTAG-Debug-Server baut eine Brücke zwischen der JTAG-Schnittstelle und dem Debugger auf dem Entwicklungssystem. Anschließend folgt der Entwickler dem bekannten Remote-Debugging-Schema.

Für den Debugger erscheint die JTAG-Schnittstelle nun wie ein Remote-Server. Letzterer übersetzt die Kommandos des Debuggers (beispielsweise zum Anhalten der Software oder zum Auslesen von Registern) in den entsprechenden JTAG-Befehlsstrom und wandelt die über JTAG ausgelesenen Werte in ein für den Debugger verständliches Format. Das erfordert keine Anpassungen an dem zu untersuchenden Programm, da die Eingriffe nur Hardware-seitig stattfinden.

JTAG erweist sich also als sehr bequeme Möglichkeit zum nicht-invasiven Debugging, wenn das Zielsystem die entsprechende Schnittstelle mitbringt. Das ist jedoch nur bei speziellen Entwicklungsboards der Fall, während den davon abgeleiteten Endprodukte diese Schnittstelle in der Regel aus Kostengründen fehlt. Bei anderen Geräten sind sie auf der Leiterplatte vorgesehen, aber nicht bestückt, sodass sich für Bastler und Hacker interessante Möglichkeiten [2] ergeben.

Fehlersuche per Emulation

Eine weitere einfache Möglichkeit, um Fehler in Programmen von eingebetteten Systemen aufzuspüren, besteht im Emulieren der Zielplattform. Die Granularität kann dabei höchst unterschiedlich sein und reicht von der Emulation eines API über die Systememulation bis hin zur vollständigen Simulation. Ein Anwendungsbeispiel wäre die erwähnte Entwicklungsumgebung von Google für Android.

Eine universellere Form der Fehlersuche per Remote Debugging erlaubt die freie virtuelle Maschine Qemu. Sie emuliert komplette Systeme, führt also auch Bare-Metal-Programme ohne spezielle Anpassung aus. Sie stellt jedoch zusätzlich eine GDB-Server-Schnittstelle bereit, die eine ähnlich umfangreiche Steuerung erlaubt wie eine JTAG-Schnittstelle und dabei hilft, die Programmausführung zu manipulieren und zu untersuchen. Analog zu JTAG kann der Entwickler die Software ab der ersten ausgeführten Instruktion beobachten, benötigt aber keine zusätzliche Hardware (Abbildung 4).

Abbildung 4: Mit Qemu, das komplette Systeme emuliert, lassen sich auch ARM-Systeme ähnlich wie mit JTAG debuggen.

Abbildung 4: Mit Qemu, das komplette Systeme emuliert, lassen sich auch ARM-Systeme ähnlich wie mit JTAG debuggen.

Genauer gesagt benötigt er theoretisch nicht einmal Zugriff auf ein physisches Exemplar des Zielsystems. Da die Emulation jedoch nicht hundertprozentig so funktioniert wie die echte Hardware, lässt sich eine spätere Ausführung auf einem realen Gerät meist nicht umgehen. Dies ist insbesondere dann der Fall, wenn man das Zeitverhalten des Zielsystems untersuchen möchte.

Eines für alle

Die meisten ARM-basierten SoC-Systeme unterscheiden sich signifikant im Aufbau, da ARM keine festen Vorgaben in Sachen I/O-Geräte oder Speicherlayout macht. Die Geräte des System-on-Chip lassen sich etwa über verschiedene Speicheradressen ansprechen, RAM, ROM und Flashspeicher befinden sich an unterschiedlichen Adressen. Bei der Portierung liegen zwar meist Treiber für die Geräte vor, da sie häufig bereits von anderen SoCs bekannt sind, dennoch muss der Entwickler die Systemsoftware an die andere Speicherstruktur anpassen.

Für ARM-Systeme weist Qemu beim Entwickeln von Systemsoftware noch weitere Vorteile auf, die das Portieren von Linux auf neue ARM-Plattformen erleichtern. So lassen sich in Qemu mit geringem Programmieraufwand beliebige Speicherlayouts emulieren, sodass sich der Programmierer von einer bestehenden lauffähigen Plattform Schritt für Schritt zur neuen Plattform bewegen kann. Die Remote-Debugging-Technik hilft ihm dabei, die Veränderungen komfortabel zu verfolgen, was Portierungen massiv beschleunigt.

Die Außenwelt

Abseits der Suche nach logischen Fehlern im Programmablauf verstecken sich im Embedded-Bereich auch Fehler in der Ansteuerung externer Geräte, die häufig an Standardschnittstellen wie RS232, I2C oder SPI hängen. Um diese Signale zu untersuchen, lassen sich so genannte Logic Analyzer nutzen, welche die Signale aufzeichnen und die übertragenen Daten am Entwicklungsrechner anzeigen. Der Funktionsumfang und die Genauigkeit der Analyzer ist sehr unterschiedlich und hängt vom Preis ab. Günstige Geräte wie etwa den Bus Pirate [3] gibt es bereits für rund 25 Euro.

Debuggen wie die Profis

Im Profibereich, besonders beim System-on-Chip-Design, stehen dem Entwickler noch weitere Werkzeuge zur Seite. So zeichnen digitale Speicher-Oszilloskope den Zugriff auf externen Speicher sowie andere Signale mit einer sehr hohen Genauigkeit auf. Da die Entwickler zudem Zugriff auf exakte Beschreibungen der Chips haben, lässt sich das Ausführen von Programmen vollständig im Rechner simulieren. Daneben kann ein Entwickler den Chip im Prototyp-Stadium um Steuerleitungen erweitern, die das Überwachen und Beobachten vereinfachen.

Fazit

Obwohl es also auf den ersten Blick komplex erscheint, bietet das Debugging von eingebetteten Systemen insgesamt eine Vielzahl von Lösungsansätzen, zwischen denen der Entwickler, abhängig von Bedarf und Zielplattform, auswählt.

Der im Gegensatz zu Standardsystemen erhöhte Aufwand, den die Trennung von Entwicklungs- und Zielsystem sowie die stark eingeschränkten Ressourcen eingebetteter Systeme verursachen, verbergen die hier vorgestellten Ansätze meist vor dem Entwickler. Der Arbeitsablauf unterscheidet sich bei einer entsprechend eingerichteten Entwicklungsumgebung kaum vom Üblichen.

Der Autor

Anselm Busse ist wissenschaftlicher Mitarbeiter und Doktorand am Fachgebiet Kommunikations- und Betriebssysteme an der Technischen Universität Berlin. Jan Richling ist dort Gastprofessor für Betriebssysteme und eingebettete Systeme. Ihr Forschungsschwerpunkt liegt unter anderem in der Steigerung der Energieeffizienz von Many-Core-Systemen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben