Das VCPI-Interface (Virtual Control Programming Interface) wurde 1989 von den Softwarefirmen PharLap, Hersteller von DOS-Extendern, und Quaterdeck, Hersteller von QEMM, vorgestellt. Diese Spezifikation behandelt Probleme, die die Koexistenz von DOS-Extendern, Multitaskern und Speicherverwaltungsprogrammen betrifft. Wie bereits weiter oben erwähnt wurde, ergeben sich für DOS-Extender Probleme, wenn sie versuchen, auf einem PC, der mit virtuellen EMS-Speicher arbeitet, den Protected Mode zu initialisieren. Der Grund für diese Probleme liegt in der Bereitstellung des virtuellen EMS-Speichers durch die Nutzung des V86-Modus (Vgl. 3.4 Virtueller 8086-Modus (V86). Der VCPI-Standard bietet trotz dieser Tatsache die Möglichkeit, in den Protected Mode zu schalten. Darüber hinaus stellt er Funktionen zur Verfügung, die z.B. den Zugriff auf das Extended Memory regeln.
Prinzip: Client und ServerDen Client-Server-Modellen im Netzwerkbereich nicht unähnlich, basiert der VCPI-Standard auf dem Zusammenspiel zwischen Clients und einem Server. Für VCPI stellt ein Server ein Programm dar, das VCPI-Funktionen zur Verfügung stellt. Diese Funktionen können von verschiedenen Clients (Kunden, Programme wie beispielsweise DOS-Extender) genutzt werden. Unter dem VCPI-Standard bezeichnet der Server-Begriff immer ein Speicherverwaltungsprogramm (z.B. EMM386.EXE). Diese Speicherverwaltung dient dabei den Clients als gemeinsame Schnittstelle zu den System-Resourcen: Protected Mode und Extended Memory. Da diese Speicherverwaltungsprogramme in den meisten Fällen bereits beim Systemstart eingebunden werden, z.B. als Treiber in der CONFIG.SYS, steht der VCPI-Server allen Programmen gleichermaßen zur Verfügung.
VCPI Funktionen / DiensteVCPI-Server stellen ihren Clients 13 verschiedene Funktionen zur Verfügung. Diese Funktionen können in die folgenden Bereiche eingeteilt werden:
Alle Funktionen wurden als Erweiterung des EMS-Standards implementiert und sind über den Interrupt 67h, Unterfunktionsnummer DEh (AH=DEh) ansprechbar. Alle VCPI-Funktionen liefern einen Rückehrcode im AH-Register, dabei steht "00h" für keine Fehler. Entsprechend zeigen Werte ungleich 0 einen aufgetretenen Fehler an.
Funktionen 1. Initialisierung des VCPIBevor Funktionen des VCPI genutzt werden können, muß festgestellt werden, ob überhaupt ein VCPI-Server im System vorhanden ist. Dazu muß zunächst geprüft werden, ob ein EMS-Treiber installiert wurde (Vgl. 1.3.3 EMS/XMS). Konnte ein EMS-Treiber gefunden werden, ist es weiterhin erforderlich, eine EMS-Seite über die "normale" EMS-Schnittstelle anzufordern. Das ist notwendig, da manche EMS-Treiber den Prozessor solange im Realmode belassen, bis ihre Dienste benötigt werden, erst dann schalten sie in den V86-Modus. Da sich VCPI-Server nur im V86-Modus zu erkennen geben, muß vorher sichergestellt sein, daß der EMS-Treiber aktiv ist.
Jetzt kann festgestellt werden, ob der verwendete EMS-Treiber die VCPI-Schnittstelle unterstützt. Für diesen Zweck existiert die Funktion 00h des VCPI:
Funktion 00h: Test auf Vorhandensein eines VCPI-Servers Aufruf mit AX = DE00h Rückgabe AH = 00h, Erfolg BX = Versionsnummer BCD AH != 00h (=84h) Fehler, kein VCPI vorhanden
Konnte auf diese Weise ein VCPI-Server erkannt werden, können die Vorbereitungen für die Initialisierung des Protected Mode fortgesetzt werden.
Obwohl VCPI-Server und Clients die gleichen linearen Adressen benutzen können, darf es nicht unkontrolliert möglich sein, daß sie die gleichen physischen (!) Speicherbereiche benutzen, da es sonst möglich wäre, daß sich Server und Clienten gegenseitig überschreiben. Da das Paging bei Verwendung des VCPI-Standards aufgrund des EMM immer aktiv ist, müssen die vom Server und vom Client verwendeten Pagetables untereinander koordiniert werden. Den physischen Speicher teilen bedeutet dabei, entweder nur eine Pagetable für Server und Clients zu verwenden und damit das Register CR3 immer unverändert zu lassen oder teilweise mit identischen Pagetables zu arbeiten. Der VCPI-Standard macht von der letztgenannten Möglichkeit Gebrauch.
Es existiert eine VCPI-Funktion, die die Pagetable des Clients so initialisiert, daß die ersten 256 Einträge (1 MB) das erste MByte des linearen Adressraums auf das erste MByte des physischen Adressraums abbilden. Alle weiteren Pages stehen dem Client zur Verfügung. Die Initialisierung der Pagetables wird dabei von der Funktion 01 des VCPI-Standards übernommen:
Funktion 01h: get protected mode interface Aufruf mit AX = DE01h ES:DI -> 4KB großer Pagetable Buffer DS:SI -> 24 Byte großer Buffer, für die Aufnahme von 3 Segmentdeskriptoren Rückgabe AH = 00h, Erfolg DI = Erster unbenutzter Eintrag im Buffer(Offset) EBX -> Einsprungspunkt für VCPI-Funktionsaufrufe aus dem Protected Mode AH != 00h, Fehler
Der Funktion wird in ES:DI ein Zeiger auf einen Buffer übergeben, der die erste Pagetable aufnimmt. Diese Pagetable muß der erste Eintrag in einem vom Client zu verwaltenden Page-Directory sein. Unter dem durch das Registerpaar DS:SI identifizierten, 24 Byte großen Puffer werden weiterhin 3 Segmentdeskriptoren abgelegt. Der Client muß dafür sorgen, daß diese 3 Deskriptoren in eine GDT (Globale Deskriptor Tabelle) eingebaut werden.
Der erste Deskriptor beschreibt dabei das Codesegment des VCPI-Servers und dient mit dem in EBX zurückgelieferten Offset als Einsprungsadresse für den Aufruf der VCPI-Funktionen aus dem Protected Mode heraus (als FAR-CALL aufgerufen). Der von der Funktion 01 im Register DI zurückgelieferte Wert gibt das Offset in der Pagetable an, an der sich der erste nicht belegte Pagetable Eintrag befindet. Ab diesem Eintrag steht die Pagetable dem Client zur Verfügung (Vgl. Abb. 4.1).
Damit sich unterschiedliche Clients im Speicher nicht gegenseitig stören, müssen alle das Extended Memory betreffenden Speicheranforderungen über die VCPI-Schnittstelle durchgeführt werden. Nur auf diese Weise können z.B. DOS-Extender und Speicherverwaltungen problemlos nebeneinander verwendet werden.
Bevor ein Client Speicher über VCPI anfordert, kann er feststellen, ob überhaupt noch Speicher zur Verfügung steht. Über die Funktion 02 der VCPI-Schnittstelle kann dazu die phys. Adresse der höchsten 4KByte großen Speicherseite angefordert werden:
Funktion 02h: get max. physical memory address Aufruf mit AX = DE02h Rückgabe AH = 00h, Erfolg EDX-> Adresse der höchsten ansprechbaren 4K Speicherseite AH != 00h Fehler
Anmerkung: Der in EDX zurückgelieferte Wert entspricht der (theoretisch) höchsten ansprechbaren 4KB Speicherseite und muß nicht unbedingt dem tatsächlich vorhandenen Speicher entsprechen.
Über die Funktion 03h kann die Anzahl der noch freien 4KB Seiten ermittelt werden:
Funktion 03h: get number of free 4K pages Aufruf mit AX = DE03h Rückgabe AH = 00h, Erfolg EDX-> number of free 4K pages AH != 00h, Fehler
Maximal kann die in EDX zurückgelieferte Anzahl 4KByte großer Speicherseiten über die Funktion 04h angefordert werden:
Funktion 04h: allocate a 4K Page Aufruf mit AX = DE04h Rückgabe AH = 00h, Erfolg EDX-> physische Adresse der allokierten 4K Speicherseite AH != 00h Fehler
Der reservierte Speicher kann dabei aus ganz unterschiedlichen Speicherbereichen stammen. Der Client muß als nächstes einen Segmentdeskriptor in seiner GDT anlegen, über den der Zugriff auf die reservierte Seite stattfindet (der Bereich kann dabei natürlich größer als 4 KByte sein, wenn mehrere Speicherseiten zusammengefaßt werden). Weiterhin muß der Client einen Eintrag in seiner Pagetable anlegen, der auf die reservierte Seite zeigt. Dabei muß beachtet werden, daß die Funktion 01h bereits Einträge in der Pagetable belegt hat.
Der Offset des ersten freien Eintrags muß daher noch ermittelt werden. Funktion 01 lieferte dazu die Offsetadresse des ersten freien Eintrags im Register DI zurück. Den Index des ersten freien Pagetable Eintrages erhält man, indem man von der ursprünglichen Offsetadresse (der Fkt. 01 im Register ES:DI übergeben) den Wert des Registers DI (von Fkt. 01 zurückgeliefert) abzieht. Dadurch erhält man die Länge der belegten Pagetable in Byte.
Anmerkung: Betrachtet man diese Differenz in der Einheit KByte, ergibt sich die erste freie Adresse im linearen Adressraum! Durch eine Division mit 4 (jeder Eintrag ist 4 Byte lang) ergibt sich der Index für die erste freie Page.
Benötigt der Client die reservierten Speicherseiten nicht mehr, können sie über die Funktion 05 wieder freigegeben werden:
Funktion 05h: free 4K Page Aufruf mit AX = DE05h EDX-> physikalische Adresse einer 4 KB Speicherseite Rückgabe AH = 00h, Erfolg AH != 00h Fehler
Über die Funktion 06h kann zu einer linearen Adresse im ersten MB festgestellt werden, wo die zu dieser Seite gehörende physische Speicherseite liegt (z.B. wo befindet sich der physische Ursprung einer EMS-Speicherseite, die an der Adresse E000h:0000h eingeblendet wird ?)
Funktion 06h: get physical adresse of page in first MB Aufruf mit AX = DE06h CX = Seiten Nummer (lineare Adresse durch 4096 dividiert, bzw. um 12 Bit nach rechts verschoben) Rückgabe AH = 00h Erfolg EDX-> physische Adresse der Speicherseite AH != 00h Fehler3. Zugriff auf Spezialregister des Prozessors (CR0, Debug-Register)
Über die Funktion 07h kann das Statusregister CR0 des Prozessors ausgelesen werden:
Funktion 07h: read CR0 Aufruf mit AX = DE07h Rückgabe AH = 00h Erfolg EBX-> Inhalt des Registers CR0
Anmerkung: Anstelle dieser Funktion kann auch über den "normalen" MOV-Befehl der Inhalt des Registers CR0 ausgelesen werden. Da "MOV EAX,CR0" in jeder Privilegstufe ausgeführt werden kann, besteht keine Notwendigkeit, die Funktion 07 zu benutzen.
Die Funktionen 08h und 09h erlauben es beispielsweise Debuggern, die Inhalte der DEBUG-Register auszulesen bzw. zu verwenden. Im Gegensatz zur Funktion 07h unterliegt der Zugriff auf die Debug-Register der Privilegierung.
Funktion 08h: read debug registers Aufruf mit AX = DE08h ES:DI-> Zeiger auf Array für 8 Doublewords Rückgabe AH = 00h Erfolg, Buffer wurde mit Debug-Registern DR0..DR7 gefüllt (DR4 und DR5 werden nicht benutzt)
Funktion 09h: set debug registers Aufruf mit AX = DE09h ES:DI-> Zeiger auf Array für 8 Doublewords (enthält neue Werte) Rückgabe AH = 00h Erfolg, Werte für DR4 und DR5 werden nicht benutzt
Mehr über die Debug-Register finden sie unter: Die Debug-Register.
4. Programmierung des Interrupt-ControllersUm zu verstehen, warum ein VCPI-Client überhaupt die Möglichkeit besitzen darf, die Einstellungen des Interrupt-Controllers zu verändern, ist eine kurze Erklärung der durch den Interrupt-Controller durchgeführten Interrupt-Verwaltung notwendig.
Computer ab dem AT (80286 Prozessor) besitzen (unabhängig vom VCPI-Standard) zwei Interrupt-Controller, um je 8 Hardware-Interrupts verwalten zu können. Die 8 Interrupts des zweiten Controllers sind dabei über den Interrupt 2 des ersten Controllers miteinander verbunden (kaskadiert). Tritt nun eine Interrupt-Anforderungen der Leitungen 8 bis 15 (also 0 bis 7 am zweiten Interrupt-Controller) auf, wird ein Interrupt 2 am ersten Interrupt-Controller ausgelöst.
Die auftretenden Interrupts werden dabei so auf die Interrupt-Vektortabelle abgebildet, wie es in Tabelle 4.1 dargestellt wird.
Interrupt Nr. | Belegung |
---|---|
00h | CPU: Division durch NULL |
01h | CPU: Einzelschritt |
02h | CPU: NMI |
03h | CPU: Breakpoint |
04h | CPU: numerischer Überlauf |
05h | Hardcopy |
06h | unbekannter Befehl |
07h | reserviert |
08h | IRQ0: Timer |
09h | IRQ1: Tastatur |
0Ah | IRQ2: 2ter Interrupt-Controller (nur AT) |
0Bh | IRQ3: serielle Schnittstelle 2 |
0Ch | IRQ4: serielle Schnittstelle 1 |
0Dh | IRQ5: Festplatte |
0Eh | IRQ6: Diskette |
0Fh | IRQ7: Drucker |
10h..6Fh | reserviert / spez. Geräte (z.B. Maus) / frei |
70h | IRQ8: Echtzeituhr |
71h | IRQ9: frei |
72h | IRQ10: frei |
73h | IRQ11: frei |
74h | IRQ12: frei |
75h | IRQ13: 80287 NMI |
76h | IRQ14: Fetsplatte |
77h | IRQ15: frei |
78h..FFh | reserviert / frei |
Wie aus Tabelle 4.1 ersichtlich, werden die Hardwareinterrupts 0..7 (IRQ0 ... IRQ7) auf die Interrupt-Vektoren 08h..0Fh und die Hardwareinterrupts 08..0Fh (IRQ8 ... IRQ15) auf die Interrupt-Vektoren 70h..77h umgeleitet. Im Gegensatz zum Realmode ergibt sich im Protected Mode ein Problem, das mit den Prozessor-Exceptions zusammenhängt. Intel belegt im Zusammenhang mit seinen Prozessoren die Interrupts 00..0Fh mit den Prozessorexceptions 00..0Fh. Tritt nun beispielsweise ein Timer-Interrupt auf (Hardware-Interrupt 8 (IRQ0), 18,2 mal pro Sekunde, umgeleitet auf Interrupt-Vektor 8) würde im Protected Mode alle 18,2 mal in der Sekunde eine Double-Fault Exception ausgelöst werden. Das ist auch der Grund dafür, daß alle in den vorangegangenen Kapiteln beschriebenen Beispielprogramme nur mit gesperrten Interrupts korrekt arbeiten!
Die Lösung dieses Problems besteht darin, den ersten Interrupt-Controller anzuweisen, seine (Hardware-) Interrupts nicht auf die Interrupts 08..0Fh umzuleiten, sondern an eine andere, freie Stelle innerhalb der Interrupt-Vektortabelle (bzw. der IDT im Protected Mode).
Dazu können unter der Nutzung der VCPI-Schnittstelle die Funktionen 0Ah und 0Bh genutzt werden, um diese Veränderung durchzuführen. Die VCPI-Funktion 0Ah dient dazu, die aktuellen Interrupt-Controller Einstellungen abzufragen:
Funktion 0Ah: get interrupt vector mappings Aufruf mit AX = DE0Ah Rückgabe AH = 00h, Erfolg BX = erster Vektor für den ersten Interruptcontroller, Interrupts 0..7 (Standard 8) CX = erster Vektor für den zweiten Interruptcontroller, Interrupts 8..15 (Standard 70h)
Besitzen die von der Funktion 0Ah in den Registern BX und CX zurückgelieferten Werte nicht mehr ihre originalen Inhalte (8, 70h), darf der Client diese Werte nicht mehr verändern, da diese Änderung bereits durch einen anderen Client durchgeführt wurde.
Eine Umleitung der Interrupts kann der VCPI-Schnittstelle durch die Funktion 0Bh mitgeteilt werden, wenn über die Funktion 0Ah festgestellt wurde, daß beide Interruptvektoren (für den ersten und zweiten Controller) ihre Orginalwerte enthalten:
Funktion 0Bh: set interrupt vector mappings Aufruf mit AX = DE0Bh BX = erster Vektor für den ersten Interruptcontroller, Interrupts 0..7 CX = erster Vektor für den zweiten Interruptcontroller, Interrupts 8..15 Rückgabe AH = 00h, Erfolg AH != 00h, Fehler
Anmerkung: Funktion 0Bh dient nur der Mitteilung der Veränderung an den VCPI-Server! Der Client muß selbst die Interrupt-Belegung durch Neuprogrammierung des Interrupt-Controller verändern.
Die Umprogrammierung der Interrupt-Controller erfordert ihre Neuinitialisierung. Der genaue Vorgang sowie die Zusammenhänge können in entsprechender Literatur zur Systemprogrammierung nachgelesen werden (z.B. Tischer: "PC-Intern" 4.0). An dieser Stelle soll nur der erforderliche Assemblercode kurz dargestellt werden:
; Neuinit der Interrupt-Controller, sowie neu Setzen des ; Interrupt Mappings mov al,00010001b ; kaskadierte Interrupt-Controller out 020h,al ; und Flankentriggerung an beider Controller out 0a0h,al ; senden ; neuer IRQ-Vektor für den ersten Controller einstellen mov al,?? ; Standardwert = 8 out 021h,al ; neuer IRQ-Vektor für zweiten Controller einstellen mov al,?? ; Standardwert = 70h out 0a1h,al ; Kaskadierung beider Controller einstellen mov al,00001000b ; Kaskadierung über IRQ2 out 021h,al mov al,00000100b ; Kaskadierung über IRQ2 out 0a1h,al ; PIC auf Intel-Umgebung und manuelle Int.- Beendigung einstellen mov al,00000001b out 021h,al out 0a1h,al5. Umschaltung zwischen V86-Modus und Protected Mode
Funktion 0Ch dient dem kontrolliertem Übergang zwischen dem V86-Modus und dem Protected Mode. Wird die Funktion im V86-Modus über den "normalen" EMS-Interrupt angesprungen, wird ein Wechsel vom V86-Modus in den Protected Mode durchgeführt. Befindet sich der Computer jedoch im Protected Mode und wird Funktion 0Ch über ein FAR-Call zum VCPI-Server aktiviert (vergleiche VCPI-Funktion 01), wird der V86-Modus initialisiert.
Je nachdem, in welchem Modus sich der Computer zum Zeitpunkt des Funktionsaufrufes befindet, erwartet die Routine eine unterschiedliche Registerbelegung.
Aufruf im V86-Modus, Int 67h: Funktion 0Ch: switch to protected mode Aufruf mit AX = DE0Ch ESI= lineare Adresse innerhalb des ersten phys. MB eines Buffers, der Werte für die Prozessorregister im Protected Mode enthählt (siehe untern)
Die Datenstruktur zum Funktionsaufruf ist in Tabelle 4.2 dargestellt (FWORD ist eine 6 Byte lange Datenstruktur, die eine Selektor:(32Bit) Offsetadresse beinhaltet).
Offset | Bedeutung | Typ |
---|---|---|
0000h | CR3 (Startadresse des Page-Directory) | DWORD |
0004h | lineare Adresse einer Variablen unterhalb der 1-MByte Grenze, die einen FAR-Zeiger (FWORD) auf die GDT enthält | DWORD |
0008h | lineare Adresse einer Variablen unterhalb der 1-MB Grenze, die einen FAR-Zeiger (FWORD) auf die IDT enthält | DWORD |
000Ch | Selektor für die LDT | WORD |
000Eh | Selektor für das TR-Register | WORD |
0010h | Einsprungsadresse in den Programmcode nach Umschaltung in den Protected Mode (Selektor:32-Bit-Offset) | FWORD |
Nach dem Aufruf der Funktion befindet sich der Computer im Protected Mode. Die Register GDTR, IDTR, LDTR, CR3 und TR wurden mit den Werten aus der Datenstruktur initialisiert. Die Interrupts sind gesperrt und der Inhalt der folgenden Register wurde zerstört bzw. ist ungültig: EAX, ESI, DS, ES, FS, GS. Die jetzt ausgeführte Protected-Mode Routine ist dafür verantwortlich, einen Stack bereitzustellen, die Selektorregister DS, ES, FS und GS mit gültigen Segmentselektoren zu laden und die Interrupts wieder zuzulassen.
Die Rückkehr in den V86-Modus wird ebenfalls über die Funktion 0Ch durchgeführt. Der Einsprungspunkt aus dem Protected Mode wird jetzt jedoch über den Codesegmentselektor des VCPI-Servers und den von Funktion 01 zurückgelieferten Offsetwert gebildet. Die Registerbelegung für den Rücksprung ist auch eine andere.
Funktion 0Ch: switch to V86-Mode Aufruf mit AX = DE0Ch DS = Segmentselektor auf Datensegment (lineares Segment von der Größe 1MB, mit der Basisadresse 0) SS:ESP-> Datenstruktur mit neuen Registerwerten (siehe Tabelle 4.3), muß unterhalb der 1 MB-Grenze liegen!
Offset | Bedeutung | Typ |
---|---|---|
00h | reserviert | QWORD (8 Byte) |
08h | EIP nach Umschaltung in V86-Mode | DWORD |
0Ch | CS nach Umschaltung in V86-Mode | DWORD |
10h | reserviert (EFLAGS-Register) | DWORD |
14h | ESP nach Umschaltung | DWORD |
18h | SS nach Umschaltung | DWORD |
1Ch | ES nach Umschaltung | DWORD |
20h | DS nach Umschaltung | DWORD |
24h | FS nach Umschaltung | DWORD |
28h | GS nach Umschaltung | DWORD |
Anmerkung: Bei den Werten für die Segmentregister handelt es sich um "normale" 16 Bit breite Realmode-Segmentadressen. Da für die einzelnen Einträge in der Datenstruktur (Tabelle 4.3) 32 Bit reserviert sind, wird der neue Wert für das entsprechende Segmentregister im ersten Wort (an der niedrigeren Adresse) gespeichert, das zweite Wort bleibt ungenutzt.
Nach der Umschaltung sind die Interrupts ausgeschaltet, das nun ausgeführte Realmode Programm muß die Interrupts über den Befehl STI wieder zulassen.
Achtung: Um eine korrekte Rückschaltung in den Realmode durchzuführen, müssen die folgenden Bedingungen erfüllt sein:
Das in Assembler geschriebene Beispielprogramm pm_05.asm initialisiert unter Nutzung der VCPI-Funktionen den Protected Mode und kehrt anschließend in den V86-Mode zurück. Bevor die Umschaltung in den Protected Mode durchgeführt wird, legt das Beispielprogramm eine Pagetable und ein zugehöriges Page-Directory an. |
Wie bereits in der Einleitung erwähnt wurde, ist die Hauptaufgabe
des VCPI, in erster Linie das Zusammenarbeiten von DOS-Extendern
und Speicherverwaltungen zu gewährleisten. VCPI wird daher
vor allem von DOS-Extendern verwendet, um die Umschaltung in den
Protected Mode vorzunehmen. Ein "normales"
Anwendungsprogramm bzw. ein Programm, das unter einem DOS-Extender
ausgeführt wird, braucht sich in der Regel nicht mit dem
VCPI-Standard auseinanderzusetzen. Daher muß sich in der Regel
auch nur der Programmierer eines DOS-Extenders mit den Funktionen
des VCPI beschäftigen. Eine Zusammenfassung aller VCPI-Funktionen
findet sich unter vcpispec.zip.
Ein weiterer Standard, ähnlich dem VCPI, ist das DOS Protected
Mode Interface (kurz: DPMI), das im Folgenden vorgestellt werden soll.