2. Entwicklung von Applikationen

Im Verlauf dieses Kapitels werden einige Beispielprogramme in C und Assembler vorgestellt, um Ihnen einen Einblick in das Konzept der Applikationsentwicklung mit dem DiceRTE-System zu geben. In Abschnitt 1 werden die Grundlagen zur Entwicklung von C-Programmen beschrieben, während sich die Abschnitte 2, 3, 4 und 5 mit der Entwicklung von Programmen in Assembler und C befassen. Im 6. Abschnitt dieses Kapitels werden schließlich alle zum Linken einer Applikation benötigten Bibliotheksdateien und Objektmodule kurz vorgestellt. Der 7. Abschnitt dieses Kapitels beschäftigt sich mit der Entwicklung von DLLs für das DiceRTE-System. Alle hier gezeigten Beispielprogramme finden Sie auch im Unterverzeichnis SAMPLES\DOC nach der Installation des Systems.

2.1. Richtlinien für C-Programme

Ein C-Programm beginnt seine Ausführung mit der ersten Anweisung in der Funktion main und endet normalerweise nach Ausführung der letzten Anweisung in main. Bei Verwendung dieser Funktion sind folgende Punkte zu beachten :
 

Die Funktion main bekommt drei Argumente übergeben :

argc :

argc ist eine Integer-Variable und gibt die Anzahl der an main übergebenen Kommandozeilenargumente, einschließlich des Namens der ausführbaren Datei selbst, an.
 
argv : argv ist ein Array aus Zeigern auf Strings ( => char *argv[] ). Dabei gilt folgende Reihenfolge :
argv[0] = Name der ausführbaren Datei
argv[1] = erstes Argument aus Kommandozeile
argv[2] = zweites Argument aus Kommandozeile
argv[argc-1] = letztes Argument aus Kommandozeile
argv[argc] = NULL
Die Argumente in der Kommandozeile werden durch Leerzeichen separiert, es sei denn, sie wurden in doppelte Anführungszeichen eingeschlossen (dann zählt der String als ein  Argument).
 
env : env ist wie argv ein Array aus Zeigern auf Strings ( => char *env[] ). Jedes Element von env steht für eine Variable aus dem Umgebungsspeicherbereich in der Form <ENV=Wert> :
ENV : Name einer Umgebungsvariable, z.B. PATH oder COMSPEC
Wert : die der Umgebungsvariable zugewiesene Zeichenkette
Das Ende des env-Arrays wird durch einen NULL-Pointer angezeigt.
 
Bei der Deklaration der Argumente zur main-Funktion muß die Reihenfolge beachtet werden. Für main existieren diese Prototypen :
int main( );
int main( int argc ); /* ??? argc macht nur zusammen mit argv Sinn */
int main( int argc, char *argv[] );
int main( int argc, char *argv[], char *env[] );
Der von main zurückgegebene Integer-Wert ist der abschließende Statuscode des Programms, der ans Betriebssystem übergeben wird. Wird zum Beenden des Programms die Fuktion exit benutzt, stellt deren Argument den abschließenden Statuscode dar, z.B. exit(1) => Statuscode 1. Es folgt das Beispielprogramm BSP1.C, welches die Verwendung der main-Funktion und deren Argumente demonstrieren soll :
#include <stdio.h>

int main( int argc, char *argv[], char *env[] )
{
  int i;

  printf( "Anzahl Argumente : %d\n", argc );
  for( i=0; i<argc; i++ )
    printf( "Argument %d : %s>\n", i, argv[i] );

  for( i=0; env[i]!=NULL; i++ )
    printf( "Env-Variable %d : %s>\n", i, env[i] );

  return 0; /* Statuscode 0 zurückgeben => alles ok */
}

Compilieren Sie nun dieses Beispielprogramm mit dem C-Compiler DCC32 : Durch diesen Aufruf wird der Quelltext aus BSP1.C compiliert und die ausführbare Programmdatei BSP1.EXE erzeugt. Der Compiler ruft dazu eigenständig den Linker auf, um die entsprechende Programmdatei zu erzeugen.

Die zum Zugriff auf die C-Laufzeitfunktionen benötigten Header-Dateien befinden sich im Unterverzeichnis INCLUDE des Basisverzeichnisses von DiceRTE. Der Compiler wird bei der Installation so eingerichtet, daß er automatisch nach Header-Dateien in diesem Verzeichnis sucht. Nun folgt eine kurze Beschreibung der einzelnen Headerdateien :
 
_DEFS.H definiert die Aufrufkonventionen für Funktionen und diverse Platzhalter-Datentypen
ASSERT.H definiert das Assert-Makro zur Fehlersuche
CONIO.H deklariert einige Betriebssystem-abhängige Ein-/Ausgabefunktionen
CTYPE.H definiert Makros zur Klassifierung und Konvertierung von Zeichen
DICERTE.H deklariert Strukturen und Funktionen des DiceRTE-System-APIs
DLL.H deklariert Einstellungen und Prototypen zur Erzeugung einer DLL
DOS.H deklariert DOS-spezifische Strukturen und Funktionen
ERRNO.H definiert konstante Werte für Fehlerbeschreibungen der Standard-Funktionen
FCNTL.H definiert symbolische Konstanten für die Bibliotheksfunktion open
FLOAT.H definiert Makros und Variablen für Fließkomma-Funktionen
FMATH.H deklariert Funktionen und Makros zum Rechnen mit Festkomma-Werten
IO.H deklariert Funktionen und Makros für die niedrigsten Ein-/Ausgabefunktionen
LIMITS.H enthält Beschränkungen für Datentypen
MATH.H deklariert Prototypen für mathematische Funktionen
SHARE.H definiert Konstanten zum Zugriff auf Dateien
STDARG.H definiert Makros zum Zugriff auf (variable) Argumentlisten, z.B. bei vprintf, vfscanf etc.
STDDEF.H definiert allgemeine Datentypen und Makros
STDIO.H enthält Prototypen für Standard-Ein-/Ausgabefunktionen und die Deklaration der Standardstreams
STDLIB.H enthält Prototypen für allgemeine Funktionen, z.B. Konvertier- oder Sortierfunktionen
STRING.H deklariert Funktionen zur String- und Speicherbearbeitung
TIME.H deklariert Strukturen und Funktionen zur Zeitverwaltung
SYS\STAT.H enthält symbolische Konstanten für das Öffnen und Erzeugen von Dateien
SYS\TYPES.H zusätzliche Deklarationen zur Zeitverwaltung

