Intel Prozessoren ab dem 80386 verfügen über 8 Spezialregister, die es ermöglichen, bis zu vier unabhängige, lineare Adressen festzulegen. Die Werte in diesen Registern vergleicht der Prozessor automatisch mit angesprochenen Adressen und generiert bei Übereinstimmung einen Interrupt 1. Welche Ereignisse einen solchen Interrupt auslösen (z.B. Schreib- oder Lesezugriffe) kann dabei vom Programmierer festgelegt werden. Im folgenden soll ein kurzer Überblick über die Debug-Register gegeben werden.
Abb. 1 stellt die verschiedenen Funktionen der Debug-Register DR0..DR7 dar.
Abb. 1: Debug-Register
Alle Debug-Register können ausschließlich über MOV-Befehle angesprochen werden und sind unabhängig von eventuellen Task-Wechseln, d.h. sie werden bei einem Task-Wechsel nicht automatisch gesichert bzw. wiederhergestellt.
Die Register DR0...DR3 können benutzt werden, um Adressen festzulegen. Dabei vergleicht der Prozessor alle Adresszugriffe mit den in diesen Registern festgelegten Werten und führt die Aktion aus, die im DR7 Register eingestellt wurde. Werte in den Registern DR0...DR3 enthalten lineare Adressen, geben also den vom entsprechenden Programm verwendeten Offsetanteil wieder. Dadurch finden Vergleiche oberhalb des Paging-Mechanismus statt und stimmen daher mit der logischen Abarbeitung der Programme überein.
Die Register DR4 und DR5 werden nicht benutzt.
Status-Register DR6Wurde aus irgendeinem Grund ein Interrupt 1 ausgelöst, kann durch die Auswertung der Bits im Register DR6 bestimmt werden, welches Ereignis den Interrupt ausgelöst hat. Der Aufbau des Registers wird in Abb. 2 dargestellt.
Abb. 2: Der Aufbau des Status-Registers
Die Bedeutung der einzelnen Bits wird in Tabelle 1 wiedergegeben.
Bit | Grund für Interrupt 1 |
---|---|
BT | Ein Task-Wechsel wurde ausgelöst und das T (Trap)-Bit des neuen Tasks wurde gesetzt, vergleiche auch mit Kapitel 2.3 Multitasking. |
BS | Einzelschritt-Interrupt, d.h. das TF-Bit im EFLAGS-Register ist gesetzt. Dadurch wird bei jedem Befehl ein Interrupt ausgelöst. |
BD | Ist dieses Bit gesetzt, ist der Intel In-Circuit-Emulator (ICE) aktiv und die Debug-Register können nicht benutzt werden. In diesem Fall können die Debug-Register zwar gelesen werden, jeder Schreibzugriff löst hingegen einen Interrupt 1 aus. Beim Intel-ICE handelt es sich um ein Gerät, das in den Prozessorsockel eingesetzt wird und sich wie ein "normaler" Prozessor verhält, jedoch von einem anderen Computer gesteuert wird. |
B3 | Es wurde auf die über DR3 definierte Adresse zugegriffen. |
B2 | Es wurde auf die über DR2 definierte Adresse zugegriffen. |
B1 | Es wurde auf die über DR1 definierte Adresse zugegriffen. |
B0 | Es wurde auf die über DR0 definierte Adresse zugegriffen. |
Anmerkung: Bits im Register DR6 werden vom Prozessor gesetzt, müssen jedoch z.B. von einem Debugger explizit zurückgesetzt werden.
Status-Register DR7Wurde auf eine in den Registern DR0..DR3 festgelegte Adresse zugegriffen, legt der Inhalt des Registers DR7 fest, ob bzw. wie der Prozessor darauf reagiert. Abb. 3 stellt den Aufbau des DR7 Registers dar.
Abb. 3: Der Aufbau des Debug-Registers 7
Über die jeweils 2 Bit breiten Felder LENx kann für jede der Adressen (DR0...DR3) festgelegt werden, welche Speichergröße adressiert wird (vgl. mit Tabelle 2).
Bitwert | Bedeutung |
---|---|
00 | Byte: Interrupt 1 wird nur ausgeführt, wenn ein Programm exakt auf das (einzelne) Byte zugreift, das durch die Adresse festgelegt wird. |
01 | Word: Interrupt 1 wird ausgeführt, wenn auf das Byte mit der Adresse x bzw. auf das Byte mit der Adresse x+1 zugegriffen wird. Die im entsprechenden Debug-Register festgehaltene Adresse muß geradzahlig sein. |
10 | reserviert |
11 | Dword: Interrupt 1 wird ausgeführt, wenn auf den Adressbereich x bis x+3 zugegriffen wird. Die im entsprechenden Debug-Register festgehaltene Adresse muß ganzahlig durch vier teilbar sein. |
Über die Felder R/Wx kann weiterhin für jede Haltepunkt-Adresse festgelegt werden, welche Zugriffsart einen Interrupt auslöst. Mögliche Werte sind dabei in Tabelle 3 dargestellt.
Bitwert | Bedeutung |
---|---|
00 | Lesezugriff: Ein Haltepunkt beim Lesen von Befehlen setzt voraus, daß das Feld LENx ebenfalls den Wert 00 enthält und das die in DRx festgelegte Adresse auf das erste Byte des Befehls-Opcodes bzw. auf das erste Präfix zeigt. |
01 | Schreibzugriff |
10 | reserviert (ab PENTIUM: I/O Schreib- oder Lesezugriff) |
11 | Schreib- oder Lesezugriff |
Die Bits Lx bzw. Gx geben letztendlich an, ob bzw. wie der Prozessor auf positive Adressvergleiche reagieren soll. Dabei können die in den Registern DRx definierten Haltepunkte entweder lokal (d.h. Lx gesetzt) bzw. global (Gx gesetzt) aktiviert werden. Sind beide Bit zurückgesetzt, reagiert der Prozessor auf positiv ausfallende Adressvergleiche zwar durch das Setzen entsprechender Bits im Register DR6, löst aber keinen Interrupt aus.
Die Unterscheidung zwischen globalen und lokalen Haltepunkten ist nur bei der Verwendung von Multitasking notwendig. Während auf globale Haltepunkte (Gx gesetzt) unabhängig von eventuellen Task-Wechseln reagiert wird, werden gesetzte Lx-Bits bei jedem Task-Wechsel zurückgesetzt. Auf diese Weise können Haltepunkte auf bestimmte Prozesse begrenzt werden. Das setzt jedoch auch voraus, daß ein entsprechender Debugger auf Task-Wechsel reagieren muß, um entsprechende Lx-Bits wieder zu setzen.
Die Bits LE (Local Exact) bzw. GE (Global Exact) sind für alle vier Haltepunkte zuständig und lösen ein Problem, das durch die interne Parallelität bei der Befehlsdekodierung durch den Prozessor entsteht: Wird an der Adresse x ein Haltepunkt erkannt und dadurch ein Debug-Interrupt ausgelöst, enthält das Register CS:EIP bereits die Adresse (die dem Interrupt-Handler übergeben wird) des nächsten oder übernächsten Befehls. Ist eines der beiden Bits gesetzt, wird die Dekodierung soweit verzögert, daß sich die (richtige) Adresse noch sicher rekonstruieren läßt. Während das GE-Bit global für alle Tasks gilt, wird das LE-Bit bei einem Task-Wechsel zurückgesetzt.
Die Beispielprogramme debug_01.asm bis debug_04.asm sollen die Anwendung der Debug-Register verdeutlichen. Neben den Assemblerbeispielen stehen auch die gleichwertigen C-Programme debug_01.c bis debug_04.c zur Verfügung (Mehr über die Verwendung von C, im Zusammenhang mit dem Protected Mode, erfahren Sie unter Protected Mode und C). |
Der Prozessor reagiert auf positiv ausfallende Adressvergleiche, wenn die entsprechenden Bits im Register DR7 gesetzt sind, mit dem Auslösen einer Exception 1 (Debugger-Interrupt).
Soll ein Programm, in der Regel also ein Debugger bzw. hier das Beispielprogramm debug_01, darauf reagieren, so muß ein entsprechender Exception-Handler zur Verfügung gestellt werden. Neben einem solchen Exception-Handler für den Debugger-Interrupt 1 stellt debug_01 auch für alle weiteren Exceptions einen Handler bereit.
Wird eine Exception ausgelöst, führt jeder Handler die folgenden Aktionen durch:
Der allgemeine Exception-Handler bringt dann alle weiteren Registerinhalte auf den Stack, um sie später in hexadezimale Werte umzuwandeln und in einen Speicherbereich im Datensegment einzutragen. Danach löst der Exception-Handler einen Sprung in den Realmode aus und gibt vor der Rückkehr ins DOS den im Protected Mode vorbereiteten Exception-Text auf dem Bildschirm aus.
Der einzige Unterschied eines "normalen" Exception-Handlers zum Exception-Handler des Debug-Interrupts 1 besteht im Beispielprogramm darin, daß dieser zusätzlich die Debug-Register DR0 bis DR3, DR6 und DR7 ausliest und auf dem Bildschirm ausgibt.
Auf diese Weise kann dann festgestellt werden, welcher Breakpoint erreicht wurde bzw. welches Ereignis den Interrupt ausgelöst hat.
Beispielprogramm debug_01.asm ruft den Debug-Interrupt 1 über den Assemblerbefehl INT 1 auf, um so den Exception-Handler zu testen. Im C-Programm debug_01.c wird das Makro BREAKPOINT verwendet, das in der Datei pmode_01.h bereitgestellt wird und ebenfalls INT 1 assembliert.
Beispiel 2Während debug_01 nur den Exception-Handler testete, soll Beispielprogramm 2 (debug_02) einen echten Breakpoint setzen. Dazu wird das Debug-Register DR0 auf eine Funktion gesetzt und das Register DR7 auf "Breakpoint bei Codezugriff" initialisiert. Während das Assemblerbeispiel debug_02.asm diese Änderungen direkt vornimmt, steht dem C-Programm debug_02.c die Funktion set_drx (aus pmode_02.c) zur Verfügung. Diese Funktion soll im weiteren näher besprochen werden.
Die Funktion set_drx erwartet vier Parameter:
Nachdem die Funktion aufgerufen wurde, wandelt sie den Funktionszeiger (void *dst), der einem Offset ins Codesegment entspricht, in eine lineare 32Bit-Adresse um. Die Funktion verwendet dazu das Makro cslin2rel aus pmode_02.h, das die übergebene Adresse umwandelt, indem zu ihr die Start-Offsetadresse des Codesegmentes addiert wird. Der so ermittelte Wert wird später dem durch int x ausgewählten Debug-Register zugewiesen. Als nächstes überprüft die Funktion, ob die übergebenen Variablen int rw und int len gültige Werte besitzen. Stellt die Funktion fest, daß es sich bei einem der übergebenen Werte um einen ungültigen handelt, kehrt die Funktion ohne Änderung mit 'return' zurück.
Anschließend werden die Werte int len und int rw in ein für das Register DR7 benötigtes Format umgerechnet (rw in den unteren 2 Bit und len in den oberen 2 Bit). In Abhängigkeit des Wertes int x wird dann der soeben berechnete 4 Bit breite Wert entsprechend rotiert, die entsprechenden Global-Enable-Bits gesetzt und der Wert in das DR7 Register geschrieben.
Versucht das Hauptprogramm debug_02.c dann die entsprechende Funktion aufzurufen, erkennt das der Prozessor und reagiert mit dem Aufruf des Debugger-Interrupts 1. Gegenüber dem Exception-Handler aus debug_01 wertet der zum Beispielprogramm debug_02 gehörende Exception-Handler die Bits im Register DR6 aus und gibt zusätzlich noch den Grund für die Auslösung des Interrupts in Form eines Textstrings aus (z.B. "Zugriff auf Breakpoint 0").
Beispiel 3Im Gegensatz zu Beispielprogramm debug_02 initialisiert das Programm debug_03 die Debug-Register so, daß ein Interrupt 1 bei einem Schreibzugriff auf eine globale Variable (int global_test) ausgelöst wird. Die Funktion set_drx wurde dafür so geändert, daß sie jetzt lineare 32Bit-Breakpoint-Adressen erwartet. Eventuelle Umrechnungen müssen deshalb vor (!) der Übergabe an set_drx durchgeführt werden. Dazu stellt pmode_03.h neben dem bekannten Makro cslin2rel das Makros dslin2rel bereit. dslin2rel rechnet Offsetadressen in das Datensegment in lineare 32Bit-Adressen um, indem die lineare Startadresse des Datensegments zur übergebenen Adresse addiert wird.
Auch Beispielprogramm 3 liegt in einer C- (debug_03.c) sowie in einer Assembler-Version (debug_03.asm) vor.
Beispiel 4Das vierte Beispielprogramm debug_04 zeigt eine Möglichkeit, den Inhalt einer Variablen bzw. im Allgemeinen einer Speicheradresse zu überwachen. debug_04 setzt dazu, ähnlich debug_03, einen "Schreibzugriffs-Breakpoint" auf eine globale Variable im Datensegment (int global_test). Damit lösen Schreibzugriffe auf diese Variable den Debugger-Interrupt 1 aus. Der entsprechende Exception-Handler kehrt nun nicht in den Realmode und zu DOS zurück, sondern gibt den Inhalt der entsprechenden Speicherzelle aus und beendet den Exception-Handler mit IRET. Dadurch wird das entsprechende Programm fortgesetzt.
Die Programme debug_04.c bzw. debug_04.asm greifen nun in einer Zählschleife auf die Variable global_test zu und lösen damit Exceptions aus. Der jeweilige Variableninhalt wird dabei vom Exception-Handler auf dem Bildschirm dargestellt.