5 Beispieltreiber

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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.

 

  1. Initialisierung des Treibers
  2. 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;
    }     
    
    
    

     

  3. Die Dispatch Routinen
  4. 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;
    }
    
    

     

  5. Die Unload Funktion
  6. 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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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 Dispatch Routinen

    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 zurück nach oben nächster Abschnitt Inhaltsverzeichnis

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

 





written 1998/1999 by Guido Wischrop
all rights reserved