Das Titelbild dieses Beitrags ist von WikiImages auf Pixabay

Man will nur einen einzelnen Schritt in einem Programm ausführen, aber der Debugger springt zu einem völlig unbekannten Bereich des Programms. Dies war in der Tat meine erste Erfahrung, als ich den MPLAB-X-Debugger von Microchip auf dem Blink-Programm ausprobierte habe. Fehler oder Feature?

Problem

Wenn man anfängt, im Web nach „Single-Stepping und Interrupts“ zu suchen, wird schnell klar, dass andere Leute beim Debuggen eingebetteter Systeme oft über das gleiche Phänomen stolpern. Was ist also das Problem?

Man hat ein Programm, das man in Einzelschritten durchlaufen möchte (in GDB mithilfe der Kommandos next und step), aber es funktioniert nicht. Anstatt eine Zeile auszuführen und dann am Anfang der nächsten Zeile anzuhalten, stoppt der Debugger in der Interruptvektortabelle. Der Grund dafür ist, dass ein Interrupt ausgelöst und die Interrupt-Behandlungssequenz gestartet wurde. Tatsächlich wurde also nur eine einzige Zeile (oder sogar nur ein Teil davon) ausgeführt, aber weil der Interrupt ausgelöst wurde, landet man in der Interrupt-Dispatch-Tabelle.

So, jetzt wissen wir, warum wir an dem seltsamen Ort gelandet sind. Es ist jedoch eine sehr unbefriedigende Erklärung. Wenn ein Timer-Interrupt aktiv ist, der jede Millisekunde ausgelöst wird, wird man wahrscheinlich nie das Vergnügen haben, den Code in Einzelschritten durchlaufen zu können.

Man findet zwei verschiedene Ratschläge, wie man mit dem Problem umgehen sollte:

  1. Anstatt Single-Stepping einzusetzen, sollten Breakpoints iterativ gesetzt werden, um damit den Code schrittweise zu durchlaufen; dabei werden Interrupt-Routinen dann „im Hintergrund“ ausgeführt.
  2. Man definiert sich auf Benutzerebene einen Debugger-Befehl, der Interrupts vor einem Einzelschritt deaktiviert und danach wieder aktiviert.

Man kann diese Methoden einsetzen. Aber es stellt sich natürlich die Frage, warum der Debugger nicht eine davon implementiert. Werfen wir einen Blick darauf, was der GDB-Debugger für AVR-MCUs macht und wie ein Hardware-Debugger wie dw-link GDB unterstützen könnte.

Wie avr-gdb mit dem Problem umgeht

avr-gdb bewältigt dieses Problem meist reibungslos. GDB übersetzt ein next– oder step-Kommando in eine Sequenz von Single-Step-Befehlen auf der Hardware-Debugger-Ebene. Ggf. wird ein temporärer Breakpoint gesetzt, um nach einem Funktionsaufruf wieder zu stoppen. Führt ein Single-Step-Befehl nicht zur erwarteten Adresse im Programm, setzt der Debugger einen temporären Breakpoint an der erwarteten Adresse und führt einen Befehl zum Fortsetzen der Ausführung aus. Mit dieser Strategie vermeidet es das oben beschriebene Problem — meistens.

Leider verwendet avr-gdb die obige Strategie nicht durchgängig. Wenn der letzte Single-Step-Befehl nicht zur erwarteten Adresse führt, dann wird kein temporärer Breakpoint gesetzt. Insbesondere wenn viele Interrupts bedient werden müssen, z.B. bei der Ausgabe eines Strings über die serielle Schnittstelle, kann es vorkommen, dass man beim Single-Stepping immer noch in der Interrupt-Vektortabelle landet. Ich glaube, das ist ein Fehler, aber es könnte natürlich ein Feature sein.

Ich bin der Meinung, dass Interrupt-Routinen für den Benutzer weitgehend transparent sein sollten. Nur wenn man in einer Interrupt-Routine ein Breakpoint gesetzt hat, dann sollte der Debugger dort stoppen. Die Frage ist, ob ein solches Verhalten im neuen dw-link-Hardware-Debugger implementiert werden könnte.

Man kann natürlich eine der beiden oben beschriebenen Methoden verwenden, d.h. einen temporären Breakpoint und einen Continue-Befehl verwenden oder einen Single-Step-Befehl durch ein Interrupt-Disable/Enable-Paar einfassen. Beide Methoden sind jedoch schwierig zu implementieren.

Die Einführung eines temporären Breakpoints auf der Ebene des Hardware-Debuggers kollidiert mit dem Breakpointmanagement, da man diesen temporären Breakpoint auch löschen muss, wenn man z.B. an einem Breakpoint in einer Interrupt-Routine landet, d.h. die Ausführung eines einzelnen Schritts wäre nicht mehr lokal. Darüber hinaus müsste man für Verzweigungsanweisungen entweder zwei temporäre Breakpoints verwenden oder man benötigt eine spezielle Lösung für Verzweigungsanweisungen, z. B. indem man sie simuliert, anstatt sie auszuführen. Mit anderen Worten, man könnte dies implementieren, aber es wäre vermutlich ziemlich umständlich.

Die zweite mögliche Lösung, ein Single-Step-Kommando durch ein Interrupt-Disable/Enable-Paar einzufassen, ist womöglich noch schwieriger zu realisieren. Da es eine große Anzahl von Maschinenbefehlen gibt, die das I-Bit möglicherweise manipulieren oder davon beeinflusst werden könnten (man denke nur an push oder ldd), müssten all diese Anweisungen simuliert statt ausgeführt werden.

Glücklicherweise gibt es eine dritte Lösung. In debugWIRE gibt es einen Befehl, um eine Anweisung in das Anweisungsregister zu laden und offline auszuführen. Dieser Befehl wird für viele verschiedene Zwecke verwendet, z.B. zum Lesen und Schreiben der allgemeinen und der E/A-Register. Ein weiterer Zweck ist die Ausführung einer Anweisung, die durch eine Break-Anweisung im Programmspeicher ersetzt wurde. Anstatt die Anweisung zurück in den Speicher zu schreiben und dann auszuführen, führt man diese Anweisung offline aus. Alle Maschinenregister werden auf die richtige Weise aktualisiert. Anscheinend sind dabei jedoch alle MCU-Timer eingefroren. Und es werden keine Interrupts ausgelöst, wenn eine solche Offline-Ausführung stattfindet (was genau das ist, was wir wollen). Der Zwei-Wort-Instruktionen müssen dabei auf besondere Weise gehandhabt werden, z.B. durch Simulation.

Die dritte Methode, die in dw-link implementiert wurde, vermeidet das Problem, in der Interrupt-Vektortabelle zu landen. Es gibt jedoch einen Nachteil. Alle Timer sind eingefroren und ausstehende Interrupts werden nicht ausgelöst, solange man nur im Einzelschritt durch den Code geht und man nicht irgendwo einen Funktionsaufruf überspringt. Ist man sich dieser Einschränkungen bewusst, ist die Situation viel besser ist als zuvor. Und man kann immer noch die ursprüngliche Form des Single-Stepping erlauben, indem man den Debugger-Befehl monitor unsafestep benutzt.

Views: 19