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 :
argc :
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[] );
#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 */ }
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 :
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 startDa 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.
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 ?
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
push ebp mov ebp,esp push ebx push esi push edi
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
pop edi pop esi pop ebx pop ebp ret
.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 endDie beiden Quelltexte aus diesem Abschnitt werden mit folgendem Befehl in ein Programm umgewandelt :
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 endDieses 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 :
void mittel( int, char *[] );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 :
double add( double a, double b ) { return a+b; }
_add: push ebp mov ebp,esp fld qword ptr 8[ebp] fadd qword ptr 16[ebp] pop ebp retDer Rückgabewert befindet sich nun auf dem Stack des Fließkommaprozessors und kann von der aufrufenden Funktion ausgewertet werden.
struct irgendwas dummy( int, int ); ... var = dummy( 1, 2 );Der entsprechende Aufruf sieht in Assembler so aus :
push 2 ;zweites Argument push 1 ;erstes Argument push offset _var ;Zeiger auf Ziel-Struktur für Rückgabewert call _dummy add esp,12 ;Korrektur um 12 Bytes, weil drei Argumente !Durch das zusätzliche Argument verschieben sich alle "echten" Argumente um 4 Bytes nach hinten, d.h. die eigentlichen Argumente der Funktion befinden sich erst ab 12[EBP] :
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 startUm 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 :
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 :
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 :
.386p .model flat extrn DosPutStr:near .code start: push offset textstr call DosPutStr retf .data textstr db "Dies ist ein Beispieltext",13,10,0 endSonderfall : 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 :
mov ah,2 ;DOS-Funktion 2 mov dl,'T' ;Zeichen 'T' int 21h ;Zeichen auf STDOUT ausgebenDies 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.
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.
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 :
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 :
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdarg.h> #include <time.h> #include "trace.h" #define MAXTRACEFILES 10 struct _tracefile *trcblock; TRCHANDLE trcOpen( const char *datei ) { FILE *f; int i; f = fopen( datei, "wt" ); if( f != NULL ) for( i=0; i<MAXTRACEFILES; i++ ) if( trcblock[i].handle==NULL ) { trcblock[i].handle = f; strcpy( trcblock[i].name, datei ); return i; } if( f ) fclose( f ); return -1; } void trcLog( TRCHANDLE hdl, const char *fmt, ... ) { va_list ap; time_t t; struct tm *ts; va_start( ap, fmt ); time( &t ); ts = localtime( &t ); fprintf( trcblock[hdl].handle, "%02d.%02d.%04d %02d:%02d:%02d ", ts->tm_mday, ts->tm_mon + 1, ts->tm_year + 1900, ts->tm_hour, ts->tm_min, ts->tm_sec ); vfprintf( trcblock[hdl].handle, fmt, ap ); fputc( '\n', trcblock[hdl].handle ); va_end( ap ); } void trcClose( TRCHANDLE hdl ) { if( trcblock[hdl].handle ) fclose( trcblock[hdl].handle ); trcblock[hdl].handle=NULL; }Die Header-Datei TRACE.H enthält folgende Zeilen :
struct _tracefile { FILE *handle; char name[96]; }; typedef int TRCHANDLE; TRCHANDLE trcOpen( const char * ); void trcLog( TRCHANDLE, const char *, ... ); void trcClose( TRCHANDLE );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.
int DllInit( void ) { trcblock = calloc( MAXTRACEFILES, sizeof(struct _tracefile) ); if( trcblock==NULL ) { printf( "Init von TRACE.DLL fehlgeschlagen : Zuwenig Speicher\n" ); return 3; } return 0; }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.
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 :
void DllExit( int code ) { int i; for( i=0; i<MAXTRACEFILES; i++ ) if( trcblock[i].handle != NULL ) fclose( trcblock[i].handle ); free( trcblock ); }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 :
#include <dll.h>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 :
LIBRARY TRACE EXPORTS DllExit @1 DllInit @2 _trcClose @3 _trcLog @4 _trcOpen @5Die 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.
Um Ihnen die Anwendung dieser DLL zu demonstrieren, wird jetzt ein Programm gezeigt, das auf die Funktionen der eben erzeugten DLL zugreift.
#define _CRTLDLL #include <stdio.h> #include "trace.h" int main() { TRCHANDLE h; h=trcOpen( "TRACEBSP.TRC" ); if( h==-1 ) { printf( "Trace-Datei konnte nicht angelegt werden\n" ); return 1; } trcLog( h, "Das Handle dieser Log-Datei war %d", h ); trcClose( h ); return 0; }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.