Dockers DEV Site


Updates · Faq · Home 

Protected Mode Programmierung in C

Einleitung

Alle bisherigen Beispielprogramme, in denen der Protected Mode direkt programmiert wurde, nutzten ausschließlich Assembler als Programmiersprache. Wenn man bedenkt, daß die dazu benötigten Befehle, z.B. LGDT oder Zugriffe auf die Spezialregister: CR0, CR3 usw., in der Regel nur von einem Assembler und nicht von einem Hochsprachen-Compiler unterstützt werden, ist die Nutzung eines Assemblers die logischere Alternative.

Da der Aufbau eines Assemblerprogramms in der Regel komplizierter zu verstehen ist, als die Struktur eines gleichwertigen Hochsprachen-Programmes, könnte es von Vorteil sein, entsprechende Programme auch in einer Hochsprachen-Version analysieren zu können. Letzteres trifft besonders auf Protected Mode Programme zu, denn eine Umschaltung in den Protected Mode setzt eine erhebliche Anzahl von Vorbereitungen voraus (z.B. müssen Deskriptortabellen und die entsprechenden Register initialisiert oder Page-Table Strukturen angelegt werden).

Wird versucht, Protected Mode mit einer Hochsprache zu verbinden, muß als erstes zwischen zwei verschiedenen Methoden unterschieden werden. Zum einen kann eine Hochsprache, in den Beispielen wird das C sein, genutzt werden, um den Programmcode zu erzeugen, der letztendlich im Protected Mode ausgeführt wird. Dabei "kümmert" sich ein Assemblerprogramm um die eigentliche Initialisierung des Protected Mode und führt dann den vom Compiler generierten Programmcode aus. Diese Möglichkeit wird mit dem Beispielprogramm 2 demonstriert.

Die andere Möglichkeit besteht darin, daß das Hochsprachenprogramm selbst den Protected Mode initialisiert und dabei, soweit wie möglich, auf den Einsatz von Assemblerbefehlen verzichtet. Wie so etwas aussehen könnte, soll im folgenden Beispiel demonstriert werden.

Protected Mode Initialisierung unter C

Ein minimales Programm, das den Prozessor vom Realmode in den Protected Mode schaltet, muß mindestens die folgenden Schritte ausführen:

Abgesehen von den dazu benötigten speziellen Assemblerbefehlen, läßt sich das auch in einer Hochsprache realisieren. Anhand der GDT-Initialisierung soll das im folgenden gezeigt werden.

Die GDT

Bei der Globalen Deskriptor Tabelle (kurz: GDT) handelt es sich um eine Struktur, die die sogenannten Segment-Deskriptoren aufnimmt. Dadurch gehört die GDT zu den wichtigsten Strukturen des Protected Mode, da sie alle nutzbaren Segmente definiert.

Das Beispielprogramm cpm_01.c deklariert für die Verwaltung von Segment-Deskriptoren die C-Struktur SEG_DESKRIPTOR.

typedef struct
    {
    WORD    segsize_0_15;            /* Segment-Size Bit  0..15 */
    WORD    segbase_0_15;            /* Segment-Base Bit  0..15 */
    BYTE    segbase_16_23;           /* Segment-Base Bit 16..23 */
    BYTE    segtype;                 /* Segment-Typ             */
    BYTE    segsize_16_19;           /* Segment-Size Bit 16..19 */
                                     /* Special Info. G-Bit etc.*/
    BYTE    segbase_24_31;           /* Segment-Base Bit 24..31 */
    } SEG_DESKRIPTOR;

Die GDT wird dann später als statisches Array dieser SEG_DESKRIPTOR-Strukturen definiert, wobei die Funktion deskr_setattrib die Initialisierung dieser Strukturen (Segment-Deskriptoren) übernimmt.

Die Funktion deskr_setattrib erwartet die folgenden Aufrufparameter.

    des        Zeiger auf eine Deskriptor-Struktur im Speicher (in die GDT)
    size       Größe des Segmentes in Byte (0..1MB)
    segment    (Realmode) Segmentadresse des Segment-Speichers
    offset     (Realmode) Offsetadresse des Segment-Speichers
    type       Segment-Typ Byte
    gran       Zusätzliches Info. Byte (Granularity, 80386 Segment ...)
               (High-Nibble des Byte 7)

