Betriebssysteme · Institut für Systemarchitektur · Fakultät Informatik · TU Dresden

Windows 2000 Treiber (Teil 2)




2. Übung - Der erste "richtige" Treiber

Inhalt:

  • Schreiben einer Treiber-Start Routine
  • Registrieren eines Gerätes
  • Bereitstellen einiger wichtiger Treiber-Funktionen
  • Anpassen der Testapplikation aus der ersten Übung um den Treiber zu testen
  • Installation des Treibers

1. Schreiben einer Treiber-Start Routine:

Wie Sie aus der Vorlesung wissen, wird für jeden Treiber eine Datenstruktur im Kern verwaltet. Diese Datenstruktur wird angelegt, wenn der Treiber vom Kern geladen wird und muss von einer Treiber-Start Routine initialisiert werden. So müssen u.a. die Funktionen für verschiedene Aufrufe des Treibers - wie Lesen und Schreiben - registriert werden und es muss festgelegt werden aus welchem Memory-Pool der Treiber seinen Speicher bezieht. Damit der Kern weiss, welche Funktion er am Anfang aufrufen muss, hat diese einen speziellen Namen. Unter Windows heisst die Treiber-Start Routine "DriverEntry". Damit sie auch die richtigen Parameter in der richtigen Reihenfolge übergeben bekommt, muss sie einige Konventionen einhalten. Das Skelett dieser Funktion sieht wie folgt aus:

NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath)

Arbeiten, welche von der DriverEntry Routine durchgeführt werden, umfassen:

  • das Auffinden von Hardware für welche der Treiber zuständig ist
  • das Intialisieren der anderen Routinen, wie Read und Write, im Driver-Objekt
  • wenn ein Mehrgeräte-Controller unterstützt wird, muss ein Controller-Objekt angelegt werden
  • Device-Objekte werden für jedes unterstützte physische Gerät angelegt
  • die Geräte werden dem Win32 Subsystem über das Anlegen eines Links sichtbar gemacht
  • das Device-Objekt wird mit einem Interrupt-Objekt verbunden
  • eine Erfolgs- oder Fehlermeldung zurückliefern, ob die Initialisierung geklappt hat

2. Registrieren eines Gerätes:

Innerhalb der DriverEntry Funktion muss jetzt ein Gerät angelegt werden und ein symbolischer Name auf dieses Gerät erzeugt werden. Nutzen Sie zum Erzeugen des Gerätes die Funktion IoCreateDevice (finden Sie mit Hilfe der Hilfe ;) heraus, welche Parameter die Funktion benötigt und welche Werte Sie dort übergeben müssen.).

Himweis: Geben Sie als Device-Typ FILE_DEVICE_UNKNOWN, bei Device-Characteristics 0 und bei Device-Exclusive TRUE an.

Prüfen Sie den Erfolg des Aufrufen anhand des Rückgabe-Wertes.
Hinweis: Z.B. bei einem Aufruf wie:
status = IoCreateDevice(...);
prüft man den Erfolg mit dem Macro NT_SUCCESS(status). Das Ergebnis des Macro ist true wenn der Aufruf erfolgreich war.

Der Geräte-Name muss im Namensraum der Geräte angesiedelt sein. Dieser beginnt immer mit '\Device' Danach folgt ein Name, welcher das Gerät beschreibt gefolgt von einem Index (beginnend bei 0). (z.B. '\Device\Harddisk0'). Um das Testen der Treiber zu vereinfachen wählen Sie als Gerätenamen 'ABSDevice0'. Der volle Name lautet also '\Device\ABSDevice0'.
Hinweis: Beachten Sie, dass der Backslash (\) in Strings als Sonderzeichen gilt und speziell dargestellt werden muss.