Falls Sie vorhaben, ausschließlich C-Programme zu schreiben, ist dies auch schon alles, was es zu beachten gilt. Sie müssen nur den Compiler aufrufen und ihm die Namen der zu kompilierenden Programmodule übergeben, um ein ausführbares Programm zu erhalten. Weiterführende Informationen zur Verwendung des Compilers und des Linkers erhalten Sie bei der Dokumentation von DCC32 und DLINK32. Eine Beschreibung aller C-Laufzeitfunktionen finden Sie im Anhang B.
Die restlichen Abschnitte dieses Kapitels beschreiben die einzelnen Schritte zur Entwicklung von Applikationen in Assembler und C und enthalten unentbehrliches Wissen für Programmierer, die ihre Programme in Assembler entwickeln wollen, und solche, die tiefer in die Materie einsteigen wollen. Um daher den vollen Nutzen aus den folgenden Abschnitten zu ziehen, sollten Sie sich unbedingt in der Assembler-Sprache der x86-Prozessoren auskennen. Kenntnisse über die Funktionsweise des Protected Mode sind nicht unbedingt erforderlich.
 

2.2. Start und Ende einer Applikation

Die Ausführung einer Applikation wird durch den Programmlader des Kernels vorbereitet, der neben der eigentlichen Anwendung alle zur Ausführung benötigten Module (DLLs) einlädt. Die Applikation bekommt beim Start zwei Parameter in Registern übergeben :

Das Ende der Applikation wird durch die Ausführung eines 'RETF'-Befehls ausgelöst. Dadurch wird die Kontrolle an den Programmlader zurückgegeben, der nun die Applikation aus dem Speicher entfernt. Der Wert im AL-Register wird als Rückgbewert an das aufrufende Programm weitergereicht.

Hier ist das Beispielprogramm BSP2.ASM, das die beiden Parameter in EAX und EDX ausgibt : 

.386p
.model flat
extrn DosPutStr:near
extrn CrLf:near
.code
start:  mov esi,edx    ;Zeiger auf Kommandozeile merken
        push eax
        call DosPutStr ;Dateinamen ausgeben
        call CrLf      ;Zeilenvorschub

        push esi       ;Kommandozeile zurückholen
        call DosPutStr ;Kommandozeile ausgeben

        call CrLf      ;Zeilenvorschub

        retf           ;Programm beenden

end start
Da wir uns im Flat-Speichermodell befinden, besteht das gesamte Programm (egal wie groß es ist) nur aus zwei Segmenten : einem Codesegment und einem Datensegment. Sie müssen sich also keine Gedanken über die effektive Größe Ihres Programms machen und Code bzw. Daten auf mehrere Segmente verteilen, wie dies bei der Programmierung eines 16Bit-Systems (wie z.B. MS-DOS oder Windows 3.x) zwingend erforderlich ist. Im Endeffekt kann (und sollte) daher auch grundsätzlich auf die Verwendung der Segmentregister verzichtet werden. Da es also grundsätzlich nur ein Codesegment gibt, wurden die beiden Funktionen DosPutStr und CrLf in dem Beispiel als "NEAR"-Typen deklariert.
Um aus dem Beispiellisting eine Programmdatei zu erzeugen, sind folgende Befehle auszuführen : Die beiden doch recht langen Programmaufrufe können auch abgekürzt werden : Die jeweiligen Dateierweiterungen können weggelassen werden, solange diese den Standards entsprechen. DASM32 sucht z.B. immer nach Dateien mit der Erweiterung '.ASM' und DLINK32 immer nach Objektmodulen mit der Erweiterung '.OBJ' und nach Bibliotheken mit '.LIB' als Erweiterung. Die erzeugte Programmdatei bekommt, falls nicht angegeben, den Namen des ersten Objektmoduls und als Erweiterung automatisch '.EXE'. Weitere Informationen über die verwendeten Dateierweiterungen erhalten Sie bei der Dokumentation des jeweiligen Programms.

2.3. Aufruf von Assembler-Funktionen in C

Im allgemeinen werden vollständige Programme kaum noch komplett in Assembler entwickelt. In den meisten Fällen wird das Hauptprogramm in einer höheren Sprache wie etwa C geschrieben und dieses um optimierte Assemblerprozeduren für zeitkritische Aufgaben erweitert. Dabei entsteht das Problem, wie Funktionen in diesen beiden unterschiedlichen Sprachen miteinander verbunden werden können, was das Thema dieses und des folgenden Abschnitts ist (in Abschnitt 4 wird der Aufruf von C-Funktionen in Assembler behandelt).

Am besten läßt sich das Problem anhand eines Beispiels darstellen (BSP3.C) : 