deskr_setattrib organisiert die übergebenen Werte so, daß sie dem Format von SEG_DESKRIPTOR entsprechen. Der über des adressierte Deskriptor wird auf diese Weise initialisiert.

Für die Parameter type und gran stehen zusätzlich die folgenden Konstanten zur Verfügung.

/* Konstante für type */
    ACCESSED            0x01
    DATASEG_NOTREADABLE 0x00
    DATASEG             0x02
    DATASEG_RESERVED    0x04
    DATASEG_EXPANDABLE  0x06                /* "expand-down" Datensegment */
    CODESEG_NOTREADABLE 0x08
    CODESEG             0x0A
    CODESEG_CONFORM_NR  0x0C
    CODESEG_CONFORM     0x0E
    SEGMENT             0x10
    SYSTEM              0x00
    DPL0                0x00
    DPL1                0x20
    DPL2                0x40
    DPL3                0x60
    PRESENT             0x80

/* Konstante für gran */
    GRAN                0x40
    _80386              0x30
    AVL                 0x10

Die Konstanten wurden dabei dem Deskriptor-Format angepaßt. Dadurch ist es möglich, durch Ausdrücke wie:

   type= DATASEG | PRESENT | DPL0 | SEGMENT

den passenden Wert für das Segmenttyp-Byte des Deskriptors zu ermitteln. Auf die gleiche Weise ist das auch für das als gran bezeichnete Feld des Deskriptors möglich.

Das folgende Beispiel demonstriert den Aufruf der Funktion deskr_setattrib. Der Deskriptor 1 einer als statisches Array definierten Globalen Deskriptor Tabelle gdt wird dabei auf einen Speicherbereich initialisiert, der an der physischen Adresse 0 beginnt und den gesamten Adressbereich von 4 GB umfasst.

           deskr_setattrib (&gdt;[1],        /* Zeiger auf Deskriptor der GDT */
                            0xFFFFF,        /* Segmentgröße                  */
                                  0,        /* Segmentadresse                */
                                  0,        /* Offsetadresse                 */
 DATASEG | SEGMENT | DPL0 | PRESENT,        /* Segmenttyp                    */
                      GRAN | _80386);       /* 80386 und 4KB Segmenteinheit  */

Das Beispielprogramm cpm_01.c stellt auf diese Weise gültige Deskriptoren für die verwendeten Segmente bereit. Dabei werden die Protected Mode Deskriptoren so initialisiert, daß sie den verwendeten Realmode-Segmenten entsprechen. Auf diese Weise kann Programmcode dort ausgeführt werden, wo er im Speicher steht. Auch bei Datenzugriffen wird so eine eventuell notwendige, zusätzliche Relokation verhindert. Zusätzlich zu diesen Alias-Segmenten legt das Beispielprogramm auch einen Deskriptor für den direkten Zugriff auf den Bildschirmspeicher an. Dieser wird später benutzt, um eine kurze Meldung auszugeben.

Der letzte Schritt eine GDT einzurichten ist es, dem Prozessor die Startadresse der GDT im Speicher mitzuteilen. Dazu steht der Assemblerbefehl LGDT zur Verfügung. LGDT erwartet als Parameter die Adresse einer speziellen Struktur, die die Position und die Größe der GDT festhält. Das Beispielprogramm cpm_01.c deklariert dazu die Struktur GDTSTRUC:

    typedef struct
        {
        WORD    limit;              /* GDT-Länge in Byte                          */
        DWORD   base;               /* Startadresse der GDT (32 Bit phys. Adresse */
        } GDTSTRUC;
sowie die Funktion lgdt.

Die Funktion lgdt legt eine Variable der oben deklarierten Struktur GDTSTRUC an (gdt_adr) und übernimmt auch die notwendige Initialisierung dieser Variablen sowie das Setzen der GDT-Startadresse. Dazu muß zunächst das Feld gdt_adr.limit auf die GDT-Größe gesetzt werden. Der Wert für gdt_adr.limit wird dabei mit der folgenden einfachen Formel berechnet.

    limit= [Anzahl_benutzter_Deskriptoren] * 8;
Die Multiplikation mit 8 ergibt sich aus der Tatsache, daß Deskriptoren immer 8 Byte lang sind.

