Die Fehlermeldung erlaubte es, den Quellcode des Treibers schnell zu finden (). Daraus lässt sich ablesen, dass, wenn mehr Zeichen zu senden sind, der Treiber in einer Schleife auf einen Interrupt wartet. Wenn der Interrupt nicht rechtzeitig kommt, verlässt der Treiber die Schleife und gibt eine Fehlermeldung auf der Konsole aus. Besondere Bedeutung hat das IIR (Interrupt Identification Register), welches Teil des UART-Protokolls ist. Als nächstes gilt es, mit dem Hardware-Debug-Modus von ZeBu Wellenformen aus dem UART-Controller zu extrahieren. Eine Schwierigkeit dabei ist es, einigermaßen genau abzuschätzen (Hardwarezyklen), wann das Problem auftritt. Immerhin benötigt der Linux-Bootvorgang etwa eine Milliarde Zyklen, sodass es viel zu lange dauern würde, den Prüfling während der gesamten Zeit unter die Lupe zu nehmen.
Eine Möglichkeit, um schnell die interessanten Abschnitte zu finden, sind Hardware-Trigger, wie sie die Laufzeitumgebung von ZeBu bereitstellt. Wenn der Prozessor bestimmte Werte ausgibt, kann der Emulator zyklusgenau anhalten. Anders als Software-Breakpoints (wie sie etwa gdb ermöglicht), halten Hardware-Trigger sowohl den Prozessorcore als auch den Rest des SoCs an. Dies ist vor allem dann von Bedeutung, wenn zwischen Core und dem Rest des SoCs Debug-Interrupts und asynchrone Ereignisse auftreten. Hardware-Trigger verhalten sich wie ein Verilog-Simulator: Die Zeit ist angehalten, das Warten hat keine Auswirkungen auf das Verhalten des Prüflings. Wenn die Emulation fortgesetzt wird, setzen das SoC und seine Testumgebung ihre Arbeit fort, als wäre nichts geschehen.
Software-Debugging mit System.map
Um den Ablauf zu vereinfachen, nutzte das Evaluierungsteam die Datei System. map, welche beim Kompilieren des Linux-Kernels (oder allgemein jedes C-Programms) erzeugt wird. Daraus ließ sich die Beziehung zwischen dem Funktionsbezeichner des Treibers (serial8250_interrupt) und seiner Hardware-Adresse herauslesen:
$ grep serial8250_interrupt System.map
d008769c t serial8250_interrupt
Sobald der Diamond-Prozessor diese Adresse ansprang, sollte ein Trigger ZeBu stoppen. Ein paar zusätzliche Zeilen TCL-Code (Tensilica-Befehle) in der Laufzeitumgebung »zRun« von ZeBu erlaubten es, den Funktionsnamen direkt einzugeben, sodass die GUI in der System. map nach der Hardware-Adresse suchen konnte. Nach dem Parsen der System.map in das assoziative Array »czrAddFrom-Sym« fügten die Entwickler dem TCL-Code von zRun zwei Zeilen hinzu, welche den Trigger-Input des Benutzers verarbeiten sollten (im Listing rot): Außerdem sollte ein zusätzliches Feld im GUI-Fenster dafür sorgen, dass ständig der aktuelle Funktionsaufruf des Kernels dargestellt würde (Bild 1). Allein zu sehen, wie der Funktionsname sich ständig änderte, war sehr aufschlussreich: Es wurde sofort sichtbar, wofür die CPU Rechenzeit aufwandte. Prinzipiell lassen sich auch ausgefeiltere Anwendungen implementieren, etwa störungsfreies Code-Profiling, besonders für kritische Abschnitte von Interruptvektoren, die sich mit anderen Methoden nur schwer untersuchen lassen.