#include <stdio.h>
int zaehlen( char * );  /* Prototyp der Assembler-Funktion */
int gross;              /* Variable für Anzahl Großbuchstaben */
int klein;              /* Variable für Anzahl Kleinbuchstaben */
char buf[256];          /* Eingabepuffer für String */

int main( int argc, char *argv[] )
{
  printf( "Bitte String eingeben : " );
  fgets( buf, 255, stdin );
  printf( "Anzahl Zeichen : %d\n", zaehlen(buf) );
  printf( "Anzahl Großbuchstaben : %d\n", gross );
  printf( "Anzahl Kleinbuchstaben : %d\n", klein );
  return 0;
}
Dieses C-Programm fordert den Anwender auf, einen String einzugeben und ruft danach die Funktion zaehlen auf, die als direkten Rückgabewert die Anzahl der Zeichen im String liefert und die Variablen gross und klein mit der Anzahl der Groß- und Kleinbuchstaben im String füllt. Um die Funktion zaehlen in Assembler zu implementieren, muß man mehrere Dinge wissen :

1. Welche Namen tragen Funktionen und Variablen in C ?

Der C-Compiler setzt automatisch einen Unterstrich vor den Namen jeder Variablen. Bei Funktionen ist dies nur bei Verwendung der Aufrufkonvention __cdecl der Fall, welche per Voreinstellung immer benutzt wird (im folgenden gehen wir immer davon aus, daß __cdecl benutzt wird). Die entsprechende Assemblerfunktion trägt damit den Namen _zaehlen und greift auf die im C-Modul definierten Variablen _gross und _klein zu.
 
2. Wie werden Argumente an aufzurufende Funktionen übergeben ? In C geschieht die Übergabe von Argumenten über den Stack per Voreinstellung von rechts nach links. Als Beispiel kann man einen Aufruf der Funktion strncmp heranziehen : strncmp(string1,string2,len);. Zum Zeitpunkt des Aufrufes sieht der Stack so aus :

Adresse    Stackpointer     Wert
...
100         -->  Rücksprungadresse
104              string1
108              string2
112              len
...

Daraus kann entnommen werden, daß der Compiler die Argumente in der umgekehrten Reihenfolge auf den Stack gebracht hat, da dieser von den hohen zu den niedrigen Adressen wächst. Die entsprechende Befehlssequenz sieht in Assembler so aus :

push len
push string2
push string1
call _strncmp
add esp,12
In C korrigiert der Aufrufer selbständig den Stackpointer ESP nach Ausführung der Funktion, um die Argumente vom Stack zu entfernen.

 

3. Welche Sicherheitsvorkehrungen muß die aufgerufene Funktion treffen ? Die aufgerufene Funktion ist dafür verantwortlich, vier Register mit ihren vorherigen Werten zurückzuliefern : EBP, EBX, ESI und EDI. Die erste Aktion einer aufgerufenen Funktion sollte es daher sein, diejenigen Register, welche innerhalb der Funktion verwendet werden, auf dem Stack zu sichern und am Ende wieder zurückzulesen.

 

4. Wie werden die übergebenen Argumente angesprochen ? Zum Zugriff auf die übergebenen Argumente verwenden C-Funktionen das Register EBP, das nach Sicherung des ursprünglichen Inhalts auf dem Stack den aktuellen Inhalt des Stackpointers ESP zugewiesen bekommt. Wir werden uns ebenfalls an diese Konvention halten und benutzen daher für Funktionen folgenden "Intro"-Code :
push ebp
mov ebp,esp
push ebx
push esi
push edi
Die Sicherung der Register EBX, ESI und EDI ist natürlich nur nötig, wenn sie auch verwendet werden. Nach Ausführung dieser Befehlssequenz sieht der Stack so aus :

Adresse    Stackpointer     Wert
...
84         -->    EDI
88                ESI
92                EBX
96                EBP
100               Rücksprungadresse
104               string1
108               string2
112               len
...
 

Nachdem das EBP-Register auf dem Stack gesichert wurde, wurde ihm der Wert 96 (zu dem Zeitpunkt Inhalt von ESP) zugewiesen. Da die Argumente bei Adresse 104 beginnen, muß also der Wert 8 addiert werden, um das erste Argument anzusprechen, 12 für das zweite, 16 für das dritte usw. Der Zugriff auf die Argumente kann also so aussehen :

mov esi,8[ebp]  ;string1 holen
mov edi,12[ebp] ;string2 holen
mov ecx,16[ebp] ;len holen
5. Wie sehen Rückgabewerte in Assembler aus ? Im C-Programm wurde die Funktion zaehlen mit dem Rückgabetyp int deklariert. Der C-Compiler erwartet Rückgabewerte grundsätzlich im EAX-Register (es gibt allerdings zwei Ausnahmen).
 
6. Wie wird die Funktion beendet ? Nachdem die am Beginn gesicherten Register zurückgeholt wurden, kann die Funktion über einen Near-Return beendet werden :
pop edi
pop esi
pop ebx
pop ebp
ret
Nachdem alle wichtigen Dinge geklärt sind, folgt nun die Funktion zaehlen für das obige C-Programm (BSP3-A.ASM) :
.386p
.model flat
public _zaehlen         ;Zugriff für C-Modul
extrn _gross:dword      ;Zugriff auf 'gross' in C-Modul
extrn _klein:dword      ;Zugriff auf 'klein' in C-Modul
.code
_zaehlen:
        push ebp        ;Register für Aufrufer sichern
        mov ebp,esp     ;EBP = Zugriff auf Argumente über Offset 8
        push ebx
        push esi
        mov esi,8[ebp]  ;übergebenen Zeiger auf String nach ESI holen
        xor eax,eax     ;Zähler für Zeichen setzen
        xor ecx,ecx     ;Zähler für Großbuchstaben setzen
        xor edx,edx     ;Zähler für Kleinbuchstaben setzen