Als nächstes muß die Funktion lgdt das Feld gdt_adr.base initialisieren. gdt_adr.base ist die 32 Bit breite, physische (bzw. lineare) Adresse der GDT im Speicher. Die Funktion muß deshalb die Adresse der GDT im Format Segment:Offset ermitteln und sie dann in eine 32 Bit physische Adresse umwandeln. Der folgende Programmausschnitt nimmt genau diese Umwandlung vor.

    segment= FP_SEG (&gdt;[0]);
    offset = FP_OFF (&gdt;[0]);

    gdt_adr.base= (segment << 4) + offset;
segment und offset wurden dabei als 32 Bit-Variablen deklariert, damit die Umrechnung korrekt durchgeführt wird. (Warum das so sein muß und was noch bei der hier dargestellten Programmierung beachtet werden muß, wird weiter unten auf dieser Seite geklärt, siehe Probleme.)

Als letztes muß die Funktion lgdt nur noch den Befehl lgdt assemblieren. Da das wohl nicht im C-Syntax vorgesehen wurde :), wird auf den Inline-Assembler von Borland C zurückgegriffen:

    asm {
        lgdt gdt_adr
        }

An dieser Stelle ist die GDT eingerichtet und der Protected Mode kann initialisiert werden. Dem C-Programm cpm_01.c steht dazu die Funktion enable_pmode zur Verfügung. Diese Funktion soll als nächstes betrachtet werden.

Protected Mode aktivieren

Bevor die Funktion enable_pmode den Protected Mode aktiviert, werden die aktuellen Realmode-Segmentadressen in globalen Variablen gerettet. Das ist deshalb notwendig, weil das Programm später in den Realmode zurückkehrt und dabei die Inhalte der Segmentregister wiederherstellt. Der folgende Programmcode nimmt diese Sicherung vor und verwendet dabei die vom BC 3.1 Compiler bereitgestellten Makros _CS, _DS usw.

    /* aktuelle Realmode Segmentadressen sichern                    */
    /* rmode_cs, rmode_ds usw. sind als globale Variable deklariert */
    rmode_cs= _CS;
    rmode_ds= _DS;
    rmode_es= _ES;
    rmode_ss= _SS;

Da bei der Umschaltung keine Interrupts auftreten dürfen, wird als nächstes die Funktion disable verwendet, um das Interrupt-Enable-Flag im Flagregister zu löschen.

Erst jetzt darf das Bit 1 des Registers CR0 gesetzt werden, um so in den Protected Mode zu schalten. Da auch für den Zugriff auf das CR0-Register kein C-Befehl zur Verfügung steht, muß auch hier auf den Inline-Assembler zurückgegriffen werden.

    /* pmode init: CR0, Bit 1 setzen */
    asm {
        mov eax,cr0
        or eax,1
        mov cr0,eax
        }

Obwohl sich der Prozessor jetzt bereits im Protected Mode befindet, ist die Initialisierung noch nicht beendet! Denn zu diesem Zeitpunkt befinden sich noch Realmode-Befehle in der Warteschlange und die Segmentregister enthalten alle noch Werte aus dem Realmode. Die Funktion enable_pmode muß jetzt also noch die Warteschlange löschen und dafür sorgen, daß die benutzten Segmentregister gültige Werte besitzen.

Die Warteschlange wird mit der Ausführung eines Sprunges gelöscht. Obwohl dafür ein NEAR-Sprung ausreichen würde, verwendet enable_pmode einen FAR-Sprung, um gleichzeitig den Codesegment-Selektor mit einem gültigen Wert zu initialisieren:

    asm {
        db 0x0ea                               /* assembliert FAR-JMP */
        dw offset pmode                        /* zur Adresse:        */
        dw sCODE                               /*        sCODE:Offset */

    pmode:
        }

Als letzten Schritt benutzt die Funktion enable_pmode die vom BC 3.1 Compiler bereitgestellten Makros _DS, _ES usw., um die Segmentregister mit gültigen Selektorwerten für den Protected Mode zu initialisieren.

     _DS= sDATA;
     _ES= sDATA;
     _SS= sSTACK;
Textausgabe im Protected Mode

