Bewährte Methoden für die Entwicklung von Surface-Teamtreibern
Einführung
Diese Richtlinien für die Treiberentwicklung wurden über viele Jahre von Treiberentwicklern bei Microsoft entwickelt. Im Laufe der Zeit, als Fahrer falsch rasiert und Lektionen gelernt wurden, wurden diese Lektionen erfasst und entwickelt, um diese Anleitung zu sein. Diese bewährten Methoden werden vom Microsoft Surface Hardware-Team verwendet, um den Gerätetreibercode zu entwickeln und zu Standard, der die einzigartigen Surface-Hardwarefunktionen unterstützt.
Wie jede Reihe von Richtlinien gibt es legitime Ausnahmen und alternative Ansätze, die gleichermaßen gültig sein werden. Erwägen Sie, diese Richtlinien in Ihre Entwicklungsstandards zu integrieren oder sie zu verwenden, um ihre Aufgaben zu starten Standard spezifische Richtlinien für Ihre Entwicklungsumgebung und Ihre individuellen Anforderungen.
Häufige Fehler von Treiberentwicklern
Behandeln von E/A
- Zugreifen auf Puffer, die aus IOCTLs abgerufen wurden, ohne die Länge zu überprüfen. Siehe Fehler beim Überprüfen der Größe von Puffern.
- Durchführen der Blockierung von E/A im Kontext eines Benutzerthreads oder zufälligen Threadkontexts. Siehe Einführung in Kernel Dispatcher-Objekte.
- Synchrone E/A an einen anderen Treiber ohne Timeout senden. Siehe synchrones Senden von E/A-Anforderungen.
- Verwenden von io IOCTLs ohne Verständnis von Sicherheitsauswirkungen. Siehe "Weder gepuffert noch direkte E/A".
- Der Rückgabestatus von WdfRequestForwardToIoQueue wird nicht überprüft oder fehler nicht ordnungsgemäß behandelt und führt zu abgebrochenen WDFREQUESTs.
- Halten Sie die WDFREQUEST außerhalb der Warteschlange in einem nicht abbruchfähigen Zustand. Siehe Verwalten von E/A-Warteschlangen, Abschließen von E/A-Anforderungen und Abbrechen von E/A-Anforderungen.
- Versuchen Sie, den Abbruch mithilfe der Funktion Mark/UnmarkCancelable zu verwalten, anstatt IoQueues zu verwenden. Siehe Framework-Warteschlangenobjekte.
- Der Unterschied zwischen dateihandle Bereinigungs- und Schließen-Vorgängen ist nicht zu kennen. Siehe Fehler beim Behandeln von Bereinigungs- und Schließvorgängen.
- Mit Blick auf potenzielle Rekursionen mit E/A-Abschluss und Erneutes Übermitteln der Abschlussroutine.
- Nicht explizit über die Energieverwaltungsattribute von WDFQUEUEs. Die Auswahl des Energiemanagements wird nicht eindeutig dokumentiert. Dies ist die Hauptursache für die Fehlerprüfung 0x9F: DRIVER_POWER_STATE_FAILURE in WDF-Treibern. Wenn das Gerät entfernt wird, löscht das Framework E/A aus der verwalteten Stromwarteschlange und der nicht energieverwalteten Warteschlange in verschiedenen Phasen des Entfernungsprozesses. Nicht energieverwaltete Warteschlangen werden gelöscht, wenn die endgültige IRP_MN_REMOVE_DEVICE empfangen wird. Wenn Sie also E/A in einer nicht energiegesteuerten Warteschlange halten, empfiehlt es sich, die E/A explizit im Kontext von EvtDeviceSelfManagedIoFlush zu löschen, um Deadlock zu vermeiden.
- Beachten Sie nicht die Regeln für die Behandlung von IRPs. Siehe Fehler beim Behandeln von Bereinigungs- und Schließvorgängen.
Synchronization
- Halten Sie Sperren für Code, der keinen Schutz benötigt. Halten Sie keine Sperre für eine gesamte Funktion, wenn nur eine kleine Anzahl von Vorgängen geschützt werden muss.
- Aufrufen von Treibern mit sperren gehaltenen Sperren. Dies ist die Hauptursache für Deadlocks.
- Verwenden von verriegelten Grundtypen, um ein Sperrschema zu erstellen, anstatt geeignete systemspezifische Sperrgrundtypen wie Mutex, Semaphor und Spinlocks zu verwenden. Siehe Einführung in Mutex-Objekte, Semaphorobjekte und Einführung in Drehsperren.
- Die Verwendung eines Spinlocks, bei dem eine Art passiver Sperre besser geeignet wäre. Siehe "Schnelle Mutexes" und "Guarded Mutexes" und "Event Objects". Weitere Perspektiven für Sperren finden Sie im Artikel "OsR": "Status der Synchronisierung".
- Aktivieren des WDF-Synchronisierungs- und Ausführungsebenenmodells ohne vollständiges Verständnis der Auswirkungen. Siehe Verwenden von Framework-Sperren. Es sei denn, Ihr Treiber ist ein monolithischer Treiber auf oberster Ebene, der direkt mit der Hardware interagiert, vermeiden Sie die Verwendung der WDF-Synchronisierung, da sie aufgrund von Rekursion zu Deadlocks führen kann.
- Abrufen von KEVENT, Semaphor, ERESOURCE, UnsafeFastMutex im Kontext mehrerer Threads ohne Eingabe kritischer Region. Dies kann zu DOS-Angriffen führen, da ein Thread, der eine dieser Sperren hält, angehalten werden kann. Siehe Einführung in Kernel Dispatcher-Objekte.
- Zuweisen von KEVENT im Threadstapel und Zurückgeben an den Aufrufer, während das EREIGNIS noch verwendet wird. Wird normalerweise mit IoBuildSyncronousFsdRequest oder IoBuildDeviceIoControlRequest verwendet. Anrufer dieser Anrufe sollten sicherstellen, dass sie sich erst vom Stapel erholen, wenn der E/A-Manager das Ereignis signalisiert hat, wenn das IRP abgeschlossen ist.
- Unbestimmtes Warten in Versandroutinen. Im Allgemeinen ist jede Art von Wartezeit in der Versandroutine eine schlechte Praxis.
- Unangemessene Überprüfung der Gültigkeit eines Objekts (wenn blah == NULL) vor dem Löschen. Dies bedeutet in der Regel, dass der Autor keinen vollständigen Überblick über den Code hat, der die Lebensdauer des Objekts steuert.
Objektverwaltung
- Nicht explizit übergeordnete WDF-Objekte. Siehe Einführung in Framework-Objekte.
- Übergeordnetes WDF-Objekt auf WDFDRIVER anstelle eines übergeordneten Elements zu einem Objekt, das eine bessere Verwaltung der Lebensdauer bietet und die Speicherauslastung optimiert. Beispiel: Übergeordnetes Ausführen von WDFREQUEST zu einem WDFDEVICE anstelle von IOTARGET. Siehe Verwenden allgemeiner Framework-Objekte, Framework-Objektlebenszyklus und Zusammenfassung von Framework-Objekten.
- Es wird kein Rundownschutz für freigegebene Speicherressourcen ausgeführt, auf die über Treiber hinweg zugegriffen wird. Siehe "ExInitializeRundownProtection"-Funktion.
- Versehentlich das gleiche Arbeitselement in die Warteschlange gestellt, während sich die vorherige bereits in der Warteschlange befindet oder bereits ausgeführt wird. Dies kann ein Problem sein, wenn der Client davon ausgeht, dass jede Arbeitsaufgabe in der Warteschlange ausgeführt wird. Siehe Verwenden von Framework WorkItems. Weitere Informationen zum Queuing WorkItems finden Sie im modul DMF_QueuedWorkitem im Projekt Driver Module Framework (DMF) - https://github.com/Microsoft/DMF.
- Warteschlangenzeitgeber vor dem Veröffentlichen der Nachricht, die der Zeitgeber verarbeiten soll. Siehe Verwenden von Zeitgebern.
- Ausführen eines Vorgangs in einem Arbeitselement, das den Abschluss eines Vorgangs blockieren oder unbestimmte Zeit in Anspruch nehmen kann.
- Entwerfen einer Lösung, die zu einer Flut von Arbeitsaufgaben führt, die in die Warteschlange gestellt werden. Es kann zu nicht reagierenden System- oder DOS-Angriffen führen, wenn der böse Typ die Aktion steuern kann (z. B. das Pumpen von E/A in einen Treiber, der eine neue Arbeitsaufgabe für jede E/A in die Warteschlange stellt). Siehe Verwenden von Framework-Arbeitselementen.
- Es wird nicht ausgeführt, dass DPC-Rückrufe der Arbeitsaufgabe abgeschlossen wurden, bevor das Objekt gelöscht wird. Siehe Richtlinien zum Schreiben von DPC-Routinen und der WdfDpcCancel-Funktion.
- Erstellen von Threads anstelle von Arbeitsaufgaben für kurze Dauer/Nichtabfragungsaufgaben. Siehe "System Worker Threads".
- Stellen Sie nicht sicher, dass Threads abgeschlossen sind, bevor Sie treiber löschen oder entladen. Weitere Informationen zur Threadausführungssynchronisierung können Sie sich den Code ansehen, der mit dem Code verknüpft ist, der DMF_Thread Modul im DmF-Projekt https://github.com/Microsoft/DMF(Driver Module Framework) zugeordnet ist .
- Verwenden eines einzelnen Treibers zum Verwalten von Geräten, die unterschiedlich sind, aber voneinander abhängig sind und globale Variablen zum Freigeben von Informationen verwenden.
Arbeitsspeicher
- Code zur passiven Ausführung nicht als PAGEABLE markieren, wenn möglich. Der Auslagerungstreibercode kann die Größe des Codebedarfs des Treibers verringern und somit Systemspeicher für andere Zwecke freigeben. Achten Sie darauf, code pageable zu markieren, die IRQL >= DISPATCH_LEVEL auslöst oder bei ausgelöstem IRQL aufgerufen werden kann. Sehen Sie , wann Code und Daten seitenfähig sein sollen, und erstellen Sie seitenfähige Treiber und erkennen Sie Code, der seitenfähig sein kann.
- Wenn Sie große Strukturen auf dem Stapel deklarieren, sollte der Heap/poolinstead verwendet werden. Weitere Informationen finden Sie unter Verwenden des KernelStack - und Zuweisungsspeichers für Systemspeicher.
- Unnötiges Nullen des WDF-Objektkontexts. Dies kann darauf hindeuten, dass nicht genügend Klarheit besteht, wenn der Arbeitsspeicher automatisch aufgehoben wird.
Allgemeine Treiberrichtlinien
- Mischen von WDM- und WDF-Grundtypen. Verwenden von WDM-Grundtypen, bei denen WDF-Grundtypen verwendet werden können. Die Verwendung von WDF-Grundtypen schützt Sie vor "gotchas", verbessert das Debuggen und macht den Treiber wichtiger als "usermode" portierbar.
- Benennen von FDOs und Erstellen symbolischer Verknüpfungen bei Bedarf. Siehe Verwalten der Steuerung des Treiberzugriffs.
- Kopieren Sie das Einfügen und Verwenden von GUIDs und anderen Konstantenwerten aus Beispieltreibern.
- Berücksichtigen Sie die Verwendung des open Source-Codes (Driver Module Framework, DMF) in Ihrem Treiberprojekt. DMF ist eine Erweiterung für WDF, die zusätzliche Funktionen für einen WDF-Treiberentwickler ermöglicht. Siehe Einführung in das Treibermodulframework.
- Verwenden der Registrierung als Prozessübergreifender Benachrichtigungsmechanismus oder als Postfach. Eine Alternative finden Sie unter DMF_NotifyUserWithEvent und DMF_NotifyUserWithRequest Module, die im DMF-Projekt verfügbar sind - https://github.com/Microsoft/DMF.
- Vorausgesetzt, alle Teile der Registrierung stehen während der frühen Startphase des Systems für den Zugriff zur Verfügung.
- Abhängigkeit von der Ladereihenfolge eines anderen Treibers oder Diensts. Da die Ladereihenfolge außerhalb der Kontrolle des Treibers geändert werden kann, kann dies zu einem Treiber führen, der anfangs funktioniert, später aber in einem unvorhersehbaren Muster fehlschlägt.
- Das Erneute Erstellen von Treiberbibliotheken, die bereits verfügbar sind, z. B. WDF, bietet PnP, die in der Unterstützung von PnP und Power Management in Ihrem Treiber beschrieben werden, oder die in der Busschnittstelle bereitgestellten Bibliotheken, wie im OsR-Artikel "Using Bus Interfaces for Driver to Driver Communication" beschrieben.
PnP/Power
- Interfacing mit einem anderen Treiber auf nicht pnp-freundliche Weise – keine Registrierung für pnp-Geräteänderungsbenachrichtigungen. Weitere Informationen finden Sie unter Registrieren der Änderungsbenachrichtigung für die Geräteschnittstelle.
- Erstellen von ACPI-Knoten zum Aufzählen von Geräten und Erstellen von Energieabhängigkeiten unter ihnen anstelle der Verwendung von Bustreibern oder System bereitgestellten Softwaregeräteerstellungsschnittstellen für PNP und Energieabhängigkeiten auf elegante Weise. Siehe Unterstützen von PnP und Power Management in Funktionstreibern.
- Kennzeichnen des Geräts, das nicht deaktiviert werden kann – Erzwingen eines Neustarts beim Treiberupdate.
- Ausblenden des Geräts im Geräte-Manager. Informationen finden Sie unter "Ausblenden von Geräten aus Geräte-Manager".
- Dabei wird davon ausgegangen, dass der Treiber nur für eine Instanz des Geräts verwendet wird.
- Es wird davon ausgegangen, dass der Treiber nie entladen wird. Siehe PnP Driver Unload Routine.
- Behandeln Sie keine unvorhersässige Benachrichtigung zur Ankunft der Schnittstelle. Dies kann passieren, und Treiber werden erwartet, dass diese Bedingung sicher behandelt wird.
- Keine S0 Idle-Energierichtlinie implementieren, die für Geräte wichtig ist, die DRIPS-Einschränkungen oder untergeordnete Elemente davon sind. Siehe Unterstützen von Leerlauf-Power-Down.
- Die Überprüfung des WdfDeviceStopIdle-Rückgabestatus führt aufgrund des WdfDeviceStopIdle/ResumeIdle-Ungleichgewichts und schließlich der 9F-Fehlerüberprüfung zu einem Strombezugsverlust.
- Nicht zu wissen, dass PrepareHardware/ReleaseHardware aufgrund der Ressourcenrebalancing mehrmals aufgerufen werden kann. Diese Rückrufe sollten auf die Initialisierung von Hardwareressourcen beschränkt sein. Siehe EVT_WDF_DEVICE_PREPARE_HARDWARE.
- Verwenden von PrepareHardware/ReleaseHardware zum Zuordnen von Softwareressourcen. Die statische Zuordnung von Softwareressourcen auf das Gerät sollte entweder in AddDevice oder in SelfManagedIoInit erfolgen, wenn die Zuordnung der erforderlichen Ressourcen mit Hardware erforderlich ist. Siehe EVT_WDF_DEVICE_SELF_MANAGED_IO_INIT.
Richtlinien für das Codieren
- Verwenden Sie keine sicheren Zeichenfolgen- und ganzzahligen Funktionen. Siehe Verwenden Tresor Zeichenfolgenfunktionen und Verwenden Tresor Ganzzahlfunktionen.
- Verwenden Sie typedefs nicht zum Definieren von Konstanten.
- Verwenden von globalen und statischen Variablen. Vermeiden Sie das Speichern pro Gerätekontext in Globalen. Globalen sind für die Gemeinsame Nutzung von Informationen über mehrere Geräteinstanzen vorgesehen. Alternativ können Sie den WDFDRIVER-Objektkontext verwenden, um Informationen über mehrere Geräteinstanzen hinweg freizugeben.
- Verwenden Sie keine beschreibenden Namen für Variablen.
- Bei Benennungsvariablen nicht konsistent – Groß-/Kleinschreibungskonsistenz. Folgt nicht dem vorhandenen Codestil, wenn Aktualisierungen an vorhandenem Code vorgenommen werden. Verwenden Sie z. B. unterschiedliche Variablennamen für allgemeine Strukturen in verschiedenen Funktionen.
- Wichtige Entwurfsentscheidungen nicht kommentieren – Energieverwaltung, Sperren, Zustandsverwaltung, Verwendung von Arbeitselementen, DPCs, Zeitgebern, globaler Ressourceneinsatz, Ressourcenvorzuordnung, komplexe Ausdrücke/bedingte Anweisungen.
- Kommentieren von Elementen, die aus dem Namen der aufgerufenen API offensichtlich sind. Machen Sie Ihren Kommentar zur englischen Sprachäquivalente des Funktionsnamens (z. B. schreiben Sie den Kommentar "Device Object erstellen" beim Aufrufen von WdfDeviceCreate).
- Erstellen Sie keine Makros mit einem Rückgabeaufruf. Siehe Funktionen (C++).
- Keine oder unvollständige Anmerkungen zum Quellcode (SOURCE Code Annotations, SAL). Siehe SAL 2.0 Anmerkungen zu Windows-Treibern.
- Verwenden von Makros anstelle von Inlinefunktionen.
- Verwenden von Makros für Konstanten anstelle von Constexpr bei Verwendung von C++
- Kompilieren Des Treibers mit dem C-Compiler anstelle des C++-Compilers, um sicherzustellen, dass Sie eine starke Typüberprüfung erhalten.
Fehlerbehandlung
- Es werden keine kritischen Treiberfehler gemeldet und das Gerät nicht ordnungsgemäß gekennzeichnet.
- Gibt keinen geeigneten NT-Fehlerstatus zurück, der in einen aussagekräftigen WIN32-Fehlerstatus übersetzt wird. Siehe Verwenden von NTSTATUS-Werten.
- Verwenden Sie NTSTATUS-Makros nicht, um den zurückgegebenen Status von Systemfunktionen zu überprüfen.
- Wird bei Bedarf nicht für Zustandsvariablen oder Flags bestätigt.
- Überprüfen Sie, ob der Zeiger gültig ist, bevor Sie darauf zugreifen, um Rennbedingungen zu umgehen.
- ASSERTION auf NULL-Zeigern. Wenn Sie versuchen, einen NULL-Zeiger für den Zugriff auf den Arbeitsspeicher zu verwenden, überprüft Windows fehler. Die Parameter der Fehlerüberprüfung stellen die erforderlichen Informationen bereit, um den Nullzeiger zu beheben. Überstunden, wenn dem Code viele nicht benötigte ASSERT-Anweisungen hinzugefügt werden, verbrauchen sie Arbeitsspeicher und verlangsamen das System.
- ASSERTING on object context pointer. Das Treiberframework garantiert, dass das Objekt immer kontextgebunden wird.
Ablaufverfolgung
- Definieren Sie keine benutzerdefinierten WPP-Typen, und verwenden Sie sie in Ablaufverfolgungsaufrufen, um lesbare Ablaufverfolgungsmeldungen zu erhalten. Siehe Hinzufügen der WPP-Softwareablaufverfolgung zu einem Windows-Treiber.
- Die IFR-Ablaufverfolgung wird nicht verwendet. Siehe Verwenden des Inflight Trace Recorder (IFR) in KMDF- und UMDF 2-Treibern.
- Aufrufen von Funktionsnamen in WPP-Ablaufverfolgungsaufrufen. WPP verfolgt bereits Funktionsnamen und Zeilennummern.
- Nicht die Verwendung von ETW-Ereignissen zum Messen der Leistung und anderer kritischer Benutzererfahrungen, die sich auf Ereignisse auswirken. Siehe Hinzufügen der Ereignisablaufverfolgung zu Kernelmodustreibern.
- Es werden keine kritischen Fehler im Ereignisprotokoll gemeldet und das Gerät ordnungsgemäß als nicht funktionsfähig markiert.
Überprüfung
- Während der Entwicklung und beim Testen wird die Treiberüberprüfung nicht mit standard- und erweiterten Einstellungen ausgeführt. Siehe Driver Verifier. In den erweiterten Einstellungen wird empfohlen, alle Regeln zu aktivieren, mit Ausnahme der Regeln, die sich auf die Simulation geringer Ressourcen beziehen. Es empfiehlt sich, die Simulationen mit niedriger Ressource isoliert auszuführen, um das Debuggen von Problemen zu vereinfachen.
- Der DevFund-Test auf dem Treiber oder der Geräteklasse, zu der der Treiber gehört, ist Teil der aktivierten erweiterten Prüfereinstellungen. Erfahren Sie, wie Sie die DevFund-Tests über die Befehlszeile ausführen.
- Nicht überprüfen, ob der Treiber HVCI-kompatibel ist. Siehe Implementieren von HVCI-Kompatibilitätscode.
- AppVerifier wird während der Entwicklung und beim Testen von Benutzermodustreibern nicht auf WUDFhost.exe ausgeführt. Siehe Application Verifier.
- Die Verwendung des Arbeitsspeichers mithilfe der Debuggererweiterung !wdfpoolusage zur Laufzeit nicht zu überprüfen, um sicherzustellen, dass WDF-Objekte nicht abgebrochen werden. Erinnerungs-, Anfragen und Arbeitsaufgaben sind häufige Opfer dieser Probleme.
- Verwenden Sie die Debuggererweiterung !wdfkd nicht, um die Objektstruktur zu überprüfen, um sicherzustellen, dass Objekte korrekt übergeordnet sind, und die Attribute wichtiger Objekte wie WDFDRIVER, WDFDEVICE, IO überprüfen.