Zum Registrieren des Gerätes im Namensraum für Win32 nutzen Sie die Funktion IoCreateSymbolicLink. (Benutzen Sie wieder die Hilfe um Information über Parameter herauszufinden.)
Hinweis:Der Namensraum für Win32 fängt mit '\??' an. Symolische Links tragen einen kürzeren Name als die meist umständlichen Gerätenamen und werden 1-indiziert. Wählen Sie hier den Namen 'ABS1', was den vollen Namen '\??\ABS1' ergibt.
Hinweis:Sowohl der Gerätename, als auch der Symbolische Name müssen als Unicode Strings definiert werden. D.h. sie müssen vom Type UNICODE_STRING sein. um Varibalen dieses Types von "normalen" String erzeugen zu können, nutzen Sie die Funktion RtlInitUnicodeString.

Wenn DriverEntry einen Fehlercode zurückliefert, entfernt der I/O Manager den Treiber wieder vom System. Geschieht dies, sollten natürlich auch die regestrierten Geräte und Namen entfernt werden. Dies wird von einer Driver-Unload Routine durchgeführt, welche im Driver-Objekt im Feld DriverUnload registriert werden muss. Schreiben Sie eine solche Funktion und registrieren Sie diese. Die Driver-Unload Routine soll die Funktionen IoDeleteSymbolicLink und IoDeleteDevice benutzen.

Damit die Driver-Unload Routine zu dem Geräte-Objekt und dem symbolischen Namen kommt, kann die Device-Extension Struktur genutzt werden. Eine Device-Extension Struktur enthält "global" Informationen über ein Gerät, welche der Treiber-Schreiber zusätzlich zu den schon vorhandenen Informationen nutzen will. Die Struktur wird beim Erzeugen eines Gerätes mit anglegt - die Grösse der Extension wird als zweiter Parameter übergeben. Auf sie kann mittels des DriverExtension Feldes des Driver-Objektes zugegriffen werden. Nutzen für Ihren Treiber folgende Struktur:

typedef struct _DEVICE_EXTENSION {
	PDEVICE_OBJECT pDevice;
	UNICODE_STRING sDeviceName;	// internal name
	UNICODE_STRING sSymLinkName;	// external name
} DEVICE_EXTENSION;

Die Werte der Struktur müssen beim Laden des Treibers gefüllt werden, damit die Driver-Unload Routine sie herausfinden kann.
Hinweis: Bekommen Sie bei Übersetzen des Treibers Fehler, welche den Fehlercode "LNK2001 unresolved Symbol" haben, überprüfen Sie ob Sie die Header-Datei "NTDDK.h" auch als extern "C" includen:

extern "C" {
	#include <ntddk.h>
}
Gleiches gilt für eine Fehlermeldung "error LNK2001: unresolved external symbol _DriverEntry@8" . Um diesen Fehler zu beheben schreiben Sie vor NTSTATUS DriverEntry(...) 'extern "C"'.
extern "C"
NTSTATUS DriverEntry(...)

zu 2. Testen / Installieren:

Um den Treiber zu testen, kopieren Sie die erhaltene .sys Datei in das Verzeichnis C:\Winnt\System32\drivers und benennen Sie sie abstreiber.sys (oder achten Sie auf den Dateinamen in den folgenden Schritten). Rufen Sie regedit auf und suchen Sie den Pfad HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services. Erzeugen Sie darin ein Unterverzeichnis (Schlüssel) 'abstreiber' (Achtung Dateiname! ohne .sys). Innerhalb dieses Unterverzeichnisses legen Sie drei DWORD Werte an:

ErrorControl=1
Start=3
Type=1
Sie können auch noch einen String Wert anlegen, welcher den in der Management Console angezeigten Namen enthält (DisplayName="ABSTreiber"). Haben Sie das Unterverzeichnis neu hinzugefügt, müssen Sie den Computer neu booten. Gab es den Eintrag schon, reicht ein Austauschen der .sys Datei.

