Im folgenden sollen die theoretischen Grundlagen aus den vorhergehenden Kapiteln in die Praxis umgesetzt werden. An mehreren Beispielen wird die Programmierung von Kernelmode Treibern demonstriert. Es werden zwei Treiber vorgestellt, die den Zugriff auf Ports ermöglichen, und ein Treiber, der unter anderem das Eventlogging demonstriert. Zum Abschluß wird noch ein Treiber für eine spezielle Hardware, die Brunelco Timer Karte, erläutert.
5.1 Treiber für Portzugriffe
Auf der intel 80x86 Plattform findet ein Großteil der Kommunikation zwischen Hard- und Software über sogenannte Ports statt. Ein Port bezeichnet einen 8, 16 oder 32 Bit breiten Speicherbereich im I/O Adressraum des Intel-Prozessors. Die insgesamt 65536 zur Verfügung stehenden Ports werden mit speziellen Maschinenbefehlen gelesen (IN) und beschrieben (OUT). Ab dem i80386 unterstützt der Prozessor einen Mechanismus, mit dem man die Zugriffe auf Ports abhängig von der aktuellen Privilegstufe einschränken kann. Windows NT macht sich diesen Mechanismus zunutze und verweigert im Usermodus alle Portzugriffe. Aus Sicht der Stabilität von Windows NT ist dies sicher sehr vorteilhaft. Kein Programm, das im Usermodus abläuft, kann so auf wichtige Hardwarekomponenten wie Festplatten- oder DMA-Controller zugreifen und etwaiges Unheil anrichten. Für den Programmierer ist diese Restriktion jedoch manchmal recht schmerzhaft. Der Schritt von der Applikationsprogrammierung zur Treiberprogrammierung, die den Zugriff auf die Hardware ermöglichen würde, ist recht groß und anfangs meist auch mit hohen Kosten verbunden. Es wäre daher manchmal sehr hilfreich, wenn man mittels einer universellen Schnittstelle mit der Hardware kommunizieren könnte oder die Restriktionen von Windows NT teilweise aufheben könnte.
Im folgenden sollen zwei Kernelmodetreiber vorgestellt werden, die einen universellen Zugriff auf Hardwarekomponenten ermöglichen.
5.1.1 Universal Porttreiber
Unter den Beispielen des Windows NT DDK findet man nach einiger Suche das Verzeichnis PortIO (unter ddk\src\general). In der Readme-Datei wird beschrieben, daß es sich hierbei um ein Beispiel für einen generischen I/O Port Treiber handelt. Es werden 8, 16 und 32 Bit Zugriffe auf einen eingeschränkten Bereich des I/O Adreßraums ermöglicht, der vorher über Einträge in der Registry definiert werden muß. Die Größe und die Position des Bereiches lassen sich dadurch nicht dynamisch anpassen.
Der vom Autor entwickelte Universal Porttreiber verwendet eine ähnliche Herangehensweise, bietet aber mehr Flexibilität und ist auf das absolut Notwendigste reduziert. Es wird der Zugriff auf den gesamten I/O Adreßraum gewährt, auf eine Überprüfung auf eventuell belegte Ressourcen wird dabei verzichtet. Da der vollständige Adreßraum angesprochen werden kann, ist es auch nicht möglich, dem Betriebssystem mitzuteilen, welche Ressourcen verwendet werden. Der Treiber läßt sich dynamisch laden und entladen. Der Zugriff auf die Ports erfolgt im Treiber selbst über die Funktionen READ_PORT_XXX und WRITE_PORT_XXX des DDK. In einem Usermode-Programm wird über die Funktion DeviceIOControl mit dem Treiber kommuniziert.
Die DriverEntry Funktion ist für die Initialisierung des Treibers verantwortlich. Da der Treiber weder Interrupts, DMAs, noch andere Ressourcen belegt, wird hier nur das Geräteobjekt erzeugt und ein symbolischer Link für den Zugriff von Win32 Applikationen eingerichtet. Weiterhin werden die Adressen der Dispatch Funktionen im Treiberobjekt eingetragen.
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { PDEVICE_OBJECT DeviceObject; NTSTATUS mStatus; WCHAR Name[]=L"\\Device\\UniPort"; WCHAR DOSName[]=L"\\DosDevices\\UniPort"; UNICODE_STRING uniName, uniDOSName; // Eintragen der Einsprungadressen in das Treiberobjekt DriverObject->MajorFunction[IRP_MJ_CREATE]=UniPort_DispatchCreateClose; DriverObject->MajorFunction[IRP_MJ_CLOSE]=UniPort_DispatchCreateClose; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=UniPort_DispatchDeviceControl; DriverObject->DriverUnload=UniPort_Unload; // wir benoetigen Unicode Strings, daher Umwandlung ANSI - Unicode RtlInitUnicodeString(&uniName, Name); RtlInitUnicodeString(&uniDOSName, DOSName); // das Geraeteobjekt wird erzeugt mStatus=IoCreateDevice(DriverObject, 0, &uniName, FILE_DEVICE_UNKNOWN, 0, TRUE, &DeviceObject); if (!NT_SUCCESS(mStatus)) { DbgPrint(("UniPort: Kann DeviceObject nicht erstellen !\n")); return mStatus; } // der Symbolische Link wird erzeugt mStatus=IoCreateSymbolicLink(&uniDOSName, &uniName); if (!NT_SUCCESS(mStatus)) { DbgPrint(("UniPort: Kann Link nicht erstellen !\n")); return mStatus; } else { DbgPrint(("UniPort: Link: %s - DosLink: %s\n", Name, DOSName)); } // der Treiber arbeitet mit Buffered IO DeviceObject->Flags |= DO_BUFFERED_IO; DbgPrint(("UniPort: Driver Entry erfolgreich abgearbeitet !\n")); return STATUS_SUCCESS; }
Der Treiber benutzt zwei Dispatch Routinen. Die erste (UniPort_DispatchCreateClose) behandelt die Aufrufe der Funktionen CreateFile und CloseHandle. In beiden Fällen wird der Status des IRP auf STATUS_ SUCCESS gesetzt und die Anfrage erfolgreich beendet.
NTSTATUS UniPort_DispatchCreateClose(IN PDEVICE_OBJECT devObj,IN PIRP Irp) { DbgPrint(("UniPort: Open / Close\n")); Irp->IoStatus.Information = 0; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; }
Die zweite Dispatch Routine verarbeitet die Aufrufe der Funktion DeviceIOControl.
Der Funktion werden über das IRP ein Ein- und ein Ausgabepuffer übergeben. Der Eingabepuffer enthält die Struktur mParamBlock, wo die Portadresse festgelegt, die Größe des Zugriffs bestimmt und im Falle eines Schreibzugriffs der Wert übergeben wird, der geschrieben werden soll. In den Ausgabepuffer wird im Falle einer Leseoperation der vom Port gelesenen Wert gespeichert. Abhängig vom Wert Size des Parameterblocks mParamBlock werden die Funktionen zum Lesen und Schreiben ausgewählt.
NTSTATUS UniPort_DispatchDeviceControl(IN PDEVICE_OBJECT devObj,IN PIRP Irp) { struct mParamBlockStruct { ULONG PortAdr; // Portadresse ULONG Size; // bestimmt die Groesse des Zugriffs // 1: Read_Port_UChar // 2: Read_Port_UShort // 4: Read_Port_ULong ULONG Value; // nur bei Write - enthaelt den Wert, // der auf den Port geschrieben werden soll } *mParamBlock; UCHAR *mBuffer; ULONG mInputBufferLength, mOutputBufferLength; ULONG mIOCTLCode; ULONG mPortAdr, mSize, mValue; NTSTATUS mStatus; PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp); // I/O Control Code lesen mIOCTLCode=IrpStack->Parameters.DeviceIoControl.IoControlCode; // Laenge der Ein-/Ausgabepuffer lesen mInputBufferLength=IrpStack->Parameters.DeviceIoControl.InputBufferLength; mOutputBufferLength=IrpStack->Parameters.DeviceIoControl.OutputBufferLength; DbgPrint(("UniPort: InputBufferLength = %x\n",mInputBufferLength)); DbgPrint(("UniPort: OutputBufferLength = %x\n",mOutputBufferLength)); DbgPrint(("UniPort: FunctionCode = %x\n",mIOCTLCode)); // Parameterblock auslesen mParamBlock=Irp->AssociatedIrp.SystemBuffer; mPortAdr=mParamBlock->PortAdr; mSize=mParamBlock->Size; if (mIOCTLCode==IOCTL_UniPort_ReadPort) { if (mOutputBufferLengthIoStatus.Information = 0; mStatus = STATUS_BUFFER_TOO_SMALL; } else { Irp->IoStatus.Information = mSize; mStatus = STATUS_SUCCESS; mBuffer=Irp->AssociatedIrp.SystemBuffer; switch (mSize) { case 1: *mBuffer=READ_PORT_UCHAR(mPortAdr); break; case 2: *mBuffer=READ_PORT_USHORT(mPortAdr); break; case 4: *mBuffer=READ_PORT_ULONG(mPortAdr); break; default: Irp->IoStatus.Information = 0; mStatus = STATUS_INVALID_PARAMETER; } } } else if (mIOCTLCode==IOCTL_UniPort_WritePort) { Irp->IoStatus.Information = 0; mStatus = STATUS_SUCCESS; mValue=mParamBlock->Value; switch (mSize) { case 1: WRITE_PORT_UCHAR(mPortAdr, mValue); break; case 2: WRITE_PORT_USHORT(mPortAdr, mValue); break; case 4: WRITE_PORT_ULONG(mPortAdr, mValue); break; default: mStatus = STATUS_INVALID_PARAMETER; } } else { Irp->IoStatus.Information = 0; mStatus = STATUS_NOT_SUPPORTED; } Irp->IoStatus.Status = mStatus; IoCompleteRequest(Irp, IO_NO_INCREMENT); return mStatus; }
Die letzte Funktion UniPort_Unload ermöglicht das Entladen des Treibers. In dieser Funktion werden die vom Treiber belegten Ressourcen wieder freigegeben. In unserem Fall müssen nur der symbolische Link und das Geräteobjekt entfernt werden.
VOID UniPort_Unload( IN PDRIVER_OBJECT DriverObject) { WCHAR LinkName[]=L"\\DosDevices\\UniPort"; UNICODE_STRING uniLinkName; NTSTATUS mStatus; DbgPrint(("UniPort: Eintritt in Unload Routine !\n")); RtlInitUnicodeString(&uniLinkName, LinkName); mStatus=IoDeleteSymbolicLink(&uniLinkName); if (!NT_SUCCESS(mStatus)) { DbgPrint(("UniPort: Kann Link nicht entfernen !\n")); } else { DbgPrint(("UniPort: Link entfernt !\n")); IoDeleteDevice(DriverObject->DeviceObject); } DbgPrint(("UniPort: Unload Ende !\n")); }
5.1.2 GiveIO
Mit dem Treiber GiveIO beschreitet Dale Roberts in [Roberts96] einen völlig anderen Weg, um den Zugriff auf Ports zu erlangen. Er hebt die Restriktionen von Windows NT teilweise auf, indem er eine Struktur des Prozessors verändert. Diese Struktur, genannt I/O Permission bit Map (IOPM), enthält für jeden Port ein Bit, und wenn dieses Bit 0 ist, wird der Zugriff auf die Ports gewährt. Ist das Bit 1, wird beim Zugriff eine Exception ausgelöst. Da Windows NT die Struktur mit 1 initialisiert, kann auf die Ports normalerweise nicht zugegriffen werden. Der Treiber benutzt mehrere undokumentierte Funktionen des Windows NT DDK, um die IOPM zu manipulieren. Die Funktionen sollen hier nur kurz beschrieben werden. In [Roberts96] werden sie ausführlicher dargestellt.
Nachdem der Treiber geladen wurde, wird mit dem Aufruf CreateFile aus der Anwendung der Zugriff auf die Ports freigeschaltet. Das Programm kann dann direkt die Assemblerbefehle IN und OUT verwenden, ohne den Umweg über einen Treiber nehmen zu müssen. Da der Treiber keine CloseHandle Funktion unterstützt, behält der Prozeß die Zugriffsrechte bis an sein Lebensende.
5.1.3 Beispielanwendung
Das Programm TestPort verwendet die beiden Treiber für die Funktionen Sound und Nosound, die den gleichnamigen Funktionen von Turbo Pascal nachgebildet sind und unter Delphi nicht mehr zur Verfügung stehen. Die Treiber werden mit Hilfe der Unit driver.pas beim Start geladen und bei Beendigung des Programms entladen.
5.2 Der Treiber K
Am Beispiel des Treibers K sollen mehrere Techniken der Treiberprogrammierung demonstriert werden. Der Treiber simuliert bei einem Tastendruck auf F12 die Tastenkombination STRG+ALT+ENTF, was einem Programm im Usermodus nicht möglich ist. Als Vorlage diente Ctrl2Cap von Mark Russinovich.
K arbeitet als layered Treiber, d.h. er legt sich über einen anderen Treiber, in unserem Fall den Tastaturtreiber, und ruft dessen Funktionen auf. In bestimmten Situationen, nämlich wenn die Taste F12 gedrückt wird, werden die Daten, die der tieferliegende Treiber zurückliefert, von K manipuliert. Dieses Ereignis wird gleichzeitig im Eventlog festgehalten, wo außerdem der Start des Treibers verzeichnet wird.
5.2.1 Die Initialisierung des Treibers
Für die Initialisierung des Treibers ist auch hier die DriverEntry Funktion zuständig. Wie üblich wird ein Geräteobjekt erzeugt und eine symbolische Verknüpfung hergestellt. Da K sich über den Tastaturtreiber legt, müssen alle Dispatch Routinen, die dieser verwendet, auch von K behandelt werden. Deshalb wird im Treiberobjekt die Einsprungadresse einer universellen Dispatch Routine eingetragen. Die Dispatch Funktion K_DispatchRead bildet eine Ausnahme, da hier die Rückgabedaten des tieferliegenden Treibers manipuliert werden sollen.
In der Debug Version des Treibers wird am Anfang der DriverEntry Funktion ein Breakpoint gesetzt. Außerdem werden die Debugmeldungen aktiviert:
// // Die Uebersetzung der Debugmeldungen (DbgPrint) soll ebenso wie // feste Breakpoints (DbgBreak) nur im "Checked-Build" erfolgen. // #if DBG #define DbgPrint(arg) DbgPrint arg #define DbgBreak() DbgBreakPoint() #else #define DbgPrint(arg) #define DbgBreak() #endif NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { PDEVICE_OBJECT DeviceObject = NULL; NTSTATUS mStatus; WCHAR Name[] = L"\\Device\\K"; UNICODE_STRING uniName; WCHAR Link[] = L"\\DosDevices\\K"; UNICODE_STRING uniLink; DbgPrint(("K: Treibereintrittspunkt\n")); // // Stop - definiertes Anhalten im Debugger - durch bedingte // Compilierung nur in der Debugversion. // DbgBreak(); // // Device Object erzeugen // RtlInitUnicodeString(&uniName,Name); mStatus=IoCreateDevice(DriverObject,0,&uniName,FILE_DEVICE_UNKNOWN, 0,TRUE,&DeviceObject ); if (NT_SUCCESS(mStatus)) { // // Erzeugen der symbolischen Verknüpfung // RtlInitUnicodeString(&uniLink,Link); mStatus=IoCreateSymbolicLink(&uniLink,&uniName); if (!NT_SUCCESS(mStatus)) DbgPrint(("K: IoCreateSymbolicLink - Fehler\n")); else DbgPrint(("K: IoCreateSymbolicLink - Erfolgreich \n")); // // Einsprungpunkte fuer alle IRPs die der Tastaturtreiber // abarbeitet // DriverObject->MajorFunction[IRP_MJ_READ] = K_DispatchRead; DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverObject->MajorFunction[IRP_MJ_FLUSH_BUFFERS] = DriverObject->MajorFunction[IRP_MJ_CLEANUP] = DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = K_DispatchGeneral; } if (!NT_SUCCESS(mStatus)) { // // Ein Fehler ist aufgetreten - Aufraeumarbeiten (Freigabe der belegten // Ressourcen usw.) // DbgPrint(("K: ein Fehler ist aufgetreten - raeume auf ...\n")); if (DeviceObject) IoDeleteDevice(DeviceObject); return mStatus; } // // K_init aufrufen und den Rückgabewert an den I/O Manager zurück geben // return K_Init(DriverObject); }
In der DriverEntry Funktion wird als letztes die Funktion K_Init aufgerufen. Diese stellt die Verbindung zum tieferliegenden Tastaturtreiber her. Dazu wird zunächst ein weiteres Geräteobjekt HookDeviceObject erstellt, das dann mit Hilfe der Funktion IoAttachDevice mit dem Geräteobjekt des Tastaturtreibers verbunden wird.
NTSTATUS K_Init(IN PDRIVER_OBJECT DriverObject) { CCHAR LowerDriverNameBuffer[64]; STRING LowerDriverNameString; UNICODE_STRING LowerDriverUnicodeString; NTSTATUS mStatus; UCHAR EventMsg[10]; // // Umwandlung des Ansi Namens in Unicode // sprintf(LowerDriverNameBuffer,"\\Device\\KeyboardClass0"); RtlInitAnsiString(&LowerDriverNameString,LowerDriverNameBuffer ); RtlAnsiStringToUnicodeString(&LowerDriverUnicodeString, &LowerDriverNameString, TRUE ); // // Erzeugen eines eigenen Device Objects fuer K // mStatus=IoCreateDevice(DriverObject,0,NULL,FILE_DEVICE_KEYBOARD,0, FALSE,&HookDeviceObject ); if (!NT_SUCCESS(mStatus)) { DbgPrint(("K: Fehler beim Erzeugen des Device Objects\n")); RtlFreeUnicodeString( &LowerDriverUnicodeString ); return STATUS_SUCCESS; } else DbgPrint(("K: Device Object wurde erzeugt \n")); HookDeviceObject->Flags |= DO_BUFFERED_IO; // // Verbinden des eigenen Device Objects mit dem tieferliegenden // Treiber, dessen Name in LowerDriverUnicodeString steht. // // kbdDevice zeigt bei Erfolg auf das Device Object des // tieferliegenden Treibers. // mStatus=IoAttachDevice(HookDeviceObject,&LowerDriverUnicodeString, &kbdDevice); if( !NT_SUCCESS(mStatus) ) { DbgPrint(("K: Verbindung zur Tastatur fehlgeschlagen !\n")); IoDeleteDevice(HookDeviceObject); RtlFreeUnicodeString(&LowerDriverUnicodeString); return STATUS_UNSUCCESSFUL; } else DbgPrint(("K: Verbindung mit Tastatur hergestellt !\n")); RtlFreeUnicodeString(&LowerDriverUnicodeString); DbgPrint(("K: Erfolgreich initialisiert !\n")); K_ReportEvent(K_MSG_DRIVER_STARTING,1,(PVOID)DriverObject,NULL,NULL,0); sprintf(EventMsg,"%x",*kbdDevice); K_ReportEvent(K_MSG_DEVOBJ_ADR,2,(PVOID)DriverObject,NULL,EventMsg,10*sizeof(UCHAR)); return STATUS_SUCCESS; }
5.2.2 Die Dispatch Routinen
Die universelle Dispatch Routine K_DispatchGeneral leitet den Aufruf an den tieferliegenden Treiber weiter, wenn die Anfrage an dessen Geräteobjekt gerichtet war, ansonsten wird STATUS_SUCCESS zurückgeliefert.
NTSTATUS K_DispatchGeneral(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp ) { PIO_STACK_LOCATION currentIrpStack = IoGetCurrentIrpStackLocation(Irp); PIO_STACK_LOCATION nextIrpStack = IoGetNextIrpStackLocation(Irp); Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; if( DeviceObject == HookDeviceObject ) { *nextIrpStack = *currentIrpStack; return IoCallDriver( kbdDevice, Irp ); } else return STATUS_SUCCESS; }
Die Dispatch Routine K_DispatchRead meldet eine Rückruffunktion an und ruft anschließend den tieferliegenden Tastaturtreiber auf.
NTSTATUS K_DispatchRead( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { PIO_STACK_LOCATION currentIrpStack = IoGetCurrentIrpStackLocation(Irp); PIO_STACK_LOCATION nextIrpStack = IoGetNextIrpStackLocation(Irp); *nextIrpStack = *currentIrpStack; IoSetCompletionRoutine(Irp,K_ReadComplete,DeviceObject,TRUE,TRUE,TRUE ); // // Rueckgabewert = Rueckgabe des unteren Tastaturtreibers // return IoCallDriver(kbdDevice,Irp); }
Die Rückruffunktion K_ReadComplete wird nach der Beendigung der Dispatch Routine des Tastaturtreibers aufgerufen. Hier wird überprüft, ob die Taste F12 losgelassen wurde, und wenn dies der Fall ist, werden im Übergabepuffer KeyData die Tastendrücke für STRG+ALT+ENTF eingetragen. Die für KeyData verwendete Struktur PKEYBOARD_INPUT_DATA ist in der Datei ntddkbd.h deklariert.
NTSTATUS K_ReadComplete(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context) { PIO_STACK_LOCATION IrpSp; PKEYBOARD_INPUT_DATA KeyData; int numKeys,i; IrpSp=IoGetCurrentIrpStackLocation(Irp); if (NT_SUCCESS(Irp->IoStatus.Status)) { // // KeyData[].MakeCode = ScanCode // KeyData[].Flags = 0 - Taste gedrückt // KeyData[].Flags = 1 - Taste losgelassen // KeyData = Irp->AssociatedIrp.SystemBuffer; numKeys = Irp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA); // // Ausgabe aller Tastencodes auf dem Debugbildschirm // for (i=0;iIoStatus.Information=6*sizeof(KEYBOARD_INPUT_DATA); } } if (Irp->PendingReturned) IoMarkIrpPending(Irp); return Irp->IoStatus.Status; }
5.2.3 Die Eventlog Funktion
Mit Hilfe der Funktion K_ReportEvent werden zu verschiedenen Zeitpunkten Einträge in das Eventlog vorgenommen. Hier wird zunächst mit IoAllocateErrorLogEntry Speicherplatz für das IO_ERROR_LOG_PACKET reserviert. Danach werden die verschiedenen Daten in das Paket eingetragen. Es kann zusätzlich ein String InsertString übergeben werden, der in die Meldung eingefügt wird. Allerdings gestaltet sich die Umwandlung von Ansi-Code in Unicode etwas komplizierter und außerdem kann sie nur bei einem IRQL gleich PASSIVE_LEVEL erfolgen, da die Funktion RtlAnsiStringToUnicodeString nur bei diesem IRQL ausgeführt werden kann.
BOOLEAN K_ReportEvent(IN NTSTATUS ErrorCode,IN ULONG UniqueErrorValue, IN PVOID IoObject,IN PIRP Irp,PUCHAR InsertString, IN ULONG StringSize) { PIO_ERROR_LOG_PACKET Packet; PIO_STACK_LOCATION IrpStack; PWCHAR pInsertionString; STRING AnsiInsertString; UNICODE_STRING UniInsertString; UCHAR PacketSize; PacketSize = sizeof(IO_ERROR_LOG_PACKET) + (StringSize+1)*sizeof(WCHAR); Packet = IoAllocateErrorLogEntry(IoObject,PacketSize); if (Packet == NULL) return FALSE; Packet->ErrorCode = ErrorCode; Packet->UniqueErrorValue = UniqueErrorValue; Packet->RetryCount = 0; Packet->SequenceNumber = 0; Packet->IoControlCode = 0; // // K benutzt das DumpData Feld nicht, daher DumpDataSize=0 // Packet->DumpDataSize = 0; if (Irp!=NULL) { IrpStack=IoGetCurrentIrpStackLocation(Irp); Packet->MajorFunctionCode = IrpStack->MajorFunction; Packet->FinalStatus = Irp->IoStatus.Status; } else { Packet->MajorFunctionCode = 0; Packet->FinalStatus = 0; } // // Konvertierung Ansi - Unicode nur im PASSIVE_LEVEL moeglich // if ((StringSize>0)&&(KeGetCurrentIrql()==PASSIVE_LEVEL)) { Packet->NumberOfStrings=1; Packet->StringOffset=sizeof(IO_ERROR_LOG_PACKET); RtlInitAnsiString(&AnsiInsertString,InsertString); RtlAnsiStringToUnicodeString(&UniInsertString,&AnsiInsertString,TRUE); pInsertionString = (PUCHAR)Packet + Packet->StringOffset; RtlCopyBytes(pInsertionString,UniInsertString.Buffer, UniInsertString.Length+sizeof(WCHAR)); RtlFreeUnicodeString(&UniInsertString); } else Packet->NumberOfStrings=0; IoWriteErrorLogEntry(Packet); return TRUE; }
5.3 Die Brunelco Timer Card
Diese ISA Karte zur Zeitmessung wird von der Firma Brunelco Electronic Engineering hergestellt. Die Karte unterstützt 8 Eingänge und bietet eine Zeitauflösung bis zu 1/10000 Sekunde. Alle Daten werden zunächst in einem internen Speicher abgelegt, was die Abfrage zeitunkritisch macht.
Da von der Herstellerfirma Windows NT nicht unterstützt wird, wurde vom Autor ein Treiber entwickelt, der den Einsatz der Karte auch unter diesem Betriebssystem ermöglicht. Die Beispielanwendung Brunelco Control Center demonstriert die Verwendung des Treibers.
5.3.1 Funktionen der Karte
Die Karte speichert bei einem Ereignis an einem der Eingänge die aktuelle Zeit auf dem internen Stack. Außerdem bietet sie folgende Funktionen:
Eine vollständige Beschreibung des Funktionsumfangs findet man in [Brunelco96].
5.3.2 Die Programmierung der Karte
Die Karte kann über zwei Ports programmiert werden. Vom Port mit der Basisadresse (Fußnote 19) können Daten gelesen werden und auf den Port mit der Basisadresse + 1 können Daten geschrieben werden.
Wenn man eine Aktion durchführen möchte, sendet man zunächst einen Funktionscode an die Karte. Danach wartet man auf eine Antwort, indem man solange den Datenport ausliest, bis dort das Bit 7 nicht mehr gesetzt ist (Fußnote 20). Der nächste Lesezugriff auf den Port liefert die gültige Antwort der Karte.
Die Tabelle 5.3.2-1 zeigt eine Auswahl der unterstützten Funktionscodes.
Funktionscode |
Aufgabe |
Rückgabewert |
01 |
Rückgabe der Quelle des aktuellen Ereignisses auf dem Stack |
1..16: normaler Eingang 20: Ereignis wurde softwareseitig ausgelöst 106: kein Ereignis verfügbar |
02 |
Stunden lesen |
0..23: Stunde 106: kein Ereignis verfügbar |
03 |
Minuten lesen |
0..59: Minuten 106: kein Ereignis verfügbar |
04 |
Sekunden lesen |
0..59: Sekunden 106: kein Ereignis verfügbar |
05 |
1/100 Sekunden lesen |
0..99: 1/100 Sekunden 106:kein Ereignis verfügbar |
06 |
1/10000 Sekunden lesen |
0..99: 1/10000 Sekunden 106: kein Ereignis verfügbar |
08 |
Stunden schreiben |
100: Acknowledge danach wird der Wert geschrieben und die Karte antwortet wieder mit 100 |
09 |
Minuten schreiben |
siehe Funktion 08 |
10 |
Sekunden schreiben |
siehe Funktion 08 |
11 |
Stop Timer |
100: Acknowledge |
12 |
(Re-)Start Timer |
100: Acknowledge |
13 |
Timer zurücksetzen (00:00:00:00:00) |
|
14 |
ein Ereignis vom Stack löschen |
100: Acknowledge 106: kein Ereignis verfügbar |
15 |
aktuelle Zeit auf den Stack legen |
100: Acknowledge Funktion 01 liefert 20 als Quelle des Ereignisses |
24 |
Karte zurücksetzen |
100: Acknowledge danach muß 219 geschrieben werden und die Karte antwortet bei Erfolg nach 2 Sekunden mit 100, andernfalls mit 103 |
29 |
Entprellung der Eingänge |
siehe Funktion 08 0 schaltet die Entprellung ab, ansonsten wird x/100 Sekunden lang kein Ereignis vom gleichen Eingang angenommen (± 1/100) |
Tabelle 5.3.2-1: Funktionscodes der Brunelco Timer Card
In [Brunelco96] werden alle Funktionscodes beschrieben.
5.3.3 Der Treiber brun.sys
Der Treiber für die Timerkarte arbeitet ähnlich wie der Uniport Treiber. Allerdings werden angepaßte IOCTL Codes benutzt und es wird zusätzlich eine Dispatch Routine für ReadFile Aufrufe zur Verfügung gestellt. Wie auch Uniport unterstützt brun.sys das dynamische Laden und Entladen. Den vollständigen Quellcode des Treibers findet man im Anhang.
Die Brunelco_DispatchDeviceControl Funktion verarbeitet Aufrufe von DeviceIOControl und unterstützt folgende IOCTL Codes:
Der Zugriff auf die Ports erfolgt in dieser Dispatch Routine über die Funktionen READ_PORT_UCHAR() und WRITE_PORT_UCHAR() des DDK.
Im Gegensatz dazu verwendet die Brunelco_DispatchRead Funktion Assemblercode, um die Daten von der Karte zu lesen.
NTSTATUS Brunelco_DispatchRead(IN PDEVICE_OBJECT devObj,IN PIRP Irp) { UCHAR *mBuffer; PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp); if (IrpStack->Parameters.Read.Length>=6) { mBuffer=Irp->AssociatedIrp.SystemBuffer; _asm { mov ebx,0 mov edx,GlobalBaseAddress mov esi,mBuffer loop1: inc ebx mov ecx,MaxLoops // max MaxLoops Durchlaeufe inc edx mov eax,ebx out dx,al // Funktion festlegen dec edx loop0: in al,dx test al,al // Test ob jns ok0 // Bit 7 gesetzt ist dec ecx jnz loop0 mov byte ptr [esi],128 // *mBuffer=128 jmp exit0 // break ok0: in al,dx mov byte ptr [esi],al // *mBuffer=READ_PORT_UCHAR() inc esi // mBuffer++ cmp ebx,6 // Funktionscode=6 ? jne loop1 // nein - dann -> loop1 exit0: } Irp->IoStatus.Information=6; Irp->IoStatus.Status=STATUS_SUCCESS; } else { Irp->IoStatus.Information = 0; Irp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; } IoCompleteRequest(Irp,IO_NO_INCREMENT); return STATUS_SUCCESS; }
Zu beachten ist, daß GlobalBaseAddress einen korrekten Wert haben muß. Der Standardwert 0x320 kann mit Hilfe der Funktion DeviceIOControl und dem IOCTL IOCTL_Brunelco_SetBaseAddress aus der Anwendung heraus geändert werden.
5.3.4 Beispielanwendung
Im Programm Brunelco Control Center wird die Verwendung des Treibers demonstriert. Im Setup können verschiedene Initialisierungen festgelegt werden. So kann zum Beispiel eingestellt werden, das beim Start die Zeit von der Karte auf die PC Uhr übernommen wird und umgekehrt. Außerdem kann der Speicher der Karte gelöscht werden und die Uhr auf 00:00:00 gesetzt werden. Weiterhin kann das Entprellen der Eingänge festgelegt werden.
Im Programm selbst werden in der Statuszeile die Uhrzeit der Karte und die Versionen des Treibers sowie des Programms angezeigt. Alle auftretenden Ereignisse werden in einem Anzeigefeld untereinander aufgelistet. Zusätzlich wird für jeden Eingang für zwei aufeinanderfolgende Ereignisse die verstrichene Zeit berechnet. Die Abbildung 5.3.4-1 zeigt einen Screenshot des Programms.
Das Auslesen der Daten erfolgt, je nach Einstellung des Wertes Use fast GetLastTimer im Setup, über die DeviceIOControl Funktion oder über ReadFile. Wie oft die Anzeige aktualisiert wird, kann ebenfalls im Setup eingestellt werden.
Abbildung 5.3.4-1: Das Brunelco Control Center