@zaehl: mov bl,[esi]    ;Zeichen aus String holen
        inc esi         ;auf nächstes Zeichen setzen 
        or bl,bl
        jz @ende        ;Ende des Strings erreicht
        inc eax         ;Anzahl Zeichen + 1
        cmp bl,'A'
        jb @zaehl
        cmp bl,'Z'
        ja @klein
        inc ecx         ;'A'-'Z' : Anzahl Großbuchstaben + 1 
        jmp @zaehl
@klein:
        cmp bl,'a'
        jb @zaehl
        cmp bl,'z'
        ja @zaehl
        inc edx         ;'a'-'z' : Anzahl Kleinbuchstaben + 1 
        jmp @zaehl
@ende:
        mov _gross,ecx  ;Anzahl Großbuchstaben in C-Variable 
        mov _klein,edx  ;Anzahl Kleinbuchstaben in C-Variable 
                        ;(Anzahl Zeichen in EAX zurückgeben) 
        pop esi		;Register für Aufrufer zurückholen 
        pop ebx
        pop ebp
        ret             ;Funktion beenden
end
Die beiden Quelltexte aus diesem Abschnitt werden mit folgendem Befehl in ein Programm umgewandelt : 2.4. Aufruf von C-Funktionen in Assembler

Nachdem in Abschnitt 3 der Aufruf von Assembler-Funktionen in C beschrieben wurde, folgt nun der umgekehrte Weg. Prinzipiell basiert dieser Mechanismus ebenfalls auf den in Abschnitt 3 vorgestellten Konventionen, weshalb hier nicht alle Details der beiden folgenden Module erklärt werden. Das Programm dieses Abschnitts soll alle übergebenen Argumente als Integer-Werte interpretieren, diese addieren und daraus den Mittelwert berechnen und ausgeben. Hier zunächst das Hauptmodul BSP4.ASM :

.386p
.model flat
public _main
public _ergebnis
extrn _mittel:near
extrn _printf:near
.code
_main:
        push ebp
        mov ebp,esp
        push ebx
        push esi
        push edi
        mov eax,8[ebp]  ;argc in EAX
        mov ebx,12[ebp] ;argv in EBX
        push ebx
        push eax
        call _mittel    ;C-Funktion ausführen
        add esp,8
        mov eax,offset txt
        mov ebx,_ergebnis
        push ebx
        push eax
        call _printf    ;Ergebnis mit printf ausgeben
        add esp,8
        xor eax,eax     ;Rückgabewert 0
        pop edi
        pop esi
        pop ebx
        pop ebp
        ret             ;main beenden
.data
txt       db "Ergebnis : %d",10,0
_ergebnis dd 0
end
Dieses Modul enthält eine Implementation der main-Funktion wie man sie von C her kennt. Da dieses Modul auf C-Funktionen zugreift, muß das C-Startmodul CX.OBJ hinzugelinkt werden, welches wichtige Initialisierungen für die C-Laufzeitbibliothek und Vorbereitungen zur Ausführung von C-Funktionen vornimmt. Ferner definiert jenes Startmodul einen Programmeinstiegspunkt, weshalb das obige Modul keinen solchen hinter der abschließenden END-Direktive definieren darf.

In der main-Fuktion werden zwei C-Funktionen aufgerufen :  mittel und printf. Während printf eine Funktion aus der C-Laufzeitbibliothek ist, wurde mittel selbst definiert. Als Argumente bekommt mittel die Argumente argc und argv der main-Funktion übergeben, die diese wiederum vom C-Startcode bekam. Die mittel-Funktion stellt den Mittelwert der übergebenen Integer-Argumente in der oben definierten Variable ergebnis bereit. Ein eventueller Rückgabewert wird nicht beachtet. Der Prototyp von mittel sieht demnach also so aus :

Hier ist der Code der C-Funktion mittel (BSP4-C.C) :
#include <stdlib.h>
extern int ergebnis;    /* Zugriff auf _ergebnis */

void mittel( int anz, char *wert[] )
{
  int i;

  for( i=1; i<anz; i++ ) /* alle Werte hochzählen */
    ergebnis+=atoi( wert[i] );

  --anz;

  if( anz!=0 )  /* Division durch Null vermeiden */
    ergebnis/=anz;      /* Mittelwert berechnen */

}
Die Funktion mittel ermittelt die Summe aller Integer-Argumente und teilt diese durch deren Anzahl, um den Mittelwert zu ermitteln. Das Ergebnis steht in der Variablen ergebnis bereit, die im Assembler-Modul definiert wurde.

Die beiden Quelltexte werden mit der folgenden Anweisung in ein Programm umgewandelt :

Hier folgen noch einige wichtige Punkte, mit deren Hilfe dem Mischen von C- und Assembler-Funktionen nichts mehr im Wege stehen sollte :

2.5. Aufruf von Real Mode-Interrupts
 
Obwohl die Applikation im Protected Mode läuft, ist es weiterhin möglich, auf die Funktionen zuzugreifen, die durch das BIOS und DOS zur Verfügung gestellt werden. In diesem Abschnitt wird daher speziell der Aufruf von Real Mode-Interrupts beschrieben, welche die Schnittstellen zu vielen BIOS- und DOS-Funktionen darstellen. Der nötige Verwaltungsaufwand zum Aufruf solcher Interrupts wird komplett vom DOS-Extender übernommen, der zu diesem Zweck über das DPMI-Interface eine spezielle Funktion zur Verfügung stellt. Diese Funktion, die das Umschalten des Prozessormodus und die Ausführung des Interrupts erledigt, benötigt beim Aufruf eine Struktur, die alle nötigen Registerinhalte für die Real Mode-Interruptprozedur enthalten muß (eine genaue Erklärung dieser Funktion erhalten Sie in der offiziellen DPMI-Dokumentation; in diesem Abschnitt wird nur ihre Anwendung in der Praxis vorgestellt). Betrachten wir zunächst die Registerstruktur :
 