Nach der Ausführung von enable_pmode befindet sich der Prozessor im Protected Mode. Das hat u.a. die Folge, daß weder BIOS- noch DOS-Funktionen zur Verfügung stehen. Denn die meisten DOS-Funktionen sind vom Inhalt der Segmentregister abhängig, die jedoch im Protected Mode anders interpretiert werden. Aus diesem Grund kann zum Beispiel auch die C-Funktion printf nicht genutzt werden.

Eine Textausgabe kann deshalb nur direkt in den Bildschirmspeicher erfolgen. Das Beispielprogramm cpm_01.c legt dazu einen Segmentdeskriptor an, der das Videosegment im Textmodus beschreibt. Dieses Segment beginnt an der physischen Adresse 0B8000h und ist 4000 Byte lang. Die Funktion print gibt einen Text auf dem Bildschirm aus. Dazu wird über das aus dem Realmode bekannten Makro MK_FP ein FAR-Pointer auf das Videosegment erzeugt. Der Text wird anschließend byteweise, unter Nutzung dieses FAR-Pointers, auf den Bildschirm kopiert.

Verwaltung der IDT

Das Beispielprogramm cpm_01.c verzichtete aus Gründen der Einfacheit auf das Anlegen einer IDT und verhindert die Ausführung von Interrupts durch das Löschen des Interrupt-Freigabe-Flags (Befehl CLI). An dieser Stelle soll jedoch auch die Nutzung einer IDT demonstriert werden.

Das Beispielprogramm cpm_02.c stellt dafür, ähnlich cpm_01.c, Funktionen und Datentypen zur IDT-Verwaltung zur Verfügung. Eine wichtige Datenstruktur für die IDT ist ein besonderer Deskriptor, der sogenannte Gate-Deskriptor. Dieser wird in cpm_02.c durch die Struktur GATE_DESKRIPTOR repräsentiert.

typedef struct
    {
    WORD    offset_0_15;
    WORD    selektor;
    BYTE    reserved;
    BYTE    type;
    WORD    offset_16_31;
    } GATE_DESKRIPTOR;

Da die einzelnen Komponeten der Struktur selbsterklärend sind bzw. bereits im Kapitel 2.4 Gates, Interrupts und Exceptions (IDT) näher betrachtet wurden, soll hier auf eine genauere Erklärung verzichtet werden.

Genau wie Deskriptoren der GDT werden Gate-Deskriptoren in einer Tabelle (d.h. einem Array) verwaltet. Diese Tabelle wird als IDT (Interrupt Deskriptor Table) bezeichnet. Für die Initialisierung der einzelnen Gate-Deskriptoren stellt das Beispielprogramm die Funktion gate_setattr zur Verfügung. Diese Funktion erwartet die folgenden Parameter.

    des       Zeiger auf eine GATE_DESKRIPTOR-Struktur in der IDT
    selektor  Selektor des entsprechenden Interrupt-Handlers
    off       Funktionszeiger auf den Interrupt-Handler
    type      Gate-Deskriptortyp

Für den Parameter type stehen die folgenden Konstanten zur Verfügung.

    TSS_80286             0x01
    LDT                   0x02
    TSS_80286_ACTIVE      0x03
    CALLGATE_80286        0x04
    TASKGATE              0x05
    INTGATE_80286         0x06
    TRAPGATE_80286        0x07
    TSS_80386             0x09
    TSS_80386_ACTIVE      0x0B
    CALLGATE_80386        0x0C
    INTGATE_80386         0x0E
    TRAPGATE_80386        0x0F

Durch die Nutzung der Funktion gate_setattr wird eine globale IDT mit gültigen Deskriptoren initialisiert. Auf diese Weise werden Exception-Handler für alle Exceptions eingerichtet. Jeder dieser Handler gibt nach seiner Aktivierung eine kurze Nachricht auf dem Bildschirm aus und kehrt anschließend zum DOS zurück.

Da das Beispielprogramm cpm_02.c die Interrupts nicht unterbindet, wird der Handler aktiv, der mit dem Interrupt 8 (Exception 8) verbunden ist. Der Grund dafür ist der PIT (Programmable Interval Timer), der standardmäßig 18,2 mal in der Sekunde aktiviert wird und dann den Interrupt 8 auslöst. Das ist auch der Grund, weshalb alle bisherigen Programme die Auslösung von Interrupts verhinderten oder den Interruptcontroller so umprogrammierten, daß der Interrupt 8 auf einen Handler zeigte, der nur per IRET zurückkehrt.