Da der Start-Wert auf 3 gesetzt wurde, wird der Treiber nur bei Bedarf geladen. Wir können das manuell veranlassen indem wir in einer Konsole "net start abstreiber" eingeben. Die Funktion des Treiber können wir jetzt mit dem WinObj Tool von Sysinternals kontrollieren: Das Objekt '\Device\ABSDriver0' und der Symbolic Link '\??\ABS1' sollten jetzt zu sehen sein. Der Teiber kann mit dem Befehl "net stop abstreiber" wieder entfernt werden.

3. Ein kleiner Loopback-Treiber

Um das angelegte Gerät nutzen zu können, muss es möglich sein, ein Handle auf dieses Gerät zu bekommen und wieder frei zu geben. Dies wird über die Win32-Funktionen CreateFile und CloseHandle realisiert. Zudem sollte es möglich sein, Daten zum Gerät zu senden und vom Gerät zu beziehen (ReadFile und WriteFile). Diese Funktionen der Win32 API werden auf sogenannte Dispatch Routinen des Treibers abgebildet, welche dieser auch implementieren muss. Diese Funktionen werden in der DriverEntry Routine im Device-Objekt registriert. Die Create Methode z.B. so:

pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
Der Unterschied zur Driver-Unload Routine ist, dass hier ein Array von Funktionen verwendet wird. Diese Dispatch Routinen sehen alle gleich aus. z.B. die Create Methode:
static NTSTATUS DispatchCreate (IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp);
Wie man sieht, bekommt jede Funktion einen Zeiger auf das Geräte-Objekt und das aktuelle IRP. Um eine Anfrage abzuhandeln, müssen im IRP nach Abhandlung einige Werte gesetzt und der IRP abgeschlossen werden. Welche das sind, sollen Sie herausfinden (siehe Hilfe zu IRP-Struktur). Zum Abschliessen eines IRP benutzen Sie die Funktion IoCompleteRequest.

Die Device-Extension, wie folgt erweitert wird sie:

typedef struct _DEVICE_EXTENSION {
	PDEVICE_OBJECT pDevice;
	UNICODE_STRING sDeviceName;	// internal name
	UNICODE_STRING sSymLinkName;	// external name
	// Reserve space for pointer to loopback buffer
	PVOID pDeviceBuffer;
	ULONG nDeviceBufferSize;
} DEVICE_EXTENSION;

Die einzelnen Dispatch-Routinen sollen folgende Funktionen implementieren:

  • Create: nichts
  • Close: eventuell allokierten Puffer freigeben
  • Write:
    • eventuell allokierten Puffer freigeben
    • neuen Puffer in Grösse der Anfrage allokieren
    • Nutzer-Daten in den Puffer kopieren
  • Read: aus dem Puffer die angefragte Menge zum Nutzer kopieren
Hinweis: Auch wenn die Create Methode keine Funktionen implementieren soll, heisst das nicht, dass in ihr nichts stehen soll. Es muss noch immer das IRP behandelt werden.

Um diese Funktionen realisieren zu können, nutzen Sie zum Puffer-Management die Funktionen: ExAllocatePool, ExFreePool und RtlCopyMemory. Speicher für den internen Puffer soll vom PagedPool kommen. Wie Sie an die Grösse einer Anfrage (wieviele Bytes übertragen werden) herankommen, entnehmen Sie der Hilfe (Hinweis: IO_STACK_LOCATION und IRP ist ein guter Anfang).

4. Anpassen der Testapplikation

Passen Sie die Testapplikation der letzten Übung so an, dass Sie das Gerät ABS1 öffnet, Daten auf das Gerät schreibt und von ihm liest.

Anmerkungen:

Ich bin gebeten wurden Euch auf die Dokumentation des DDK hinzuweisen (wo dann die Sachen wie IoCreateDevice beschrieben sind). Sie ist im IRZ zu finden unter:
Programme -> Development Kits -> Windows 2000 DDK -> DDK Documentation.


webmaster@os, home
25. Jun 2004
· Copyright © 2001-2022 Operating Systems Group, TU Dresden | Impressum ·