Offset Bytes Bedeutung
00 4 EDI-Register
04 4 ESI-Register
08 4 EBP-Register
12 4 -Reserviert-
16 4 EBX-Register
20 4 EDX-Register
24 4 ECX-Register
28 4 EAX-Register
32 2 Flags-Register
34 2 ES-Register
36 2 DS-Register
38 2 FS-Register
40 2 GS-Register
42 2 IP-Register
44 2 CS-Register
46 2 SP-Register
48 2 SS-Register

Zum Aufruf von Real Mode-Interrupts werden nur die unteren 16 Bits der 32-Bit-Register benötigt. Nach der Ausführung des Interrupts werden die Inhalte aller CPU-Register außer SS, SP, CS und IP wieder in diese Struktur eingetragen, so daß die Rückgabewerte der jeweiligen Interrupt-Funktion ausgewertet werden können.

Jetzt aber zu einem konkreten Beispiel : es soll ein Text mit Hilfe der Funktion 9 des DOS-Interrupts 21h ausgegeben werden. Hier ist zunächst das Programm BSP5.ASM :

.386p
.model flat
extrn LowAlloc:near
extrn MemFree:near
.code
start:
        ;1. Schritt : Vorbereitung des Strings:
        push 32                 ;32 Bytes belegen
        call LowAlloc           ;DOS-Block allokieren
        mov lowbuf,eax          ;Zeiger auf Block merken
        mov edi,eax             ;Zeiger auf Block in EDI
        mov esi,offset textstr  ;Zeiger auf String in ESI
        mov ecx,28              ;28 Zeichen
        rep movsb               ;String in DOS-Block kopieren

        ;2.Schritt : DPMI-Registerstruktur vorbereiten:
        mov edi,offset regstrct ;Adresse der Registerstruktur holen
        mov byte ptr 29[edi],9  ;AH=9 (Funktion #9)
        mov word ptr 32[edi],0  ;Flags löschen
        mov dword ptr 46[edi],0 ;SS+SP löschen
        mov eax,lowbuf          ;Umrechnung der Blockadresse in ...
        mov edx,eax             ;... Seg+Off für Real-Mode-Register
        shr eax,4
        mov word ptr 36[edi],ax ;DS=RealMode-Seg des Blocks mit Text
        and dx,15
        mov word ptr 20[edi],dx ;DX=RealMode-Off des Blocks mit Text

        ;3.Schritt : Interrupt ausführen:
        mov ax,300h             ;DPMI-Funktion 300h
        mov bl,21h              ;DOS-Interrupt 21h
        mov bh,0                ;keine Flags für Aufruf
        xor cx,cx               ;keine zusätzlichen Stackbytes
        int 31h                 ;Interrupt über DPMI-Host auslösen

        ;4.Schritt : Rückgabe des DOS-Blocks und Ende:
        push lowbuf
        call MemFree
        retf
.data
textstr  db "Dies ist ein Beispieltext",13,10,"$"

.data?
regstrct db 52 dup (?)
lowbuf   dd ?

end start
Um die gewünschte DOS-Funktion in einem 16Bit-Programm unter MS-DOS aufzurufen, müßten lediglich das AH-Register mit dem Wert 9 und das Registerpaar DS:DX mit einem Zeiger auf den auszugebenden String gefüllt und der Interrupt 21h mit 'int 21h' ausgeführt werden. Dies wären drei bzw.vier Zeilen Assemblercode. Da das Programm aber im 32Bit-Protected Mode abläuft, ist die Sache wesentlich komplexer. Das oben abgedruckte Programm, das den gleichen Zweck erfüllt, besteht aus insgesamt vier Schritten :
 

1.Schritt : Vorbereitung des Strings

Das erste Problem ist es, der DOS-Funktion den String in erreichbare Nähe zu bringen. Bekanntlich können im Real Mode nur die ersten 1 MB des Speichers (= konventioneller Speicher) adressiert werden, weshalb der String zunächst aus dem erweiterten Speicher (in dem sich der Programmtext mit seinen Daten befindet) in einen Block im unteren 1 MB-Bereich kopiert werden muß. Dazu wird zunächst mit der API-Funktion LowAlloc ein entsprechender Block belegt, der danach mit dem String gefüllt wird (die Funktion LowAlloc ist im Prinzip ein Aufruf der Funktion 48h des DOS-Interrupts 21h nach dem hier beschriebenen Prinzip). Wenn eine Real Mode-Interruptfunktion einen Zeiger auf irgendeinen Speicherblock erwartet, muß sich dieser generell im konventionellen Speicher befinden.
 

2.Schritt : DPMI-Registerstruktur vorbereiten

Bevor die DOS-Funktion über den DPMI-Host ausgeführt werden kann, muß die benötigte Registerstruktur gefüllt werden. Dazu werden folgende Register-Abbilder in der Struktur initialisiert :

Um die Register-Abbilder DS und DX in der Struktur zu füllen, muß die Adresse des allokierten Blocks in ein 16Bit-Segment und einen 16Bit-Offset umgerechnet werden.
 

3.Schritt : Interrupt ausführen

Da jetzt alle Vorbereitungen für den Aufruf getroffen wurden, kann die DOS-Funktion vom DPMI-Host ausgeführt werden. Dazu wird die Funktion 300h des DPMI-Hosts ('Simulate Real Mode Interrupt') benutzt, die insgesamt drei Parameter benötigt :

Ausgelöst wird der Aufruf schließlich durch Ausführung von 'int 31h'. Alle DPMI-Funktionen sind über den Interrupt 31h realisiert worden, wobei der Inhalt des AX-Registers die Nummer der auszuführenden DPMI-Funktion enthalten muß (in unserem Beispiel 300h).
 

4.Schritt : Block zurückgeben und beenden

Der in Schritt 1 belegte Speicherblock wird nun zurückgegeben und das Programm mit 'retf' beendet.
 
Aus dem Beispielprogramm kann mit den folgenden Befehlen ein ausführbares Programm erstellt werden :

Bevor Sie das oben aufgeführte Programm als Vorlage zum Aufruf von Real Mode-Interruptprozeduren verwenden, sollten Sie im System-API nachschauen, ob die gewünschte Prozedur dort bereits als Funktion vorhanden ist. Die Funktion DosPutStr erfüllt beispielsweise den gleichen Zweck wie das in diesem Abschnitt vorgestellte Programm. Dies könnte daher auch so aussehen :
.386p
.model flat
extrn DosPutStr:near
.code
start:  push offset textstr
        call DosPutStr
        retf

.data
textstr  db "Dies ist ein Beispieltext",13,10,0

end
Sonderfall : BIOS- und DOS-Funktionen ohne Segmentregister

Unter den unzähligen BIOS- und DOS-Funktionen befinden sich einige, die direkt aus einem 32Bit-Programm aufgerufen werden können. In solchen Fällen kann auf den komplizierten Umweg über die DPMI-Funktion 300h verzichtet werden, indem praktisch genau wie in einem 16Bit-Programm der jeweilige Interrupt durch Ausführung des 'int'-Befehls ausgelöst wird. Erkennbar sind solche Funktionen an ihren Parametern : wird kein Zeiger auf einen Speicherblock im konventionellen Speicher benötigt, kann diese Funktion auch direkt ausgeführt werden (dies ist NICHT generell der Fall !). Ein allgemeingültiges Beispiel dafür ist die Funktion 2 des DOS-Interrupts 21h, die ein Zeichen auf dem Standard-Ausgabegerät ausgibt :

Dies ist möglich, da alle Interruptvektoren in der IDT, für die keine expliziten Handler im Protected Mode installiert wurden, auf eine spezielle Funktion weisen, die beim Aufruf in den Real Mode schaltet, die Interrupt-Anforderung an den entsprechenden Real Mode-Handler weiterreicht und danach zurückschaltet (ein sogenannter "Redirector"). Alle Inhalte der CPU-Register werden dabei übernommen mit Ausnahme der Segmentregister, da deren Inhalte in den beiden Betriebsmodi unterschiedliche Bedeutungen haben. Die damit verbundene Umschaltung zwischen den Betriebsmodi bleibt für das ausführende Programm unsichtbar, doch trotzdem sollte diese Methode mit Vorsicht eingesetzt werden, denn nicht alle Interrupt-Funktionen sind auf diese Weise ausführbar, auch wenn sie keine Zeiger als Parameter benötigen. Im Einzelfall sollten Sie dies selbst überprüfen und diese Methode im Zweifelsfall vermeiden, denn mit der (aufwendigeren) DPMI-Funktion 300h ist man auf jeden Fall auf der sicheren Seite.

2.6. Objektmodule und Bibliotheken

Nachdem die einzelnen Quelltexte in Objektmodule umgewandelt wurden (durch Compilierung oder Assemblierung), werden diese zur Erzeugung einer Applikation an den Linker übergeben. Neben den erstellten Objektmodulen benötigt der Linker eine oder mehrere Bibliotheken und eventuell ein Startmodul, falls im Programm C-Funktionen benutzt werden.

In diesem Abschnitt werden die mit dem DiceRTE-System ausgelieferten Objektmodule und Bibliotheken beschrieben, die Sie zur Erstellung Ihrer eigenen Applikationen brauchen. Danach werden noch einmal die einzelnen Befehlsfolgen zum Erzeugen der in diesem Kapitel vorgestellten Beispielprogramme dargestellt.

1. CX.OBJ

Das Objektmodul CX.OBJ ist das Startmodul für C-Programme. Neben den Objektmodulen, die die eigentliche Applikation darstellen, muß außerdem dieses Startmodul an den Linker übergeben werden. Es definiert den Einstiegspunkt für die Ausführung der Applikation, initialisiert die Variablen für die statischen Laufzeitfunktionen aus CRTS.LIB, bereitet die Argumente für die ‘main’-Funktion vor und ruft schließlich diese Funktion auf. Nach der Rückkehr aus der ‘main’-Funktionen werden alle allokierten Speicherbereiche freigegeben, alle noch geöffneten Dateien geschlossen und die Applikation wird beendet. Dieses Objektmodul muß immer zusammen mit CRTS.LIB gelinkt werden. Den Quelltext für das Modul CX.OBJ finden Sie im Unterverzeichnis 'LIB\STARTUP'. 2. CRTS.LIB Die Bibliothek CRTS.LIB enthält die C-Laufzeitfunktionen in statischer Form. Jede Applikation, die eine C-Laufzeitfunktion aufruft, benötigt diese Bibliothek (bzw. CRTD.LIB) zum Linken. Da viele der Funktionen wiederum auf Funktionen des System-APIs beruhen, muß auch die Bibliothek SYS4G.LIB zum Linken verwendet werden. Im Gegensatz zum System-API werden die C-Laufzeitfunktionen durch diese Bibliothek "statisch" gelinkt, d.h. die Adressen und der Code der Funktionen selbst sind zum Zeitpunkt des Linkens bekannt und werden fest in die Applikation integriert. Im Gegensatz hierzu werden diese Funktionen durch CRTD.LIB wie die des System-APIs "dynamisch" gelinkt. 3. CXD.OBJ 4. CRTD.LIB 5. SYS4G.LIB Die Bibliothek SYS4G.LIB enthält Import-Definitionen zum Zugriff auf die Funktionen des System-APIs aus der dynamischen Bibliothek SYS4G.DLL. Jede Applikation, die auf die Funktionen des System-APIs zugreift, benötigt diese Importbibliothek beim Linken. 6. DX.OBJ Alle hier beschriebenen Module und Bibliotheken befinden sich im Unterverzeichnis LIB des DiceRTE Hauptverzeichnisses.

Das Ergebnis des Linkvorgangs ist eine Programmdatei im Portable-Executable-Format, die in dieser Form noch nicht direkt ausführbar ist. Um ein eigenständig lauffähiges Programm zu erzeugen, ist ein weiterer Schritt nötig, bei dem diese Programmdatei mit dem Kernel, dem Programmlader und dem DOS-Extender verbunden wird. Das Ergebnis dieses zweiten Linkvorgangs, der automatisch von DLINK32 vorgenommen wird, ist die endgültige Fassung des DiceRTE-Programms.

Die Programme in Beispiel 1, 3 und 4 wurden bisher mit der statischen C-Laufzeitbibliothek gelinkt. Zur Verwendung der dynamischen Bibliothek muß das Objektmodul CX durch CXD und die Bibliothek CRTS durch CRTD ersetzt werden. Außerdem muß bei der Compilierung der C-Quelldateien das Präprozessor-Makro '_CRTLDLL' definiert werden, damit in den Header-Dateien Anpassungen an die dynamische Laufzeitbibliothek vorgenommen werden. Für das Beispiel 4 würde der Aufruf des Compilers so aussehen :

Die Bedeutung der beiden Optionen '-W' und '-D' wird in der Dokumentation des Compilers beschrieben.

2.7. Erstellung von DLLs

Neben den zum DiceRTE-System mitgelieferten Bibliotheken können Sie natürlich auch Ihre eigenen erstellen, um so die Funktionalität des DiceRTE-Systems beliebig zu erweitern. Die Erstellung einer statischen Bibliothek ist relativ einfach : compilieren bzw. assemblieren Sie Ihre Quelltexte mit DCC32 oder DASM32 und lassen Sie die entstandenen Objektmodule von DLIB zu einer Bibliotheks-Datei zusammenfassen ... fertig ! Dahingegen ist die Erstellung einer dynamischen Bibliothek schon wesentlich komplizierter und ähnelt sehr der Erzeugung einer Applikation. Tatsächlich sind sich Applikationen und DLLs sehr ähnlich : beide werden vom Linker erzeugt und liegen damit in einem ausführbaren Format vor. Die wesentlichen Unterschiede liegen darin, daß eine DLL im Gegensatz zu einer Applikation nicht mehr mit BIND2EXE weiterverarbeitet werden muß und daß sie keinen eigenen Stack besitzt.
Eine DLL wird genau wie eine Applikation durch den Programmlader eingeladen und resident im Speicher installiert. Nach dem Einladen aller DLLs wird in jeder einzelnen DLL nach einer exportierten Funktion namens 'DllInit' gesucht und diese ausgeführt. In dieser Funktion können notwendige Initialisierungen ausgeführt werden, z.B. Allokation von Speicherblöcken, Öffnen von Dateien etc. Nach der Beendigung der Applikation wird in jeder DLL, die nicht mehr benötigt wird, nach der Funktion 'DllExit' gesucht und diese ausgeführt. Hier können nun die in 'DllInit' getätigten Initialisierungen rückgängig gemacht werden, z.B. Speicherblöcke freigeben, Dateien schließen etc. Beide Funktionen werden genau wie eine Applikation mit einem FAR-Call aufgerufen und müssen deshalb auch mit einem FAR-Return terminieren. Bei der Ausführung der 'DllInit'- und 'DllExit'-Funktionen werden Abhängigkeiten zwischen den DLLs automatisch berücksichtigt, d.h. die DLLs, welche die Basis für andere DLLs sind, werden zuerst initialisiert und zuletzt entfernt. Ein definierter Programm-Einstiegspunkt spielt bei einer DLL im Gegensatz zu einer Applikation keine Rolle und wird ignoriert.

Um Ihnen einen genaueren Einblick in den Prozeß der Entwicklung einer DLL zu geben, werden wir jetzt eine solche erzeugen. Sie soll Applikationen einige Funktionen zur Verfügung stellen, mit deren Hilfe sich Debug-Meldungen zur Laufzeit in eine Log-Datei schreiben lassen. Es sollen insgesamt drei Funktionen mit Namen 'trcOpen', 'trcLog' und 'trcClose' in der DLL enthalten sein. Hier ist zunächst der Code der drei Funktionen als C-Quelltext :

Die Header-Datei TRACE.H enthält folgende Zeilen : Die erste Funktion, 'trcOpen' soll eine neue Log-Datei öffnen und deren Handle sowie Dateinamen in einer Tabelle eintragen. Der Rückgabewert ist entweder der Index in die Tabelle ( = Erfolg) oder -1, falls die Datei nicht angelegt werden konnte oder die Tabelle voll ist. Die Funktion 'trcLog' schreibt mit Hilfe der C-Funktion vfprintf einen String in eine geöffnete Log-Datei, deren "Handle" ein Index in die Log-Tabelle ist, aus der der entsprechende Stream ermittelt wird. Die letzte Funktion, 'trcClose', schließt die Log-Datei und gibt den verwendeten Log-Tabellenplatz wieder frei.
Wie Sie aus dem Listing sicher bereits entnommen haben, ist der Tabelle noch kein Speicher zugeordnet. Dies ist eine typische Aktion für eine 'DllInit'-Funktion, die in diesem Fall so aussehen könnte : Da diese Funktion ausgeführt wird, bevor eine Applikation oder eine andere DLL die Möglichkeit haben, eine der drei 'trc...'-Funktionen auszuführen, ist die Tabelle somit korrekt initialisiert.
Der Rückgabewert der Funktion DllInit spielt eine wichtige Rolle. Sollte eine der Initialisierungen fehlschlagen (z.B. kein freier Speicher mehr), muß dies dem Programmlader mit dem Rückgabewert 3 mitgeteilt werden. Dadurch wird die Ausführung sofort abgebrochen und alle bereits installierten DLLs werden zusammen mit der Applikation wieder entfernt. Alle anderen Werte stehen für eine erfolgreiche Initialisierung der DLL.

Beim Entfernen der DLL aus dem Speicher sollten alle noch geöffneten Log-Dateien geschlossen und der Speicher für die Tabelle wieder ans System zurückgegeben werden. Hier ist die entsprechende 'DllExit'-Funktion :

Der Parameter 'code' enthält den Rückgabewert der DllInit-Funktion und könnte hier ausgewertet werden. Dazu besteht allerdings in diesem Fall kein Anlaß.

Ein wichtiges Element fehlt noch in dem Quelltext : in einer DLL ist es nicht möglich, die statische C-Laufzeitbibliothek (d.h. CRTS.LIB) zu verwenden, sondern ausschließlich die dynamische. Genau wie bei einer Applikation benötigt daher auch eine solche DLL ein Startmodul, das u.a. die entsprechenden Initialisierungen zum Zugriff auf die Funktionen der C-DLL vornimmt. In der ersten Zeile des Quelltextes muß deshalb die Header-Datei DLL.H eingeladen werden, die das Makro _CRTLDLL definiert und weitere Definitionen zur Erstellung einer DLL enthält. Die erste Zeile lautet also :

Damit ist der Quelltext für die DLL fertig. Bevor dieser aber erstellt werden kann, wird noch eine Moduldefinitionsdatei benötigt, die die Namen und Ordinalnummern der zu exportierenden Funktionen enthält. Ohne eine solche Moduldefinitionsdatei wäre keine der Funktionen von anderen Programmen sichtbar und die DLL damit nutzlos. Die Moduldefinitionsdatei für die TRACE.DLL sieht so aus : Die fünf Funktionen, die in der DLL enthalten sind, sind nun von jeder Applikation oder anderen DLL verwendbar. Auch die Funktionen 'DllInit' und 'DllExit' müssen explizit exportiert werden, da sie sonst vom Programmlader nicht gefunden werden können.
Die DLL kann nun mit der folgenden Anweisung erstellt werden : Durch die Option '-Wdd' wird DCC32 mitgeteilt, daß eine DLL erzeugt werden soll. Dadurch wird automatisch das Modul DX.OBJ als Startmodul verwendet und mit CRTD.LIB gelinkt. Dieses Modul dient dazu, den Zugriff auf die Funktionen der C-DLL zu ermöglichen und Schnittstellen für die Funktionen 'DllInit' und 'DllExit' zur Verfügung zu stellen. Durch den automatischen Aufruf von DLINK32 werden nun zwei Dateien erzeugt : TRACE.DLL und TRACE.LIB. Die Datei TRACE.DLL enthält die eigentliche DLL mit dem Funktionscode, der vom Programmlader beim Start eingeladen wird. Die Datei TRACE.LIB ist eine Importbibliothek, mit deren Hilfe DLINK32 bei Applikationen und anderen DLLs Import-Definitionen in die Programmdateien integrieren kann, anhand derer der Programmlader später die richtigen Funktionen aus der TRACE.DLL importieren kann.

Um Ihnen die Anwendung dieser DLL zu demonstrieren, wird jetzt ein Programm gezeigt, das auf die Funktionen der eben erzeugten DLL zugreift.

Dieser Quelltext lädt die Header-Datei TRACE.H ein, in der die Prototypen der drei Trace-Funktionen und der Datentyp TRCHANDLE deklariert wurden. Danach wird versucht, eine Log-Datei namens TRACEBSP.TRC mit 'trcOpen' zu öffnen. Schlug dieser Versuch fehl, wird das Programm mit einer Fehlermeldung beendet. Ansonsten wird eine Meldung in die Log-Datei mit 'trcLog' ausgegeben und diese danach mit 'trcClose' wieder geschlossen.
Durch das Makro _CRTLDLL wird dem Compiler mitgeteilt, daß die Applikation die Funktionen der C-DLL benutzen soll. Diese wird auch von der TRACE-DLL benutzt und muß deshalb ohnehin durch den Programmlader installiert werden. Die Verwendung der statischen C-Bibliothek in dieser Applikation wäre deshalb nur reine Speicherverschwendung. Die Applikation und die TRACE-DLL greifen dadurch gemeinsam auf die entsprechenden Funktionen der C-DLL zu.
Erstellen Sie nun hieraus eine Programmdatei : Die Datei TRACE.LIB muß bei dem Aufruf angegeben werden, damit DLINK32 die entsprechenden Import-Informationen der TRACE.DLL-Funktionen ermitteln und für den Progammlader in der Programmdatei hinterlegen kann.