Probleme

Damit die hier dargestellte Programmierung fehlerfrei, d.h. in der Regel absturzfrei funktioniert, müssen folgende Regeln eingehalten werden:

C generierten Programmcode im Protected Mode ausführen

Die zweite und normalerweise praktikablere Möglichkeit, Protected Mode mit der Sprache C zu verbinden, ist es, das eigentliche Programm von der Initialisierung des Protected Mode und den damit zusammenhängenden Verwaltungsaufgaben zu trennen. Damit kann ein Programm in C geschrieben werden, das direkt im Protected Mode ausgeführt wird und sich nicht um den Protected Mode kümmern braucht.

Realisierung

Wie kann eine solche Trennung praktisch realisiert werden?

Die Programmausführung von C-Programmen startet immer mit der Funktion main. Doch der eigentliche Programmcode, der nach dem Programmstart aufgerufen wird, ist nicht die Funktion main, sondern der sogenannte "Startup-Code". In den meisten Fällen liegt dieser Startup-Code bereits compiliert bzw. assembliert in Form einer OBJ-Datei vor und wird zum eigentlichen C-Programmprojekt dazugelinkt. Beispielsweise stellen Realmode-C-Compiler auf diese Weise unterschiedliche Konfigurationen für verschiedene Speichermodelle zur Verfügung.

Wird dieser Startup-Code nun für die Initialisierung des Protected Mode verwendet, kann das C-Programm direkt im Protected Mode aufgerufen werden, indem der Startup-Code die main-Funktion, wie jede andere Funktion, aufruft. Der Startup-Code für das dritte Beispielprogramm c0pmode.asm nimmt genau diese Initialisierung vor und ruft die Funktion main auf. Nachdem die Funktion main zurückgekehrt ist, das C-Programm also beendet wurde, schaltet der Startup-Code in den Realmode und kehrt zu DOS zurück.

Die eigentliche Initialisierung des Protected Mode in c0pmode.asm unterscheidet sich nur unwesentlich von der Vorgehensweise anderer, bereits in vorherigen Kapiteln vorgestellten Programmen. Der größte Unterschied, der beachtet werden muß, ist die unterschiedliche Behandlung von konstanten bzw. uninitialisierten Daten durch die meisten C-Compiler. Diese Variablen werden in unterschiedlichen Segmenten abgelegt, deshalb muß auch der Startup-Code unterschiedliche Segmente für den Protected Mode vorbereiten, die außerdem auch im Segmentnamen übereinstimmen müssen.

Weiterhin muß auch der verwendete C-Compiler beachtet werden, denn nicht jeder Compiler ist gleichermaßen für das hier dargestellte Verfahren und besonders für den oben dargestellten Startup-Code geeignet. Der Startup-Code c0pmode.asm wurde für 32-Bit Compiler entwickelt. Er setzt voraus, daß die vom C-Compiler verwendeten Segmentnamen und Segmenttypen den in c0pmode.asm verwendeten entsprechen (z.B. trägt das Codesegment den Namen _TEXT und den USE32 Parameter). Der Startup-Code sowie das eigentliche C-Beispielprogramm cpm_03.c wurden mit dem Borland-C-Compiler der Version 4.0 (BC 4.0) erfolgreich getestet.

Daß das C-Programm jetzt keine direkte Manipulation des Protected Mode mehr durchführen muß, darf jedoch nicht darüber hinwegtäuschen, daß die grundsätzlichen Probleme der Protected Mode Nutzung weiterhin gelten. Dazu zählt in erster Linie die direkte bzw. indirekte Benutzung von DOS-Funktionen durch die Funktionen der C-Standardbibliothek! Deshalb benutzt das Beispielprogramm cpm_03.c auch die bereits in den beiden anderen Beispielprogrammen benutzte Funktion print zur direkten Textausgabe.

Index
weiter >>
<< zurück ||

Last change 27/11/2022 by Docker Rocker.
This page uses no cookies, no tracking - just HTML.
Author: "Docker Rocker" ~ 2022 · [Public Git]